From 4c56cc246ef3d8fed49b7d458ff82b8b83cf6e20 Mon Sep 17 00:00:00 2001 From: enriquedlh97 Date: Sat, 14 Feb 2026 12:56:57 -0500 Subject: [PATCH 1/3] docs: add upstream sync instructions to README --- README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.md b/README.md index 18aa2654..a6d532c0 100644 --- a/README.md +++ b/README.md @@ -166,6 +166,21 @@ contract Strategy is AMMStrategyBase { } ``` +## Syncing with Upstream + +This repo is forked from [benedictbrady/amm-challenge](https://github.com/benedictbrady/amm-challenge). To pull in the latest updates: + +```bash +git checkout main +git fetch upstream +git merge upstream/main +git push origin main +``` + +Remotes: +- `origin` — your fork (push here) +- `upstream` — original repo (pull updates from here) + ## CLI ```bash From 33fe170a36d75b519482eec436b66c70733af9aa Mon Sep 17 00:00:00 2001 From: enriquedlh97 Date: Sat, 14 Feb 2026 16:25:12 -0500 Subject: [PATCH 2/3] feat: add project infrastructure and Phase 1 static fee sweep - Add engineering standards (CLAUDE.md) and methodology doc (APPROACH.md) - Add Makefile with check, lint, test, validate, run, sweep targets - Add ruff linting and pytest config to pyproject.toml - Add static fee sweep script and baseline strategy template - Fix .gitignore: track strategies/, ignore .claude/ and .solc-select/ - Commit uv.lock for reproducible builds Phase 1 results: static fee sweep (10-100 bps) shows 80 bps as optimal static fee (edge 380) vs normalizer baseline of 344 at 30 bps. --- .gitignore | 7 +- APPROACH.md | 76 ++++++ CLAUDE.md | 82 ++++++ Makefile | 42 +++ pyproject.toml | 21 ++ scripts/sweep_static_fees.py | 108 ++++++++ strategies/README.md | 24 ++ strategies/static_fee.sol | 23 ++ uv.lock | 486 +++++++++++++++++++++++++++++++++++ 9 files changed, 867 insertions(+), 2 deletions(-) create mode 100644 APPROACH.md create mode 100644 CLAUDE.md create mode 100644 Makefile create mode 100644 scripts/sweep_static_fees.py create mode 100644 strategies/README.md create mode 100644 strategies/static_fee.sol create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 8b8c1bdf..8529165a 100644 --- a/.gitignore +++ b/.gitignore @@ -50,8 +50,11 @@ Thumbs.db .coverage htmlcov/ -# Strategies -strategies/ +# Claude Code +.claude/ + +# Solidity compiler +.solc-select/ # Misc *.log diff --git a/APPROACH.md b/APPROACH.md new file mode 100644 index 00000000..8af6bd35 --- /dev/null +++ b/APPROACH.md @@ -0,0 +1,76 @@ +# AMM Fee Strategy Challenge: Approach + +## Problem Understanding + +We operate an Automated Market Maker (AMM) that holds two tokens (X and Y) and trades with anyone using the constant product formula (x * y = k). We control **one thing**: the fees we charge on each trade (bid fee for buys, ask fee for sells). After every trade, we can update our fees. + +Two types of traders interact with our AMM: +- **Retail traders**: uninformed, pay fees willingly — this is our revenue +- **Arbitrageurs**: informed, exploit stale prices — this is our cost + +Our AMM competes against a normalizer AMM running fixed 30 bps fees. Retail flow splits between us based on who offers better prices (lower fees). If our fees are too high, we get no retail volume. + +**Objective**: maximize average **edge** (net profit) across 1,000 randomized simulations. + +## Methodology + +### Phase 1: Static Baselines + +Before building anything dynamic, establish how fixed fees perform. This tells us: +- What the baseline landscape looks like +- Whether there's a clear "best static fee" +- How much variance there is across simulations +- What edge values to expect + +**Strategies**: fixed symmetric fees at 20, 30, 40, 50, 60, 80, 100 bps. + +### Phase 2: Simple Dynamic Strategy + +Build a minimal reactive strategy based on observations from Phase 1. The goal is something interpretable — a strategy where we can explain *why* it works in 2 minutes. + +Key insight to test: big trades relative to reserves are likely arbitrage (price correction), small trades are likely retail. After a big trade, the price has been corrected, so it's safe to lower fees and attract retail. + +### Phase 3: Iterate (if time allows) + +Potential improvements to explore: +- Asymmetric bid/ask fees +- Volatility estimation from trade patterns +- Time-based decay / regime detection +- Parameter tuning + +## Observations + +### Phase 1 Results + +Static fee sweep (1000 sims each): + +``` + 10 bps → 159.19 edge + 20 bps → 282.49 + 30 bps → 343.60 ← normalizer baseline + 40 bps → 358.61 + 50 bps → 369.45 + 60 bps → 376.23 + 80 bps → 380.06 ← best static +100 bps → 374.54 +``` + +Key observations: +1. **Higher fees generally win** — 80 bps beats 30 bps by ~36 points. The arb protection from higher fees outweighs the lost retail volume. +2. **Diminishing returns** — the curve flattens from 50-80 bps and starts declining at 100 bps. At 100 bps we lose too much retail to the competitor. +3. **The normalizer is beatable with static fees alone** — simply setting 80 bps outperforms 30 bps. This suggests the normalizer's fee is suboptimal and there's room for improvement. +4. **10 bps is terrible** — undercutting aggressively gives us volume but arbs destroy us. + +This tells us: a good dynamic strategy should default to something in the 50-80 bps range and adjust from there. + +### Phase 2 Results + +*(To be filled after implementing dynamic strategy)* + +## Final Strategy + +*(To be filled)* + +## What I Would Do With More Time + +*(To be filled)* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..639b5fad --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,82 @@ +# Project Conventions + +## Overview + +This is a take-home challenge for Galaxy DeFi Trading. We design a dynamic fee strategy for a constant-product AMM. The final submission is a **single `.sol` file** uploaded to ammchallenge.com. + +## Repository Structure + +``` +contracts/src/ # Upstream strategy contracts (DO NOT modify) +strategies/ # Our experiment strategies and logs +scripts/ # Automation scripts (sweep, analysis) +amm_competition/ # Upstream simulation pipeline (DO NOT modify) +amm_sim_rs/ # Upstream Rust simulator (DO NOT modify) +tests/ # Upstream test suite (DO NOT modify) +APPROACH.md # Our methodology, observations, and rationale +``` + +## Engineering Standards + +### General + +- All code must be production-quality — clean, documented, no hacks +- Run `make check` before every commit to validate all constraints +- Keep git history clean: one logical change per commit, conventional commit messages +- Use `uv run` for all Python commands; never install globally + +### Solidity (Strategy Files) + +- Contract MUST be named `Strategy` and inherit `AMMStrategyBase` +- Imports MUST be `./AMMStrategyBase.sol` and `./IAMMStrategy.sol` (relative only) +- Fees returned in WAD precision (`bpsToWad(30)` for 30 bps) +- Fee range: 0 to 1000 bps (0% to 10%) — use `clampFee()` on all returned fees +- Storage: only `slots[0]` through `slots[31]` (32 uint256 values) +- Gas limit: 250,000 per function call +- FORBIDDEN: assembly, inline Yul, external calls, contract creation, selfdestruct +- Always validate with `make validate STRATEGY=` before running +- Document each slot's purpose with a comment at the top of the contract + +### Python (Scripts) + +- Type hints on all function signatures +- Docstrings on all public functions +- Use pathlib for file paths, not string concatenation +- Run ruff for linting: `make lint` + +### Documentation + +- `APPROACH.md` is the primary deliverable alongside the `.sol` file +- Log every experiment in `strategies/README.md` with edge scores and takeaways +- Document the "why" not just the "what" + +## Submission Constraints Checklist + +Before submitting, verify ALL of these: + +- [ ] Contract is named `Strategy` +- [ ] Inherits from `AMMStrategyBase` +- [ ] Implements `afterInitialize`, `afterSwap`, `getName` +- [ ] Imports use `./AMMStrategyBase.sol` and `./IAMMStrategy.sol` +- [ ] No assembly or inline Yul +- [ ] No external calls (`.call()`, `.delegatecall()`, etc.) +- [ ] No contract creation (`new`, `create`, `create2`) +- [ ] All fees pass through `clampFee()` +- [ ] Storage uses only `slots[0..31]` +- [ ] Each storage slot's purpose is documented +- [ ] `getName()` returns a descriptive string +- [ ] `make validate` passes +- [ ] `make run` completes without errors +- [ ] Edge score is documented in `strategies/README.md` + +## Common Commands + +```bash +make test # Run upstream test suite +make validate STRATEGY=strategies/my_strat.sol +make run STRATEGY=strategies/my_strat.sol +make run STRATEGY=strategies/my_strat.sol SIMS=10 # Quick test +make sweep # Run static fee sweep +make lint # Lint Python code +make check # Run all checks (lint + test + validate) +``` diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..eb0da830 --- /dev/null +++ b/Makefile @@ -0,0 +1,42 @@ +.PHONY: test lint validate run sweep check setup + +# Defaults +STRATEGY ?= strategies/static_fee.sol +SIMS ?= 1000 + +# ─── Setup ──────────────────────────────────────────────────────────────────── + +setup: ## Install all dependencies and build Rust simulator + uv sync + uv pip install pytest ruff + maturin develop --release --manifest-path amm_sim_rs/Cargo.toml + +# ─── Quality ────────────────────────────────────────────────────────────────── + +lint: ## Lint Python code with ruff + uv run ruff check scripts/ --fix + uv run ruff format scripts/ + +test: ## Run upstream test suite + uv run python -m pytest tests/ -v + +# ─── Strategy ───────────────────────────────────────────────────────────────── + +validate: ## Validate a strategy (STRATEGY=path/to/file.sol) + uv run amm-match validate $(STRATEGY) + +run: ## Run a strategy (STRATEGY=path/to/file.sol SIMS=1000) + uv run amm-match run $(STRATEGY) --simulations $(SIMS) + +sweep: ## Run static fee sweep across multiple fee values + uv run python scripts/sweep_static_fees.py + +# ─── Combined ───────────────────────────────────────────────────────────────── + +check: lint test validate ## Run all checks: lint, test, validate + +# ─── Help ───────────────────────────────────────────────────────────────────── + +help: ## Show this help message + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \ + awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}' diff --git a/pyproject.toml b/pyproject.toml index 288408ac..f18438a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,3 +24,24 @@ amm-match = "amm_competition.cli:main" [tool.setuptools.packages.find] where = ["."] include = ["amm_competition*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.ruff] +target-version = "py310" +line-length = 100 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify +] + +[tool.ruff.format] +quote-style = "double" diff --git a/scripts/sweep_static_fees.py b/scripts/sweep_static_fees.py new file mode 100644 index 00000000..0a202276 --- /dev/null +++ b/scripts/sweep_static_fees.py @@ -0,0 +1,108 @@ +"""Sweep static fee values and record edge scores. + +Generates temporary strategy files with fixed symmetric fees, +runs each through the simulation, and reports comparative results. +""" + +import os +import re +import subprocess +import tempfile +from pathlib import Path + +# fmt: off +TEMPLATE = """\ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {{AMMStrategyBase}} from "./AMMStrategyBase.sol"; +import {{TradeInfo}} from "./IAMMStrategy.sol"; + +contract Strategy is AMMStrategyBase {{ + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + {{ + uint256 fee = bpsToWad({bps}); + return (fee, fee); + }} + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + {{ + uint256 fee = bpsToWad({bps}); + return (fee, fee); + }} + + function getName() external pure override returns (string memory) {{ + return "Static {bps} bps"; + }} +}} +""" +# fmt: on + +STRATEGIES_DIR = Path(__file__).resolve().parent.parent / "strategies" +FEE_VALUES = [10, 20, 30, 40, 50, 60, 80, 100] +SIMULATIONS = 1000 + + +def run_strategy(bps: int) -> float | None: + """Generate a static fee strategy, run it, and return the average edge. + + Args: + bps: Fee in basis points to test. + + Returns: + Average edge score, or None if the run failed. + """ + code = TEMPLATE.format(bps=bps) + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".sol", delete=False, dir=STRATEGIES_DIR + ) as f: + f.write(code) + path = f.name + + try: + result = subprocess.run( + ["uv", "run", "amm-match", "run", path, "--simulations", str(SIMULATIONS)], + capture_output=True, + text=True, + timeout=300, + ) + output = result.stdout + result.stderr + match = re.search(r"Edge:\s*([-\d.]+)", output) + if match: + return float(match.group(1)) + print(f" Could not parse output for {bps} bps:\n{output}") + return None + except subprocess.TimeoutExpired: + print(f" Timeout for {bps} bps") + return None + finally: + os.unlink(path) + + +def main() -> None: + """Run the static fee sweep and print results.""" + print(f"Running static fee sweep ({SIMULATIONS} simulations each)\n") + print(f"{'Fee (bps)':<12} {'Edge':>10}") + print("-" * 24) + + results: dict[int, float] = {} + for bps in FEE_VALUES: + print(f"{bps:<12}", end="", flush=True) + edge = run_strategy(bps) + if edge is not None: + print(f"{edge:>10.2f}") + results[bps] = edge + else: + print(f"{'FAILED':>10}") + + print("\n--- Summary ---") + if results: + best_bps = max(results, key=results.get) + print(f"Best static fee: {best_bps} bps with edge {results[best_bps]:.2f}") + + +if __name__ == "__main__": + main() diff --git a/strategies/README.md b/strategies/README.md new file mode 100644 index 00000000..fe9fa8e1 --- /dev/null +++ b/strategies/README.md @@ -0,0 +1,24 @@ +# Experiment Log + +All strategies are run locally with `amm-match run --simulations 1000`. + +## Static Fee Sweep + +| Fee (bps) | Edge (1000 sims) | +|-----------|------------------| +| 10 | 159.19 | +| 20 | 282.49 | +| 30 | 343.60 | +| 40 | 358.61 | +| 50 | 369.45 | +| 60 | 376.23 | +| **80** | **380.06** | +| 100 | 374.54 | + +Best static fee: **80 bps** (edge 380.06). The normalizer (30 bps) scores 343.60. + +## Dynamic Strategies + +| # | Strategy | Description | Edge (1000 sims) | Takeaway | +|---|----------|-------------|-------------------|----------| +| 01 | Dynamic v1 | Reactive: raise after big trades, decay | — | — | diff --git a/strategies/static_fee.sol b/strategies/static_fee.sol new file mode 100644 index 00000000..3ea42ba9 --- /dev/null +++ b/strategies/static_fee.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Static Fee Strategy +/// @notice Fixed symmetric fee — used as baseline for comparison +contract Strategy is AMMStrategyBase { + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { + uint256 fee = bpsToWad(30); + return (fee, fee); + } + + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { + uint256 fee = bpsToWad(30); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "Static 30 bps"; + } +} diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..c0198f22 --- /dev/null +++ b/uv.lock @@ -0,0 +1,486 @@ +version = 1 +revision = 1 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.11'", + "python_full_version < '3.11'", +] + +[[package]] +name = "amm-challenge" +version = "0.1.0" +source = { editable = "." } +dependencies = [ + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "py-solc-x" }, + { name = "pyrevm" }, +] + +[package.optional-dependencies] +dev = [ + { name = "pytest" }, +] + +[package.metadata] +requires-dist = [ + { name = "numpy", specifier = ">=1.24.0" }, + { name = "py-solc-x", specifier = ">=2.0.0" }, + { name = "pyrevm", specifier = ">=0.3.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, +] +provides-extras = ["dev"] + +[[package]] +name = "certifi" +version = "2026.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/b8/6d51fc1d52cbd52cd4ccedd5b5b2f0f6a11bbf6765c782298b0f3e808541/charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d", size = 209709 }, + { url = "https://files.pythonhosted.org/packages/5c/af/1f9d7f7faafe2ddfb6f72a2e07a548a629c61ad510fe60f9630309908fef/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8", size = 148814 }, + { url = "https://files.pythonhosted.org/packages/79/3d/f2e3ac2bbc056ca0c204298ea4e3d9db9b4afe437812638759db2c976b5f/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad", size = 144467 }, + { url = "https://files.pythonhosted.org/packages/ec/85/1bf997003815e60d57de7bd972c57dc6950446a3e4ccac43bc3070721856/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8", size = 162280 }, + { url = "https://files.pythonhosted.org/packages/3e/8e/6aa1952f56b192f54921c436b87f2aaf7c7a7c3d0d1a765547d64fd83c13/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d", size = 159454 }, + { url = "https://files.pythonhosted.org/packages/36/3b/60cbd1f8e93aa25d1c669c649b7a655b0b5fb4c571858910ea9332678558/charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313", size = 153609 }, + { url = "https://files.pythonhosted.org/packages/64/91/6a13396948b8fd3c4b4fd5bc74d045f5637d78c9675585e8e9fbe5636554/charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e", size = 151849 }, + { url = "https://files.pythonhosted.org/packages/b7/7a/59482e28b9981d105691e968c544cc0df3b7d6133152fb3dcdc8f135da7a/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93", size = 151586 }, + { url = "https://files.pythonhosted.org/packages/92/59/f64ef6a1c4bdd2baf892b04cd78792ed8684fbc48d4c2afe467d96b4df57/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0", size = 145290 }, + { url = "https://files.pythonhosted.org/packages/6b/63/3bf9f279ddfa641ffa1962b0db6a57a9c294361cc2f5fcac997049a00e9c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84", size = 163663 }, + { url = "https://files.pythonhosted.org/packages/ed/09/c9e38fc8fa9e0849b172b581fd9803bdf6e694041127933934184e19f8c3/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e", size = 151964 }, + { url = "https://files.pythonhosted.org/packages/d2/d1/d28b747e512d0da79d8b6a1ac18b7ab2ecfd81b2944c4c710e166d8dd09c/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db", size = 161064 }, + { url = "https://files.pythonhosted.org/packages/bb/9a/31d62b611d901c3b9e5500c36aab0ff5eb442043fb3a1c254200d3d397d9/charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6", size = 155015 }, + { url = "https://files.pythonhosted.org/packages/1f/f3/107e008fa2bff0c8b9319584174418e5e5285fef32f79d8ee6a430d0039c/charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f", size = 99792 }, + { url = "https://files.pythonhosted.org/packages/eb/66/e396e8a408843337d7315bab30dbf106c38966f1819f123257f5520f8a96/charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d", size = 107198 }, + { url = "https://files.pythonhosted.org/packages/b5/58/01b4f815bf0312704c267f2ccb6e5d42bcc7752340cd487bc9f8c3710597/charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69", size = 100262 }, + { url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988 }, + { url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324 }, + { url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742 }, + { url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863 }, + { url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837 }, + { url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550 }, + { url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162 }, + { url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019 }, + { url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310 }, + { url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022 }, + { url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383 }, + { url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098 }, + { url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991 }, + { url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456 }, + { url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978 }, + { url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969 }, + { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425 }, + { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162 }, + { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558 }, + { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497 }, + { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240 }, + { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471 }, + { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864 }, + { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647 }, + { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110 }, + { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839 }, + { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667 }, + { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535 }, + { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816 }, + { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694 }, + { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131 }, + { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390 }, + { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091 }, + { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936 }, + { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180 }, + { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346 }, + { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874 }, + { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076 }, + { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601 }, + { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376 }, + { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825 }, + { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583 }, + { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366 }, + { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300 }, + { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465 }, + { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404 }, + { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092 }, + { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408 }, + { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746 }, + { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889 }, + { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641 }, + { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779 }, + { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035 }, + { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542 }, + { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524 }, + { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395 }, + { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680 }, + { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045 }, + { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687 }, + { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014 }, + { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044 }, + { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940 }, + { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104 }, + { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743 }, + { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402 }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740 }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008 }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484 }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245 }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048 }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542 }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301 }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320 }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050 }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034 }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185 }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149 }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620 }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963 }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743 }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616 }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579 }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005 }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570 }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548 }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521 }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866 }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455 }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348 }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362 }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103 }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382 }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462 }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618 }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511 }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783 }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506 }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190 }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828 }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006 }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765 }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736 }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719 }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072 }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213 }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632 }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532 }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885 }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467 }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144 }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217 }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014 }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935 }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122 }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143 }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260 }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225 }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374 }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391 }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754 }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476 }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666 }, +] + +[[package]] +name = "numpy" +version = "2.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/57/fd/0005efbd0af48e55eb3c7208af93f2862d4b1a56cd78e84309a2d959208d/numpy-2.4.2.tar.gz", hash = "sha256:659a6107e31a83c4e33f763942275fd278b21d095094044eb35569e86a21ddae", size = 20723651 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/44/71852273146957899753e69986246d6a176061ea183407e95418c2aa4d9a/numpy-2.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7e88598032542bd49af7c4747541422884219056c268823ef6e5e89851c8825", size = 16955478 }, + { url = "https://files.pythonhosted.org/packages/74/41/5d17d4058bd0cd96bcbd4d9ff0fb2e21f52702aab9a72e4a594efa18692f/numpy-2.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7edc794af8b36ca37ef5fcb5e0d128c7e0595c7b96a2318d1badb6fcd8ee86b1", size = 14965467 }, + { url = "https://files.pythonhosted.org/packages/49/48/fb1ce8136c19452ed15f033f8aee91d5defe515094e330ce368a0647846f/numpy-2.4.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:6e9f61981ace1360e42737e2bae58b27bf28a1b27e781721047d84bd754d32e7", size = 5475172 }, + { url = "https://files.pythonhosted.org/packages/40/a9/3feb49f17bbd1300dd2570432961f5c8a4ffeff1db6f02c7273bd020a4c9/numpy-2.4.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cb7bbb88aa74908950d979eeaa24dbdf1a865e3c7e45ff0121d8f70387b55f73", size = 6805145 }, + { url = "https://files.pythonhosted.org/packages/3f/39/fdf35cbd6d6e2fcad42fcf85ac04a85a0d0fbfbf34b30721c98d602fd70a/numpy-2.4.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f069069931240b3fc703f1e23df63443dbd6390614c8c44a87d96cd0ec81eb1", size = 15966084 }, + { url = "https://files.pythonhosted.org/packages/1b/46/6fa4ea94f1ddf969b2ee941290cca6f1bfac92b53c76ae5f44afe17ceb69/numpy-2.4.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c02ef4401a506fb60b411467ad501e1429a3487abca4664871d9ae0b46c8ba32", size = 16899477 }, + { url = "https://files.pythonhosted.org/packages/09/a1/2a424e162b1a14a5bd860a464ab4e07513916a64ab1683fae262f735ccd2/numpy-2.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2653de5c24910e49c2b106499803124dde62a5a1fe0eedeaecf4309a5f639390", size = 17323429 }, + { url = "https://files.pythonhosted.org/packages/ce/a2/73014149ff250628df72c58204822ac01d768697913881aacf839ff78680/numpy-2.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1ae241bbfc6ae276f94a170b14785e561cb5e7f626b6688cf076af4110887413", size = 18635109 }, + { url = "https://files.pythonhosted.org/packages/6c/0c/73e8be2f1accd56df74abc1c5e18527822067dced5ec0861b5bb882c2ce0/numpy-2.4.2-cp311-cp311-win32.whl", hash = "sha256:df1b10187212b198dd45fa943d8985a3c8cf854aed4923796e0e019e113a1bda", size = 6237915 }, + { url = "https://files.pythonhosted.org/packages/76/ae/e0265e0163cf127c24c3969d29f1c4c64551a1e375d95a13d32eab25d364/numpy-2.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:b9c618d56a29c9cb1c4da979e9899be7578d2e0b3c24d52079c166324c9e8695", size = 12607972 }, + { url = "https://files.pythonhosted.org/packages/29/a5/c43029af9b8014d6ea157f192652c50042e8911f4300f8f6ed3336bf437f/numpy-2.4.2-cp311-cp311-win_arm64.whl", hash = "sha256:47c5a6ed21d9452b10227e5e8a0e1c22979811cad7dcc19d8e3e2fb8fa03f1a3", size = 10485763 }, + { url = "https://files.pythonhosted.org/packages/51/6e/6f394c9c77668153e14d4da83bcc247beb5952f6ead7699a1a2992613bea/numpy-2.4.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:21982668592194c609de53ba4933a7471880ccbaadcc52352694a59ecc860b3a", size = 16667963 }, + { url = "https://files.pythonhosted.org/packages/1f/f8/55483431f2b2fd015ae6ed4fe62288823ce908437ed49db5a03d15151678/numpy-2.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40397bda92382fcec844066efb11f13e1c9a3e2a8e8f318fb72ed8b6db9f60f1", size = 14693571 }, + { url = "https://files.pythonhosted.org/packages/2f/20/18026832b1845cdc82248208dd929ca14c9d8f2bac391f67440707fff27c/numpy-2.4.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:b3a24467af63c67829bfaa61eecf18d5432d4f11992688537be59ecd6ad32f5e", size = 5203469 }, + { url = "https://files.pythonhosted.org/packages/7d/33/2eb97c8a77daaba34eaa3fa7241a14ac5f51c46a6bd5911361b644c4a1e2/numpy-2.4.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:805cc8de9fd6e7a22da5aed858e0ab16be5a4db6c873dde1d7451c541553aa27", size = 6550820 }, + { url = "https://files.pythonhosted.org/packages/b1/91/b97fdfd12dc75b02c44e26c6638241cc004d4079a0321a69c62f51470c4c/numpy-2.4.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d82351358ffbcdcd7b686b90742a9b86632d6c1c051016484fa0b326a0a1548", size = 15663067 }, + { url = "https://files.pythonhosted.org/packages/f5/c6/a18e59f3f0b8071cc85cbc8d80cd02d68aa9710170b2553a117203d46936/numpy-2.4.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e35d3e0144137d9fdae62912e869136164534d64a169f86438bc9561b6ad49f", size = 16619782 }, + { url = "https://files.pythonhosted.org/packages/b7/83/9751502164601a79e18847309f5ceec0b1446d7b6aa12305759b72cf98b2/numpy-2.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:adb6ed2ad29b9e15321d167d152ee909ec73395901b70936f029c3bc6d7f4460", size = 17013128 }, + { url = "https://files.pythonhosted.org/packages/61/c4/c4066322256ec740acc1c8923a10047818691d2f8aec254798f3dd90f5f2/numpy-2.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8906e71fd8afcb76580404e2a950caef2685df3d2a57fe82a86ac8d33cc007ba", size = 18345324 }, + { url = "https://files.pythonhosted.org/packages/ab/af/6157aa6da728fa4525a755bfad486ae7e3f76d4c1864138003eb84328497/numpy-2.4.2-cp312-cp312-win32.whl", hash = "sha256:ec055f6dae239a6299cace477b479cca2fc125c5675482daf1dd886933a1076f", size = 5960282 }, + { url = "https://files.pythonhosted.org/packages/92/0f/7ceaaeaacb40567071e94dbf2c9480c0ae453d5bb4f52bea3892c39dc83c/numpy-2.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:209fae046e62d0ce6435fcfe3b1a10537e858249b3d9b05829e2a05218296a85", size = 12314210 }, + { url = "https://files.pythonhosted.org/packages/2f/a3/56c5c604fae6dd40fa2ed3040d005fca97e91bd320d232ac9931d77ba13c/numpy-2.4.2-cp312-cp312-win_arm64.whl", hash = "sha256:fbde1b0c6e81d56f5dccd95dd4a711d9b95df1ae4009a60887e56b27e8d903fa", size = 10220171 }, + { url = "https://files.pythonhosted.org/packages/a1/22/815b9fe25d1d7ae7d492152adbc7226d3eff731dffc38fe970589fcaaa38/numpy-2.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:25f2059807faea4b077a2b6837391b5d830864b3543627f381821c646f31a63c", size = 16663696 }, + { url = "https://files.pythonhosted.org/packages/09/f0/817d03a03f93ba9c6c8993de509277d84e69f9453601915e4a69554102a1/numpy-2.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bd3a7a9f5847d2fb8c2c6d1c862fa109c31a9abeca1a3c2bd5a64572955b2979", size = 14688322 }, + { url = "https://files.pythonhosted.org/packages/da/b4/f805ab79293c728b9a99438775ce51885fd4f31b76178767cfc718701a39/numpy-2.4.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8e4549f8a3c6d13d55041925e912bfd834285ef1dd64d6bc7d542583355e2e98", size = 5198157 }, + { url = "https://files.pythonhosted.org/packages/74/09/826e4289844eccdcd64aac27d13b0fd3f32039915dd5b9ba01baae1f436c/numpy-2.4.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:aea4f66ff44dfddf8c2cffd66ba6538c5ec67d389285292fe428cb2c738c8aef", size = 6546330 }, + { url = "https://files.pythonhosted.org/packages/19/fb/cbfdbfa3057a10aea5422c558ac57538e6acc87ec1669e666d32ac198da7/numpy-2.4.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3cd545784805de05aafe1dde61752ea49a359ccba9760c1e5d1c88a93bbf2b7", size = 15660968 }, + { url = "https://files.pythonhosted.org/packages/04/dc/46066ce18d01645541f0186877377b9371b8fa8017fa8262002b4ef22612/numpy-2.4.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0d9b7c93578baafcbc5f0b83eaf17b79d345c6f36917ba0c67f45226911d499", size = 16607311 }, + { url = "https://files.pythonhosted.org/packages/14/d9/4b5adfc39a43fa6bf918c6d544bc60c05236cc2f6339847fc5b35e6cb5b0/numpy-2.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f74f0f7779cc7ae07d1810aab8ac6b1464c3eafb9e283a40da7309d5e6e48fbb", size = 17012850 }, + { url = "https://files.pythonhosted.org/packages/b7/20/adb6e6adde6d0130046e6fdfb7675cc62bc2f6b7b02239a09eb58435753d/numpy-2.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7ac672d699bf36275c035e16b65539931347d68b70667d28984c9fb34e07fa7", size = 18334210 }, + { url = "https://files.pythonhosted.org/packages/78/0e/0a73b3dff26803a8c02baa76398015ea2a5434d9b8265a7898a6028c1591/numpy-2.4.2-cp313-cp313-win32.whl", hash = "sha256:8e9afaeb0beff068b4d9cd20d322ba0ee1cecfb0b08db145e4ab4dd44a6b5110", size = 5958199 }, + { url = "https://files.pythonhosted.org/packages/43/bc/6352f343522fcb2c04dbaf94cb30cca6fd32c1a750c06ad6231b4293708c/numpy-2.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:7df2de1e4fba69a51c06c28f5a3de36731eb9639feb8e1cf7e4a7b0daf4cf622", size = 12310848 }, + { url = "https://files.pythonhosted.org/packages/6e/8d/6da186483e308da5da1cc6918ce913dcfe14ffde98e710bfeff2a6158d4e/numpy-2.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:0fece1d1f0a89c16b03442eae5c56dc0be0c7883b5d388e0c03f53019a4bfd71", size = 10221082 }, + { url = "https://files.pythonhosted.org/packages/25/a1/9510aa43555b44781968935c7548a8926274f815de42ad3997e9e83680dd/numpy-2.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5633c0da313330fd20c484c78cdd3f9b175b55e1a766c4a174230c6b70ad8262", size = 14815866 }, + { url = "https://files.pythonhosted.org/packages/36/30/6bbb5e76631a5ae46e7923dd16ca9d3f1c93cfa8d4ed79a129814a9d8db3/numpy-2.4.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d9f64d786b3b1dd742c946c42d15b07497ed14af1a1f3ce840cce27daa0ce913", size = 5325631 }, + { url = "https://files.pythonhosted.org/packages/46/00/3a490938800c1923b567b3a15cd17896e68052e2145d8662aaf3e1ffc58f/numpy-2.4.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:b21041e8cb6a1eb5312dd1d2f80a94d91efffb7a06b70597d44f1bd2dfc315ab", size = 6646254 }, + { url = "https://files.pythonhosted.org/packages/d3/e9/fac0890149898a9b609caa5af7455a948b544746e4b8fe7c212c8edd71f8/numpy-2.4.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00ab83c56211a1d7c07c25e3217ea6695e50a3e2f255053686b081dc0b091a82", size = 15720138 }, + { url = "https://files.pythonhosted.org/packages/ea/5c/08887c54e68e1e28df53709f1893ce92932cc6f01f7c3d4dc952f61ffd4e/numpy-2.4.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fb882da679409066b4603579619341c6d6898fc83a8995199d5249f986e8e8f", size = 16655398 }, + { url = "https://files.pythonhosted.org/packages/4d/89/253db0fa0e66e9129c745e4ef25631dc37d5f1314dad2b53e907b8538e6d/numpy-2.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:66cb9422236317f9d44b67b4d18f44efe6e9c7f8794ac0462978513359461554", size = 17079064 }, + { url = "https://files.pythonhosted.org/packages/2a/d5/cbade46ce97c59c6c3da525e8d95b7abe8a42974a1dc5c1d489c10433e88/numpy-2.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0f01dcf33e73d80bd8dc0f20a71303abbafa26a19e23f6b68d1aa9990af90257", size = 18379680 }, + { url = "https://files.pythonhosted.org/packages/40/62/48f99ae172a4b63d981babe683685030e8a3df4f246c893ea5c6ef99f018/numpy-2.4.2-cp313-cp313t-win32.whl", hash = "sha256:52b913ec40ff7ae845687b0b34d8d93b60cb66dcee06996dd5c99f2fc9328657", size = 6082433 }, + { url = "https://files.pythonhosted.org/packages/07/38/e054a61cfe48ad9f1ed0d188e78b7e26859d0b60ef21cd9de4897cdb5326/numpy-2.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:5eea80d908b2c1f91486eb95b3fb6fab187e569ec9752ab7d9333d2e66bf2d6b", size = 12451181 }, + { url = "https://files.pythonhosted.org/packages/6e/a4/a05c3a6418575e185dd84d0b9680b6bb2e2dc3e4202f036b7b4e22d6e9dc/numpy-2.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fd49860271d52127d61197bb50b64f58454e9f578cb4b2c001a6de8b1f50b0b1", size = 10290756 }, + { url = "https://files.pythonhosted.org/packages/18/88/b7df6050bf18fdcfb7046286c6535cabbdd2064a3440fca3f069d319c16e/numpy-2.4.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:444be170853f1f9d528428eceb55f12918e4fda5d8805480f36a002f1415e09b", size = 16663092 }, + { url = "https://files.pythonhosted.org/packages/25/7a/1fee4329abc705a469a4afe6e69b1ef7e915117747886327104a8493a955/numpy-2.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d1240d50adff70c2a88217698ca844723068533f3f5c5fa6ee2e3220e3bdb000", size = 14698770 }, + { url = "https://files.pythonhosted.org/packages/fb/0b/f9e49ba6c923678ad5bc38181c08ac5e53b7a5754dbca8e581aa1a56b1ff/numpy-2.4.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:7cdde6de52fb6664b00b056341265441192d1291c130e99183ec0d4b110ff8b1", size = 5208562 }, + { url = "https://files.pythonhosted.org/packages/7d/12/d7de8f6f53f9bb76997e5e4c069eda2051e3fe134e9181671c4391677bb2/numpy-2.4.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:cda077c2e5b780200b6b3e09d0b42205a3d1c68f30c6dceb90401c13bff8fe74", size = 6543710 }, + { url = "https://files.pythonhosted.org/packages/09/63/c66418c2e0268a31a4cf8a8b512685748200f8e8e8ec6c507ce14e773529/numpy-2.4.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d30291931c915b2ab5717c2974bb95ee891a1cf22ebc16a8006bd59cd210d40a", size = 15677205 }, + { url = "https://files.pythonhosted.org/packages/5d/6c/7f237821c9642fb2a04d2f1e88b4295677144ca93285fd76eff3bcba858d/numpy-2.4.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bba37bc29d4d85761deed3954a1bc62be7cf462b9510b51d367b769a8c8df325", size = 16611738 }, + { url = "https://files.pythonhosted.org/packages/c2/a7/39c4cdda9f019b609b5c473899d87abff092fc908cfe4d1ecb2fcff453b0/numpy-2.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b2f0073ed0868db1dcd86e052d37279eef185b9c8db5bf61f30f46adac63c909", size = 17028888 }, + { url = "https://files.pythonhosted.org/packages/da/b3/e84bb64bdfea967cc10950d71090ec2d84b49bc691df0025dddb7c26e8e3/numpy-2.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7f54844851cdb630ceb623dcec4db3240d1ac13d4990532446761baede94996a", size = 18339556 }, + { url = "https://files.pythonhosted.org/packages/88/f5/954a291bc1192a27081706862ac62bb5920fbecfbaa302f64682aa90beed/numpy-2.4.2-cp314-cp314-win32.whl", hash = "sha256:12e26134a0331d8dbd9351620f037ec470b7c75929cb8a1537f6bfe411152a1a", size = 6006899 }, + { url = "https://files.pythonhosted.org/packages/05/cb/eff72a91b2efdd1bc98b3b8759f6a1654aa87612fc86e3d87d6fe4f948c4/numpy-2.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:068cdb2d0d644cdb45670810894f6a0600797a69c05f1ac478e8d31670b8ee75", size = 12443072 }, + { url = "https://files.pythonhosted.org/packages/37/75/62726948db36a56428fce4ba80a115716dc4fad6a3a4352487f8bb950966/numpy-2.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:6ed0be1ee58eef41231a5c943d7d1375f093142702d5723ca2eb07db9b934b05", size = 10494886 }, + { url = "https://files.pythonhosted.org/packages/36/2f/ee93744f1e0661dc267e4b21940870cabfae187c092e1433b77b09b50ac4/numpy-2.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:98f16a80e917003a12c0580f97b5f875853ebc33e2eaa4bccfc8201ac6869308", size = 14818567 }, + { url = "https://files.pythonhosted.org/packages/a7/24/6535212add7d76ff938d8bdc654f53f88d35cddedf807a599e180dcb8e66/numpy-2.4.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:20abd069b9cda45874498b245c8015b18ace6de8546bf50dfa8cea1696ed06ef", size = 5328372 }, + { url = "https://files.pythonhosted.org/packages/5e/9d/c48f0a035725f925634bf6b8994253b43f2047f6778a54147d7e213bc5a7/numpy-2.4.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:e98c97502435b53741540a5717a6749ac2ada901056c7db951d33e11c885cc7d", size = 6649306 }, + { url = "https://files.pythonhosted.org/packages/81/05/7c73a9574cd4a53a25907bad38b59ac83919c0ddc8234ec157f344d57d9a/numpy-2.4.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da6cad4e82cb893db4b69105c604d805e0c3ce11501a55b5e9f9083b47d2ffe8", size = 15722394 }, + { url = "https://files.pythonhosted.org/packages/35/fa/4de10089f21fc7d18442c4a767ab156b25c2a6eaf187c0db6d9ecdaeb43f/numpy-2.4.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e4424677ce4b47fe73c8b5556d876571f7c6945d264201180db2dc34f676ab5", size = 16653343 }, + { url = "https://files.pythonhosted.org/packages/b8/f9/d33e4ffc857f3763a57aa85650f2e82486832d7492280ac21ba9efda80da/numpy-2.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2b8f157c8a6f20eb657e240f8985cc135598b2b46985c5bccbde7616dc9c6b1e", size = 17078045 }, + { url = "https://files.pythonhosted.org/packages/c8/b8/54bdb43b6225badbea6389fa038c4ef868c44f5890f95dd530a218706da3/numpy-2.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5daf6f3914a733336dab21a05cdec343144600e964d2fcdabaac0c0269874b2a", size = 18380024 }, + { url = "https://files.pythonhosted.org/packages/a5/55/6e1a61ded7af8df04016d81b5b02daa59f2ea9252ee0397cb9f631efe9e5/numpy-2.4.2-cp314-cp314t-win32.whl", hash = "sha256:8c50dd1fc8826f5b26a5ee4d77ca55d88a895f4e4819c7ecc2a9f5905047a443", size = 6153937 }, + { url = "https://files.pythonhosted.org/packages/45/aa/fa6118d1ed6d776b0983f3ceac9b1a5558e80df9365b1c3aa6d42bf9eee4/numpy-2.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:fcf92bee92742edd401ba41135185866f7026c502617f422eb432cfeca4fe236", size = 12631844 }, + { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379 }, + { url = "https://files.pythonhosted.org/packages/f4/f8/50e14d36d915ef64d8f8bc4a087fc8264d82c785eda6711f80ab7e620335/numpy-2.4.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:89f7268c009bc492f506abd6f5265defa7cb3f7487dc21d357c3d290add45082", size = 16833179 }, + { url = "https://files.pythonhosted.org/packages/17/17/809b5cad63812058a8189e91a1e2d55a5a18fd04611dbad244e8aeae465c/numpy-2.4.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6dee3bb76aa4009d5a912180bf5b2de012532998d094acee25d9cb8dee3e44a", size = 14889755 }, + { url = "https://files.pythonhosted.org/packages/3e/ea/181b9bcf7627fc8371720316c24db888dcb9829b1c0270abf3d288b2e29b/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:cd2bd2bbed13e213d6b55dc1d035a4f91748a7d3edc9480c13898b0353708920", size = 5399500 }, + { url = "https://files.pythonhosted.org/packages/33/9f/413adf3fc955541ff5536b78fcf0754680b3c6d95103230252a2c9408d23/numpy-2.4.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:cf28c0c1d4c4bf00f509fa7eb02c58d7caf221b50b467bcb0d9bbf1584d5c821", size = 6714252 }, + { url = "https://files.pythonhosted.org/packages/91/da/643aad274e29ccbdf42ecd94dafe524b81c87bcb56b83872d54827f10543/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e04ae107ac591763a47398bb45b568fc38f02dbc4aa44c063f67a131f99346cb", size = 15797142 }, + { url = "https://files.pythonhosted.org/packages/66/27/965b8525e9cb5dc16481b30a1b3c21e50c7ebf6e9dbd48d0c4d0d5089c7e/numpy-2.4.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:602f65afdef699cda27ec0b9224ae5dc43e328f4c24c689deaf77133dbee74d0", size = 16727979 }, + { url = "https://files.pythonhosted.org/packages/de/e5/b7d20451657664b07986c2f6e3be564433f5dcaf3482d68eaecd79afaf03/numpy-2.4.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be71bf1edb48ebbbf7f6337b5bfd2f895d1902f6335a5830b20141fc126ffba0", size = 12502577 }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366 }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538 }, +] + +[[package]] +name = "py-solc-x" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/34/8d/f30778d5f08387cc3f7576aedf4a1e6841caeb8d6d600bb070aa9480b689/py_solc_x-2.0.5.tar.gz", hash = "sha256:2d8440d2be8a5577137fb2313ee211b5b35f36c4c922f274192d34f401616e83", size = 78761 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/37/60ccc88d918d8f1cd218e413fb6263afcde350dc259fb2ba413d9f7d1051/py_solc_x-2.0.5-py3-none-any.whl", hash = "sha256:d6c24b699a7db8f7bf731f32dcfe8c43d7ea9d181191b2a3b7cb5e60395dd449", size = 18750 }, +] + +[[package]] +name = "pygments" +version = "2.19.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 }, +] + +[[package]] +name = "pyrevm" +version = "0.3.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/89/128ca575eb4bb8163e50ca8a46d7da0ae43a5ca5e5deb4574eac3a79a7fc/pyrevm-0.3.7.tar.gz", hash = "sha256:9fc770ec166a44a037da9a6dcdfa3e1757dbd3e0a2cdee97d7679ff5ce6a8f74", size = 60806 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/ba/d99d87e769e9884690952c64a5d0875ab5e3b1ca9c048a7964aacedc73b4/pyrevm-0.3.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9a167214a036e36f67424a0776ebdfaf5e17f7643e602f935c3301f20b950361", size = 3977666 }, + { url = "https://files.pythonhosted.org/packages/77/72/531b44c85b3e2c79269c27eca4aba53fc1f72ad6b2be7dd992e7dcbbbb7b/pyrevm-0.3.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c5c02592c5079b60764d2a38e9277f729f311a32b22129e239023c0849a1b25e", size = 3886069 }, + { url = "https://files.pythonhosted.org/packages/3d/f3/62162bbe7d1f1adc2b95c5e26d1f7c23a9afd2fa29ff1051ea96386d8ee0/pyrevm-0.3.7-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca46087037c438d32228b41321d1858499ed6f88aa476eafaf7dc4edb685ea67", size = 4300027 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/646bcef88f298f7b35af4e023bc8eb3e345deb677f9bc32ebc0c601e0943/pyrevm-0.3.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:477b9b21c638179691f7bb0e1c7eb9d84c03ba90dc0e40cf50cf416e96d0ff4d", size = 4318401 }, + { url = "https://files.pythonhosted.org/packages/8d/49/9244c64e3f9c14eceb8d55595bbe3c0864527f025160a8c552070227c65f/pyrevm-0.3.7-cp310-none-win_amd64.whl", hash = "sha256:989b17e24ff5ee47985d996bb7e0d9f38cc2079f30e5d9a960436422008d31ed", size = 3830669 }, + { url = "https://files.pythonhosted.org/packages/72/6a/3dc9c0d244786ab7d529403bd235c564f3c6607cb6dfac63fd8a4424b5cf/pyrevm-0.3.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:e43aae42944bfd35ba37f01aec2cd092440865581af44e4580ffc153e30f82cf", size = 3977822 }, + { url = "https://files.pythonhosted.org/packages/e0/ad/2bd8a51d61457c0c9a1e265aca46957fcf294e231f57fb0c2acf65fbe035/pyrevm-0.3.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f5b606806c1e65cbc2633ac13d819a04bce342d68335655ceb9a1bc51fed4447", size = 3886176 }, + { url = "https://files.pythonhosted.org/packages/ad/ce/56d83dd012561ee21f99ed3db5de6602e7328092542e4e9f0d203cbffb40/pyrevm-0.3.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2d68a13be420d0f129e37c1cf0e2868812ce17e45749f93f97ef07980c765341", size = 4299865 }, + { url = "https://files.pythonhosted.org/packages/ef/5c/d5b3e55b827f3e1352ced97347c0e4467a068be2f346b16dc73a5badf5bd/pyrevm-0.3.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d46fdc51cb6c06b5c2ad85b1c2160b35efc74d27baf318167360d2fb6636aa26", size = 4318428 }, + { url = "https://files.pythonhosted.org/packages/c5/d9/1bd728ce052f9e4b0e81ea05a70df84243d5dcb4a6c95acdeef1731c38c5/pyrevm-0.3.7-cp311-none-win_amd64.whl", hash = "sha256:cfc698975f47cf62f310d452e57148c81ac0463f19f04b8fa34b135170394c33", size = 3830676 }, + { url = "https://files.pythonhosted.org/packages/71/84/35d3bcd79e80fbbb4d92d1e1539babcb121b2efbc7906d89bfeaa9c3bb1e/pyrevm-0.3.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:456411132c5431ccc58e59353325e43736ea4bd4941fd8b17c7c14583afda031", size = 3977492 }, + { url = "https://files.pythonhosted.org/packages/9c/55/279f3521ca43c7561c2f54cdd23b6fb91e65ea1e14d22d8482e01b32f13b/pyrevm-0.3.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b149fe2b3e79fad7c638de6f3d5761d4c97fa35fd40a552337af8305c57a70dd", size = 3886572 }, + { url = "https://files.pythonhosted.org/packages/e0/ab/2caf460c88de7a582277f0ecb814188ab7ff6f209eff0908f76aba851e78/pyrevm-0.3.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a958e65c694ca99a22fe9283d57b83828902a808114f98f033490bbf0526bc75", size = 4302963 }, + { url = "https://files.pythonhosted.org/packages/7c/6c/17f60815e678474fa8bb36363e4a9f336fa444abb983c2a1e4bb08f681ae/pyrevm-0.3.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d055860c3352d6bc180b80255d8563f0b3c0fe6ce62b5c2a52e1bc7ecc4fad4f", size = 4317826 }, + { url = "https://files.pythonhosted.org/packages/b4/99/a039c1c1cabf79f551c628fc4cccca06b113126489d9c2e3deb43c355f5f/pyrevm-0.3.7-cp312-none-win_amd64.whl", hash = "sha256:e5ce297d397c60c50f1c46f75d04620efa36190ef3b91523769d3631dfcefed2", size = 3825155 }, + { url = "https://files.pythonhosted.org/packages/fe/fb/364072806feb7d507c8362fa44a4c530d2767a30c7e400926c3cf8478e72/pyrevm-0.3.7-cp313-none-win_amd64.whl", hash = "sha256:288b6408c538670b21bffa36ae4b44fb7f252cf7355df73f46c9e044bf6c7225", size = 3825154 }, + { url = "https://files.pythonhosted.org/packages/8c/f5/93a4d93333fa160b617d91abde66b058607ba30458876a73356097eae722/pyrevm-0.3.7-cp314-none-win_amd64.whl", hash = "sha256:438686704d2705f7d7e4c833002da62f4e15e28006633a6aca3ae795cfb0ba22", size = 3825150 }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801 }, +] + +[[package]] +name = "requests" +version = "2.32.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738 }, +] + +[[package]] +name = "tomli" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663 }, + { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469 }, + { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039 }, + { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007 }, + { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875 }, + { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271 }, + { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770 }, + { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626 }, + { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842 }, + { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894 }, + { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053 }, + { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481 }, + { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720 }, + { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014 }, + { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820 }, + { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712 }, + { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296 }, + { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553 }, + { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915 }, + { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038 }, + { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245 }, + { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335 }, + { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962 }, + { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396 }, + { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530 }, + { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227 }, + { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748 }, + { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725 }, + { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901 }, + { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375 }, + { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639 }, + { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897 }, + { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697 }, + { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567 }, + { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556 }, + { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014 }, + { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339 }, + { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490 }, + { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398 }, + { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515 }, + { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806 }, + { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340 }, + { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106 }, + { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504 }, + { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561 }, + { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477 }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614 }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584 }, +] From d52d5300440fdabca9a07d1fd32ab83543e69bf3 Mon Sep 17 00:00:00 2001 From: enriquedlh97 Date: Sun, 15 Feb 2026 22:53:29 -0500 Subject: [PATCH 3/3] feat: add dual-EWMA vol regime strategy (edge 407.97 vs 380.06 static) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Systematic search across 18 candidates in 4 strategy families using paired-seed evaluation (same seeds, per-seed deltas) found that a dual-EWMA volatility ratio (lambda=0.90/0.98) with ±3 bps regime adjustment beats static 80 bps by +27.92 edge points (t=127.91, 100% win rate, 1000 sims). Key findings: - Static asymmetry: zero effect - Absolute EWMA vs nominal variance: badly miscalibrated (delta -45) - Directional adjustment: harmful (delta -1 to -5) - Dual-EWMA ratio: self-calibrating, +28 delta across all variantsv --- APPROACH.md | 118 +++- scripts/paired_eval.py | 898 ++++++++++++++++++++++++++ scripts/sweep_asymmetric.py | 137 ++++ strategies/README.md | 60 +- strategies/_best_asym.sol | 11 + strategies/_fine_77.sol | 10 + strategies/_fine_78.sol | 10 + strategies/_fine_79.sol | 10 + strategies/_fine_81.sol | 10 + strategies/_fine_82.sol | 10 + strategies/_fine_83.sol | 10 + strategies/_sweep_75.sol | 10 + strategies/_sweep_78.sol | 10 + strategies/_sweep_80.sol | 10 + strategies/_sweep_82.sol | 10 + strategies/_sweep_85.sol | 10 + strategies/_sweep_88.sol | 10 + strategies/_sweep_90.sol | 10 + strategies/_test_asym.sol | 11 + strategies/asymmetric_v3a.sol | 40 ++ strategies/asymmetric_v3b.sol | 35 + strategies/combined_v4c.sol | 91 +++ strategies/dir_adjust_v5.sol | 69 ++ strategies/directional_v2b.sol | 98 +++ strategies/escalating_v3c.sol | 67 ++ strategies/ewma_vol_v2c.sol | 106 +++ strategies/impact_reactive_v1.sol | 80 +++ strategies/inventory_linear_v4b.sol | 104 +++ strategies/reverse_asym_v4a.sol | 44 ++ strategies/static_asym.sol | 40 ++ strategies/timestamp_two_tier_v2a.sol | 67 ++ strategies/timestamp_v3d.sol | 51 ++ strategies/vol_regime_dir_v5.sol | 121 ++++ strategies/vol_responsive_v5.sol | 88 +++ 34 files changed, 2456 insertions(+), 10 deletions(-) create mode 100644 scripts/paired_eval.py create mode 100644 scripts/sweep_asymmetric.py create mode 100644 strategies/_best_asym.sol create mode 100644 strategies/_fine_77.sol create mode 100644 strategies/_fine_78.sol create mode 100644 strategies/_fine_79.sol create mode 100644 strategies/_fine_81.sol create mode 100644 strategies/_fine_82.sol create mode 100644 strategies/_fine_83.sol create mode 100644 strategies/_sweep_75.sol create mode 100644 strategies/_sweep_78.sol create mode 100644 strategies/_sweep_80.sol create mode 100644 strategies/_sweep_82.sol create mode 100644 strategies/_sweep_85.sol create mode 100644 strategies/_sweep_88.sol create mode 100644 strategies/_sweep_90.sol create mode 100644 strategies/_test_asym.sol create mode 100644 strategies/asymmetric_v3a.sol create mode 100644 strategies/asymmetric_v3b.sol create mode 100644 strategies/combined_v4c.sol create mode 100644 strategies/dir_adjust_v5.sol create mode 100644 strategies/directional_v2b.sol create mode 100644 strategies/escalating_v3c.sol create mode 100644 strategies/ewma_vol_v2c.sol create mode 100644 strategies/impact_reactive_v1.sol create mode 100644 strategies/inventory_linear_v4b.sol create mode 100644 strategies/reverse_asym_v4a.sol create mode 100644 strategies/static_asym.sol create mode 100644 strategies/timestamp_two_tier_v2a.sol create mode 100644 strategies/timestamp_v3d.sol create mode 100644 strategies/vol_regime_dir_v5.sol create mode 100644 strategies/vol_responsive_v5.sol diff --git a/APPROACH.md b/APPROACH.md index 8af6bd35..45e0c8b8 100644 --- a/APPROACH.md +++ b/APPROACH.md @@ -30,13 +30,21 @@ Build a minimal reactive strategy based on observations from Phase 1. The goal i Key insight to test: big trades relative to reserves are likely arbitrage (price correction), small trades are likely retail. After a big trade, the price has been corrected, so it's safe to lower fees and attract retail. -### Phase 3: Iterate (if time allows) +### Phase 3: Systematic Parameter Search -Potential improvements to explore: -- Asymmetric bid/ask fees -- Volatility estimation from trade patterns -- Time-based decay / regime detection -- Parameter tuning +Rather than guessing improvements, run a theory-constrained search across four +strategy families, using paired-seed evaluation (same seeds for candidate and +baseline) to cut through noise. The search protocol has three stages: + +1. **Broad screen** (200 sims): test all ~18 candidates, promote top 8 +2. **Narrow** (500 sims): re-evaluate top 8, promote top 3 +3. **Final validation** (1000 sims): confirm top 3 with high statistical power + +**Four families tested:** +- **Static Asymmetric**: different bid/ask fees around 80 bps +- **Volatility-Responsive**: EWMA of squared returns vs nominal variance +- **Directional**: adjust fees based on trade direction +- **Combined**: dual-EWMA volatility ratio + regime detection + optional direction ## Observations @@ -65,12 +73,104 @@ This tells us: a good dynamic strategy should default to something in the 50-80 ### Phase 2 Results -*(To be filled after implementing dynamic strategy)* +**ImpactReactive_v1**: bump fees to 150 bps after large trades (>0.5% of reserves), decay 3 bps per trade back to 80 bps base. + +Result: **379.78 edge** (1000 sims) — essentially matches static 80 bps (380.06) but doesn't beat it. + +Key insight from deeper analysis: +- At 80 bps, the no-arb band is ~±0.8%, requiring ~8.5σ price moves (extremely rare with σ ≈ 0.094%/step) +- **Arbs barely trade at this fee level** — the impact detection rarely triggers +- The routing formula is nearly insensitive to fee differences in the 60-100 bps range (~0.04% volume difference) +- **Per-trade fee revenue dominates over volume share** — this is why higher static fees win + +This tells us: simply reacting to trade impact is insufficient because the signal (arb trades) is too rare. We need a different approach — either exploit the within-step timing (arb → retail ordering) or find a signal that fires more often. + +### Phase 3 Results + +The paired search revealed a striking pattern: + +**What didn't work:** +- **Static Asymmetry** (delta ≈ 0): Asymmetric bid/ask fees around 80 bps have + no measurable effect. The routing formula is insensitive to small fee differences + at this level. +- **Volatility-Responsive with absolute EWMA** (delta ≈ -45): Using EWMA variance + against a fixed nominal variance badly miscalibrates — the actual per-trade + squared return (~1.6e-5) is much higher than the nominal (2e-6), so the fee + is systematically pushed above optimal. +- **Directional Adjustment** (delta ≈ -1 to -5, scaling linearly with adjustment + size): This creates a cheaper side that arbs can exploit on the return leg of + round trips. The penalty scales precisely with the adjustment amount. + +**What worked spectacularly:** +- **Dual-EWMA Volatility Ratio** (delta ≈ +28): Using the ratio of a fast EWMA + (lambda=0.90) to a slow EWMA (lambda=0.98) of squared spot returns. This + self-calibrates per simulation — no need for a nominal variance constant. + - After large trades: shortVar spikes → fee increases → arb protection + - During quiet periods: shortVar decays → fee decreases → attracts retail + - Adding a ±3 bps regime adjustment (based on shortVar vs longVar) provides + an additional ~2-3 edge points + - Removing directional adjustment adds ~2 edge points + +**Key insight**: Static 80 bps is the best *unconditional* fee. But the dual-EWMA +strategy provides *conditional* optimization — it adapts to each simulation's +realized volatility, effectively choosing a better fee for each market environment. +The hyperparameter variance (sigma: 0.000882-0.001008, retail rate: 0.6-1.0) +means different simulations have different optimal fees, and the strategy learns +which regime it's in. ## Final Strategy -*(To be filled)* +**DualEWMA_VolRegime_80_3** (`strategies/vol_regime_dir_v5.sol`) + +Edge: **407.97** (1000 sims) — beats static 80 bps (380.06) by +27.92 points +(t=127.91, p≈0, 100% win rate across 1000 paired seeds). + +### How it works + +Two layers on top of an 80 bps base fee: + +1. **Vol scaling**: `fee = baseFee * (0.5 + 0.5 * shortVar/longVar)` + - `shortVar`: fast EWMA (lambda=0.90) of squared spot price returns + - `longVar`: slow EWMA (lambda=0.98) of the same + - Self-calibrating: no absolute variance threshold needed + - Clamped to [40, 160] bps range + +2. **Regime detection**: `fee += 3 bps` when shortVar > longVar (vol increasing), + `fee -= 3 bps` otherwise (vol stable/decreasing) + +### Storage (5 of 32 slots used) + +| Slot | Purpose | +|------|---------| +| 0 | lastTimestamp | +| 1 | prevSpotPrice (WAD) | +| 2 | shortTermVar (fast EWMA, WAD-scaled) | +| 3 | longTermVar (slow EWMA, WAD-scaled) | +| 4 | initialized flag | + +### Why no directional adjustment? + +The search showed directional adjustment consistently hurts (delta -1 to -5 bps +per bps of adjustment). The mechanism: after a buy, lowering the ask fee gives +arbs a cheaper path on the return leg. Since retail direction is random (50/50), +the asymmetry only benefits informed flow. ## What I Would Do With More Time -*(To be filled)* +1. **Finer parameter grid**: The search tested discrete lambda values (0.90, 0.98). + A continuous optimization over lambda_short in [0.85, 0.95] and lambda_long in + [0.96, 0.99] could find the exact optimum. + +2. **Regime-dependent base fee**: Instead of a fixed 80 bps base, use the long-term + variance estimate to shift the base (e.g., 75 bps in low-vol, 85 bps in high-vol). + +3. **Multi-step lookback**: The current strategy only uses the latest trade's spot + change. Tracking cumulative price drift over multiple trades could detect trending + markets earlier. + +4. **Order flow imbalance**: Track the ratio of buy vs sell volume over a window. + Persistent imbalance suggests informed flow, warranting higher fees on the + overrepresented side. + +5. **Adaptive regime adjustment**: Instead of fixed ±3 bps, scale the regime + adjustment by the magnitude of the shortVar/longVar ratio deviation. diff --git a/scripts/paired_eval.py b/scripts/paired_eval.py new file mode 100644 index 00000000..fe4f5a5a --- /dev/null +++ b/scripts/paired_eval.py @@ -0,0 +1,898 @@ +"""Paired-seed evaluation harness for AMM strategies. + +Runs candidate and baseline strategies on identical seeds and computes +per-seed edge deltas to dramatically reduce variance in comparisons. + +Usage: + # Compare a candidate against static 80 bps (default baseline): + uv run python scripts/paired_eval.py strategies/my_strat.sol --sims 200 + + # Compare two .sol files: + uv run python scripts/paired_eval.py strategies/a.sol --baseline strategies/b.sol --sims 200 + + # Run full search protocol across all candidate families: + uv run python scripts/paired_eval.py --search \ + --stage1-sims 200 --stage2-sims 500 --stage3-sims 1000 +""" + +import argparse +import math +import sys +from pathlib import Path + +import numpy as np + +import amm_sim_rs +from amm_competition.competition.config import ( + BASELINE_SETTINGS, + BASELINE_VARIANCE, + baseline_nominal_retail_rate, + baseline_nominal_retail_size, + baseline_nominal_sigma, +) +from amm_competition.competition.match import MatchRunner +from amm_competition.evm.adapter import EVMStrategyAdapter +from amm_competition.evm.baseline import load_vanilla_strategy + +STRATEGIES_DIR = Path(__file__).resolve().parent.parent / "strategies" + +# ── Strategy Templates ─────────────────────────────────────────────────────── + +STATIC_ASYM_TEMPLATE = """\ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {{AMMStrategyBase}} from "./AMMStrategyBase.sol"; +import {{TradeInfo}} from "./IAMMStrategy.sol"; + +/// @title Static Asymmetric Fee Strategy +/// @notice Fixed asymmetric bid/ask fees +/// Storage: none needed +contract Strategy is AMMStrategyBase {{ + uint256 internal constant BID_FEE = {bid_bps} * BPS; + uint256 internal constant ASK_FEE = {ask_bps} * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + {{ + return (clampFee(BID_FEE), clampFee(ASK_FEE)); + }} + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + {{ + return (clampFee(BID_FEE), clampFee(ASK_FEE)); + }} + + function getName() external pure override returns (string memory) {{ + return "StaticAsym_{bid_bps}_{ask_bps}"; + }} +}} +""" + +STATIC_SYM_TEMPLATE = """\ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {{AMMStrategyBase}} from "./AMMStrategyBase.sol"; +import {{TradeInfo}} from "./IAMMStrategy.sol"; + +contract Strategy is AMMStrategyBase {{ + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + {{ + uint256 fee = bpsToWad({bps}); + return (fee, fee); + }} + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + {{ + uint256 fee = bpsToWad({bps}); + return (fee, fee); + }} + + function getName() external pure override returns (string memory) {{ + return "Static_{bps}"; + }} +}} +""" + +VOL_RESPONSIVE_TEMPLATE = """\ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {{AMMStrategyBase}} from "./AMMStrategyBase.sol"; +import {{TradeInfo}} from "./IAMMStrategy.sol"; + +/// @title Volatility-Responsive Fee Strategy +/// @notice Adjusts fee based on EWMA of squared spot price returns. +/// Fee = baseFee * (0.5 + 0.5 * ewmaVar / nominalVar), clamped. +/// +/// Storage layout: +/// slots[0] = prevSpotPrice (WAD) +/// slots[1] = ewmaVariance (WAD-scaled) +/// slots[2] = lastTimestamp +/// slots[3] = initialized (0 or 1) +contract Strategy is AMMStrategyBase {{ + uint256 internal constant BASE_FEE = {base_bps} * BPS; + uint256 internal constant LAMBDA = {lambda_e18}; + uint256 internal constant ONE_MINUS_LAMBDA = WAD - LAMBDA; + uint256 internal constant NOMINAL_VAR = {nominal_var}; + uint256 internal constant VOL_SCALE = {vol_scale_e18}; + uint256 internal constant HALF_WAD = WAD / 2; + uint256 internal constant MIN_MULTIPLIER = HALF_WAD; + uint256 internal constant MAX_MULTIPLIER = 2 * WAD; + + function afterInitialize(uint256 initialX, uint256 initialY) + external override returns (uint256 bidFee, uint256 askFee) + {{ + slots[0] = wdiv(initialY, initialX); + slots[1] = NOMINAL_VAR; + slots[3] = 1; + return (clampFee(BASE_FEE), clampFee(BASE_FEE)); + }} + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + {{ + uint256 spotPrice = wdiv(trade.reserveY, trade.reserveX); + uint256 prevSpot = slots[0]; + uint256 ewmaVar = slots[1]; + + if (slots[3] == 1 && prevSpot > 0) {{ + uint256 delta = absDiff(spotPrice, prevSpot); + uint256 sqReturn = wdiv(wmul(delta, delta), wmul(prevSpot, prevSpot)); + ewmaVar = wmul(LAMBDA, ewmaVar) + wmul(ONE_MINUS_LAMBDA, sqReturn); + slots[1] = ewmaVar; + }} + + slots[0] = spotPrice; + slots[2] = trade.timestamp; + + uint256 ratio = wdiv(ewmaVar, NOMINAL_VAR); + uint256 scaledRatio = wmul(VOL_SCALE, ratio); + uint256 multiplier = HALF_WAD + scaledRatio / 2; + if (multiplier < MIN_MULTIPLIER) multiplier = MIN_MULTIPLIER; + if (multiplier > MAX_MULTIPLIER) multiplier = MAX_MULTIPLIER; + + uint256 fee = clampFee(wmul(BASE_FEE, multiplier)); + return (fee, fee); + }} + + function getName() external pure override returns (string memory) {{ + return "VolResponsive_{base_bps}"; + }} +}} +""" + +DIR_ADJUST_TEMPLATE = """\ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {{AMMStrategyBase}} from "./AMMStrategyBase.sol"; +import {{TradeInfo}} from "./IAMMStrategy.sol"; + +/// @title Directional Adjustment Fee Strategy +/// @notice Adjusts fees based on trade direction — charges more for repeated +/// same-direction trades (likely informed flow). +/// +/// Storage layout: +/// slots[0] = lastTradeIsBuy (0 or 1) +/// slots[1] = initialized (0 or 1) +contract Strategy is AMMStrategyBase {{ + uint256 internal constant BASE_FEE = {base_bps} * BPS; + uint256 internal constant DIR_ADJUST = {dir_bps} * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + {{ + return (clampFee(BASE_FEE), clampFee(BASE_FEE)); + }} + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + {{ + uint256 fee = BASE_FEE; + uint256 bidF; + uint256 askF; + + if (slots[1] == 1) {{ + if (trade.isBuy) {{ + bidF = fee + DIR_ADJUST; + askF = fee > DIR_ADJUST ? fee - DIR_ADJUST : 0; + }} else {{ + bidF = fee > DIR_ADJUST ? fee - DIR_ADJUST : 0; + askF = fee + DIR_ADJUST; + }} + }} else {{ + bidF = fee; + askF = fee; + }} + + slots[0] = trade.isBuy ? 1 : 0; + slots[1] = 1; + + return (clampFee(bidF), clampFee(askF)); + }} + + function getName() external pure override returns (string memory) {{ + return "DirAdjust_{base_bps}_{dir_bps}"; + }} +}} +""" + +COMBINED_TEMPLATE = """\ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {{AMMStrategyBase}} from "./AMMStrategyBase.sol"; +import {{TradeInfo}} from "./IAMMStrategy.sol"; + +/// @title Combined Vol+Regime+Direction Strategy +/// @notice Three-layer fee adjustment: +/// 1. Volatility: scale base fee by EWMA variance ratio +/// 2. Regime: shift fee based on short/long variance ratio +/// 3. Direction: asymmetric adjustment based on last trade direction +/// +/// Storage layout: +/// slots[0] = lastTimestamp +/// slots[1] = prevSpotPrice (WAD) +/// slots[2] = shortTermVar (WAD, fast EWMA lambda=0.90) +/// slots[3] = longTermVar (WAD, slow EWMA lambda=0.98) +/// slots[4] = lastTradeIsBuy (0 or 1) +/// slots[5] = initialized (0 or 1) +contract Strategy is AMMStrategyBase {{ + uint256 internal constant BASE_FEE = {base_bps} * BPS; + uint256 internal constant VOL_SCALE = {vol_scale_e18}; + uint256 internal constant REGIME_ADJUST = {regime_bps} * BPS; + uint256 internal constant DIR_ADJUST = {dir_bps} * BPS; + + uint256 internal constant LAMBDA_SHORT = 900000000000000000; + uint256 internal constant ONE_MINUS_SHORT = 100000000000000000; + uint256 internal constant LAMBDA_LONG = 980000000000000000; + uint256 internal constant ONE_MINUS_LONG = 20000000000000000; + + uint256 internal constant HALF_WAD = WAD / 2; + uint256 internal constant INIT_VAR = 2e12; + + function afterInitialize(uint256 initialX, uint256 initialY) + external override returns (uint256 bidFee, uint256 askFee) + {{ + slots[1] = wdiv(initialY, initialX); + slots[2] = INIT_VAR; + slots[3] = INIT_VAR; + slots[5] = 1; + return (clampFee(BASE_FEE), clampFee(BASE_FEE)); + }} + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + {{ + uint256 spotPrice = wdiv(trade.reserveY, trade.reserveX); + uint256 prevSpot = slots[1]; + uint256 shortVar = slots[2]; + uint256 longVar = slots[3]; + + // --- Layer 0: Update EWMA variance estimates --- + if (slots[5] == 1 && prevSpot > 0) {{ + uint256 delta = absDiff(spotPrice, prevSpot); + uint256 sqReturn = wdiv(wmul(delta, delta), wmul(prevSpot, prevSpot)); + shortVar = wmul(LAMBDA_SHORT, shortVar) + wmul(ONE_MINUS_SHORT, sqReturn); + longVar = wmul(LAMBDA_LONG, longVar) + wmul(ONE_MINUS_LONG, sqReturn); + slots[2] = shortVar; + slots[3] = longVar; + }} + + slots[0] = trade.timestamp; + slots[1] = spotPrice; + slots[5] = 1; + + // --- Layer 1: Vol-adjusted base fee --- + uint256 fee = BASE_FEE; + if (longVar > 0) {{ + uint256 ratio = wdiv(shortVar, longVar); + uint256 scaledHalf = wmul(HALF_WAD, ratio); + uint256 multiplier = HALF_WAD + scaledHalf; + if (multiplier > 2 * WAD) multiplier = 2 * WAD; + if (multiplier < HALF_WAD) multiplier = HALF_WAD; + fee = wmul(BASE_FEE, multiplier); + }} + + // --- Layer 2: Regime adjustment --- + if (REGIME_ADJUST > 0 && longVar > 0) {{ + if (shortVar > longVar) {{ + fee = fee + REGIME_ADJUST; + }} else if (fee > REGIME_ADJUST) {{ + fee = fee - REGIME_ADJUST; + }} + }} + + // --- Layer 3: Directional adjustment --- + uint256 bidF = fee; + uint256 askF = fee; + if (DIR_ADJUST > 0) {{ + if (trade.isBuy) {{ + bidF = fee + DIR_ADJUST; + askF = fee > DIR_ADJUST ? fee - DIR_ADJUST : 0; + }} else {{ + bidF = fee > DIR_ADJUST ? fee - DIR_ADJUST : 0; + askF = fee + DIR_ADJUST; + }} + }} + + slots[4] = trade.isBuy ? 1 : 0; + + return (clampFee(bidF), clampFee(askF)); + }} + + function getName() external pure override returns (string memory) {{ + return "Combined_{base_bps}_{regime_bps}_{dir_bps}"; + }} +}} +""" + + +# ── Candidate Definitions ──────────────────────────────────────────────────── + + +def _build_candidates() -> list[dict]: + """Build the full list of candidates across all families. + + Returns: + List of dicts with keys: name, template, params. + """ + candidates = [] + + # Family 1: Static Asymmetric + for name, bid, ask in [ + ("A1", 82, 78), + ("A2", 84, 76), + ("A3", 78, 82), + ("A4", 85, 75), + ]: + candidates.append( + { + "name": name, + "family": "StaticAsym", + "template": STATIC_ASYM_TEMPLATE, + "params": {"bid_bps": bid, "ask_bps": ask}, + } + ) + + # Family 2: Volatility-Responsive + # lambda_e18 values: 0.94*1e18, 0.90*1e18 + # vol_scale_e18: 1.0*1e18, 1.5*1e18 + # nominal_var: 2e12 (calibrated estimate) + for name, base, lam, scale in [ + ("V1", 80, "940000000000000000", "1000000000000000000"), + ("V2", 80, "900000000000000000", "1000000000000000000"), + ("V3", 80, "940000000000000000", "1500000000000000000"), + ("V4", 78, "940000000000000000", "1000000000000000000"), + ]: + candidates.append( + { + "name": name, + "family": "VolResponsive", + "template": VOL_RESPONSIVE_TEMPLATE, + "params": { + "base_bps": base, + "lambda_e18": lam, + "nominal_var": "2000000000000", + "vol_scale_e18": scale, + }, + } + ) + + # Family 3: Directional + for name, base, d in [ + ("D1", 80, 2), + ("D2", 80, 3), + ("D3", 80, 4), + ("D4", 80, 1), + ]: + candidates.append( + { + "name": name, + "family": "DirAdjust", + "template": DIR_ADJUST_TEMPLATE, + "params": {"base_bps": base, "dir_bps": d}, + } + ) + + # Family 4: Combined + for name, base, vol_s, regime, d in [ + ("C1", 80, "1000000000000000000", 3, 2), + ("C2", 80, "1000000000000000000", 0, 2), + ("C3", 80, "1500000000000000000", 3, 0), + ("C4", 78, "1000000000000000000", 3, 2), + ("C5", 80, "500000000000000000", 2, 1), + ("C6", 82, "1000000000000000000", 3, 2), + ]: + candidates.append( + { + "name": name, + "family": "Combined", + "template": COMBINED_TEMPLATE, + "params": { + "base_bps": base, + "vol_scale_e18": vol_s, + "regime_bps": regime, + "dir_bps": d, + }, + } + ) + + return candidates + + +# ── Core Evaluation ────────────────────────────────────────────────────────── + + +def _normal_cdf(x: float) -> float: + """Standard normal CDF via erfc (good approximation for t-dist when df > 30).""" + return 0.5 * math.erfc(-x / math.sqrt(2)) + + +def compile_sol(sol_path: Path) -> EVMStrategyAdapter: + """Compile a .sol file and return an adapter.""" + source = sol_path.read_text() + return EVMStrategyAdapter.from_source(source) + + +def compile_source(source: str) -> EVMStrategyAdapter: + """Compile Solidity source code and return an adapter.""" + return EVMStrategyAdapter.from_source(source) + + +def generate_strategy(template: str, params: dict) -> str: + """Generate Solidity source from template and parameters.""" + return template.format(**params) + + +def build_runner(n_sims: int) -> MatchRunner: + """Build a MatchRunner with baseline config and n_workers=1.""" + config = amm_sim_rs.SimulationConfig( + n_steps=BASELINE_SETTINGS.n_steps, + initial_price=BASELINE_SETTINGS.initial_price, + initial_x=BASELINE_SETTINGS.initial_x, + initial_y=BASELINE_SETTINGS.initial_y, + gbm_mu=BASELINE_SETTINGS.gbm_mu, + gbm_sigma=baseline_nominal_sigma(), + gbm_dt=BASELINE_SETTINGS.gbm_dt, + retail_arrival_rate=baseline_nominal_retail_rate(), + retail_mean_size=baseline_nominal_retail_size(), + retail_size_sigma=BASELINE_SETTINGS.retail_size_sigma, + retail_buy_prob=BASELINE_SETTINGS.retail_buy_prob, + seed=None, + ) + return MatchRunner( + n_simulations=n_sims, + config=config, + n_workers=1, + variance=BASELINE_VARIANCE, + ) + + +def extract_per_seed_edges(result) -> np.ndarray: + """Extract per-seed edge array from a MatchResult with stored results.""" + edges = [] + for sim in result.simulation_results: + edges.append(float(sim.edges["submission"])) + return np.array(edges) + + +def paired_compare( + cand_edges: np.ndarray, + base_edges: np.ndarray, +) -> dict: + """Compute paired-seed statistics between candidate and baseline edges. + + Args: + cand_edges: Per-seed edges for the candidate. + base_edges: Per-seed edges for the baseline. + + Returns: + Dict with mean_delta, se, t_stat, p_value, win_rate, etc. + """ + n = len(cand_edges) + deltas = cand_edges - base_edges + + mean_delta = float(np.mean(deltas)) + std_delta = float(np.std(deltas, ddof=1)) if n > 1 else 0.0 + se = std_delta / math.sqrt(n) if n > 0 else 0.0 + t_stat = mean_delta / se if se > 0 else 0.0 + # Normal approx for p-value (good for n > 30) + p_value = 2.0 * (1.0 - _normal_cdf(abs(t_stat))) if se > 0 else 1.0 + win_rate = float(np.mean(deltas > 0)) + + return { + "n": n, + "cand_mean": float(np.mean(cand_edges)), + "base_mean": float(np.mean(base_edges)), + "mean_delta": mean_delta, + "se": se, + "t_stat": t_stat, + "p_value": p_value, + "win_rate": win_rate, + } + + +def run_single_eval( + candidate_path: Path, + baseline_path: Path | None, + n_sims: int, +) -> dict: + """Run a single paired evaluation between two .sol files. + + Args: + candidate_path: Path to the candidate .sol file. + baseline_path: Path to the baseline .sol file (default: static 80 bps). + n_sims: Number of simulations to run. + + Returns: + Dict with paired comparison statistics. + """ + print(f"Compiling candidate: {candidate_path.name}") + candidate = compile_sol(candidate_path) + cand_name = candidate.get_name() + + if baseline_path: + print(f"Compiling baseline: {baseline_path.name}") + baseline = compile_sol(baseline_path) + else: + print("Using default baseline: Static 80 bps") + source = generate_strategy(STATIC_SYM_TEMPLATE, {"bps": 80}) + baseline = compile_source(source) + base_name = baseline.get_name() + + normalizer = load_vanilla_strategy() + runner = build_runner(n_sims) + + print(f"Running {n_sims} sims for candidate ({cand_name})...") + cand_result = runner.run_match(candidate, normalizer, store_results=True) + cand_edges = extract_per_seed_edges(cand_result) + + print(f"Running {n_sims} sims for baseline ({base_name})...") + base_result = runner.run_match(baseline, normalizer, store_results=True) + base_edges = extract_per_seed_edges(base_result) + + stats = paired_compare(cand_edges, base_edges) + stats["candidate"] = cand_name + stats["baseline"] = base_name + + return stats + + +def print_stats(stats: dict) -> None: + """Print paired evaluation statistics.""" + print(f"\n{'=' * 60}") + print(f" Candidate: {stats['candidate']}") + print(f" Baseline: {stats['baseline']}") + print(f" Sims: {stats['n']}") + print(f"{'=' * 60}") + print(f" Candidate mean edge: {stats['cand_mean']:.2f}") + print(f" Baseline mean edge: {stats['base_mean']:.2f}") + print(f" Mean delta: {stats['mean_delta']:+.2f}") + print(f" SE(delta): {stats['se']:.2f}") + print(f" t-stat: {stats['t_stat']:+.3f}") + print(f" p-value: {stats['p_value']:.4f}") + print(f" Win rate: {stats['win_rate']:.1%}") + if stats["p_value"] < 0.01: + sig = "***" + elif stats["p_value"] < 0.05: + sig = "**" + elif stats["p_value"] < 0.10: + sig = "*" + else: + sig = "" + if stats["mean_delta"] > 0: + print(f" => Candidate BETTER by {stats['mean_delta']:.2f} {sig}") + elif stats["mean_delta"] < 0: + print(f" => Candidate WORSE by {abs(stats['mean_delta']):.2f} {sig}") + else: + print(" => No difference") + print() + + +# ── Search Protocol ────────────────────────────────────────────────────────── + + +def run_search( + stage1_sims: int = 200, + stage2_sims: int = 500, + stage3_sims: int = 1000, +) -> None: + """Run the full staged search protocol. + + Stage 0: Lock baseline (static 80 bps) at stage1_sims. + Stage 1: Broad screen — all candidates at stage1_sims, promote top 8. + Stage 2: Narrow — top 8 at stage2_sims, promote top 3. + Stage 3: Final — top 3 at stage3_sims. + """ + normalizer = load_vanilla_strategy() + candidates = _build_candidates() + + # ── Stage 0: Lock baseline ─────────────────────────────────────────── + print("=" * 70) + print(f" STAGE 0: Lock baseline (Static 80 bps) at {stage1_sims} sims") + print("=" * 70) + + base_source = generate_strategy(STATIC_SYM_TEMPLATE, {"bps": 80}) + baseline = compile_source(base_source) + runner = build_runner(stage1_sims) + base_result = runner.run_match(baseline, normalizer, store_results=True) + base_edges = extract_per_seed_edges(base_result) + base_mean = float(np.mean(base_edges)) + print(f" Baseline mean edge: {base_mean:.2f} ({stage1_sims} sims)") + print() + + # ── Stage 1: Broad screen ──────────────────────────────────────────── + print("=" * 70) + print(f" STAGE 1: Broad screen — {len(candidates)} candidates at {stage1_sims} sims") + print("=" * 70) + + stage1_results = [] + for i, cand_def in enumerate(candidates): + name = cand_def["name"] + source = generate_strategy(cand_def["template"], cand_def["params"]) + try: + adapter = compile_source(source) + except Exception as e: + print(f" [{i + 1}/{len(candidates)}] {name}: COMPILE FAILED — {e}") + continue + + cand_name = adapter.get_name() + print(f" [{i + 1}/{len(candidates)}] {name} ({cand_name})...", end=" ", flush=True) + + cand_result = runner.run_match(adapter, normalizer, store_results=True) + cand_edges = extract_per_seed_edges(cand_result) + stats = paired_compare(cand_edges, base_edges) + stats["candidate"] = cand_name + stats["baseline"] = "Static_80" + stats["def"] = cand_def + + sig = "" + if stats["p_value"] < 0.01: + sig = "***" + elif stats["p_value"] < 0.05: + sig = "**" + elif stats["p_value"] < 0.10: + sig = "*" + + print( + f"delta={stats['mean_delta']:+.2f} " + f"SE={stats['se']:.2f} " + f"t={stats['t_stat']:+.2f} " + f"win={stats['win_rate']:.0%} {sig}" + ) + stage1_results.append(stats) + + # Sort by mean delta descending + stage1_results.sort(key=lambda s: s["mean_delta"], reverse=True) + + # Eliminate anything with mean delta < -5 + filtered = [s for s in stage1_results if s["mean_delta"] >= -5] + promoted = filtered[:8] + + print( + f"\n Stage 1 summary: {len(stage1_results)} evaluated, {len(promoted)} promoted to Stage 2" + ) + print(" Promoted:") + for s in promoted: + print(f" {s['candidate']}: delta={s['mean_delta']:+.2f} (t={s['t_stat']:+.2f})") + print() + + if not promoted: + print(" No candidates survived Stage 1. Stopping.") + _print_final_summary([], base_mean) + return + + # ── Stage 2: Narrow evaluation ─────────────────────────────────────── + print("=" * 70) + print(f" STAGE 2: Narrow — {len(promoted)} candidates at {stage2_sims} sims") + print("=" * 70) + + runner2 = build_runner(stage2_sims) + # Rerun baseline at stage2_sims + base_result2 = runner2.run_match(baseline, normalizer, store_results=True) + base_edges2 = extract_per_seed_edges(base_result2) + + stage2_results = [] + for i, s1 in enumerate(promoted): + cand_def = s1["def"] + source = generate_strategy(cand_def["template"], cand_def["params"]) + adapter = compile_source(source) + cand_name = adapter.get_name() + print(f" [{i + 1}/{len(promoted)}] {cand_name}...", end=" ", flush=True) + + cand_result = runner2.run_match(adapter, normalizer, store_results=True) + cand_edges = extract_per_seed_edges(cand_result) + stats = paired_compare(cand_edges, base_edges2) + stats["candidate"] = cand_name + stats["baseline"] = "Static_80" + stats["def"] = cand_def + + sig = "" + if stats["p_value"] < 0.01: + sig = "***" + elif stats["p_value"] < 0.05: + sig = "**" + elif stats["p_value"] < 0.10: + sig = "*" + + print( + f"delta={stats['mean_delta']:+.2f} " + f"SE={stats['se']:.2f} " + f"t={stats['t_stat']:+.2f} " + f"win={stats['win_rate']:.0%} {sig}" + ) + stage2_results.append(stats) + + stage2_results.sort(key=lambda s: s["mean_delta"], reverse=True) + + # Check stop condition + any_positive = any(s["mean_delta"] > 0 for s in stage2_results) + if not any_positive: + print("\n No candidate shows positive mean delta after Stage 2.") + print(" Conclusion: practical ceiling is near static 80 bps.") + _print_final_summary(stage2_results, base_mean) + return + + promoted2 = [s for s in stage2_results if s["mean_delta"] > 0][:3] + print( + f"\n Stage 2 summary: {len(stage2_results)} evaluated, " + f"{len(promoted2)} promoted to Stage 3" + ) + for s in promoted2: + print(f" {s['candidate']}: delta={s['mean_delta']:+.2f} (t={s['t_stat']:+.2f})") + print() + + # ── Stage 3: Final validation ──────────────────────────────────────── + print("=" * 70) + print(f" STAGE 3: Final validation — {len(promoted2)} candidates at {stage3_sims} sims") + print("=" * 70) + + runner3 = build_runner(stage3_sims) + base_result3 = runner3.run_match(baseline, normalizer, store_results=True) + base_edges3 = extract_per_seed_edges(base_result3) + + stage3_results = [] + for i, s2 in enumerate(promoted2): + cand_def = s2["def"] + source = generate_strategy(cand_def["template"], cand_def["params"]) + adapter = compile_source(source) + cand_name = adapter.get_name() + print(f" [{i + 1}/{len(promoted2)}] {cand_name}...", end=" ", flush=True) + + cand_result = runner3.run_match(adapter, normalizer, store_results=True) + cand_edges = extract_per_seed_edges(cand_result) + stats = paired_compare(cand_edges, base_edges3) + stats["candidate"] = cand_name + stats["baseline"] = "Static_80" + stats["def"] = cand_def + + sig = "" + if stats["p_value"] < 0.01: + sig = "***" + elif stats["p_value"] < 0.05: + sig = "**" + elif stats["p_value"] < 0.10: + sig = "*" + + print( + f"delta={stats['mean_delta']:+.2f} " + f"SE={stats['se']:.2f} " + f"t={stats['t_stat']:+.2f} " + f"win={stats['win_rate']:.0%} {sig}" + ) + stage3_results.append(stats) + + stage3_results.sort(key=lambda s: s["mean_delta"], reverse=True) + _print_final_summary(stage3_results, float(np.mean(base_edges3))) + + +def _print_final_summary(results: list[dict], base_mean: float) -> None: + """Print the final summary of the search protocol.""" + print("\n" + "=" * 70) + print(" FINAL SUMMARY") + print("=" * 70) + print(f" Baseline (Static 80 bps) mean edge: {base_mean:.2f}") + print() + + if not results: + print(" No candidates to report.") + print(" Recommendation: submit static 80 bps.") + return + + best = results[0] + print(" Ranking:") + for i, s in enumerate(results): + marker = " <-- BEST" if i == 0 else "" + print( + f" {i + 1}. {s['candidate']}: " + f"edge={s['cand_mean']:.2f} " + f"delta={s['mean_delta']:+.2f} " + f"t={s['t_stat']:+.2f} " + f"p={s['p_value']:.4f}{marker}" + ) + + print() + if best["mean_delta"] > 3 and abs(best["t_stat"]) > 1.5: + cname = best["candidate"] + d, t = best["mean_delta"], best["t_stat"] + print(f" Recommendation: use {cname} (delta={d:+.2f}, t={t:+.2f})") + elif best["mean_delta"] > 0: + print(f" Marginal improvement: {best['candidate']} (delta={best['mean_delta']:+.2f})") + print(" Consider submitting this, but static 80 is also a strong choice.") + else: + print(" No candidate beats static 80 bps.") + print(" Recommendation: submit static 80 bps.") + + +# ── CLI ────────────────────────────────────────────────────────────────────── + + +def main() -> int: + """Main entry point.""" + parser = argparse.ArgumentParser( + description="Paired-seed evaluation for AMM strategies", + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "candidate", + nargs="?", + help="Path to candidate .sol file (for single-pair mode)", + ) + parser.add_argument( + "--baseline", + type=str, + default=None, + help="Path to baseline .sol file (default: static 80 bps)", + ) + parser.add_argument( + "--sims", + type=int, + default=200, + help="Number of simulations (for single-pair mode)", + ) + parser.add_argument( + "--search", + action="store_true", + help="Run the full staged search protocol", + ) + parser.add_argument("--stage1-sims", type=int, default=200) + parser.add_argument("--stage2-sims", type=int, default=500) + parser.add_argument("--stage3-sims", type=int, default=1000) + + args = parser.parse_args() + + if args.search: + run_search( + stage1_sims=args.stage1_sims, + stage2_sims=args.stage2_sims, + stage3_sims=args.stage3_sims, + ) + return 0 + + if not args.candidate: + parser.error("Provide a candidate .sol file or use --search for batch mode") + return 1 + + candidate_path = Path(args.candidate) + if not candidate_path.exists(): + print(f"Error: {candidate_path} not found") + return 1 + + baseline_path = Path(args.baseline) if args.baseline else None + if baseline_path and not baseline_path.exists(): + print(f"Error: {baseline_path} not found") + return 1 + + stats = run_single_eval(candidate_path, baseline_path, args.sims) + print_stats(stats) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/sweep_asymmetric.py b/scripts/sweep_asymmetric.py new file mode 100644 index 00000000..f86439c0 --- /dev/null +++ b/scripts/sweep_asymmetric.py @@ -0,0 +1,137 @@ +"""Sweep asymmetric bid/ask fee combinations. + +Tests multiple bid/ask fee pairs to find the optimal asymmetric strategy. +Runs a quick screen (10 sims) for each combination, then reruns top +candidates at full resolution. +""" + +import os +import re +import subprocess +import tempfile +from pathlib import Path + +TEMPLATE = """\ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {{AMMStrategyBase}} from "./AMMStrategyBase.sol"; +import {{TradeInfo}} from "./IAMMStrategy.sol"; + +contract Strategy is AMMStrategyBase {{ + uint256 internal constant BID_FEE = {bid} * BPS; + uint256 internal constant ASK_FEE = {ask} * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + {{ + return (BID_FEE, ASK_FEE); + }} + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + {{ + return (BID_FEE, ASK_FEE); + }} + + function getName() external pure override returns (string memory) {{ + return "Asym_{bid}_{ask}"; + }} +}} +""" + +STRATEGIES_DIR = Path(__file__).resolve().parent.parent / "strategies" + +# Asymmetric pairs to test: (bid_bps, ask_bps) +PAIRS = [ + # Average ~80 bps + (70, 90), + (65, 95), + (55, 105), + (50, 110), + (45, 115), + # Average ~75 bps + (50, 100), + (40, 110), + # Average ~85 bps + (60, 110), + (70, 100), + # Average ~70 bps + (40, 100), + (30, 110), + # Average ~90 bps + (60, 120), + (70, 110), + # Very aggressive + (30, 130), + (20, 140), + # Mild asymmetry + (75, 85), +] + + +def run_strategy(bid: int, ask: int, sims: int = 10) -> float | None: + """Generate and run an asymmetric fee strategy. + + Args: + bid: Bid fee in basis points. + ask: Ask fee in basis points. + sims: Number of simulations to run. + + Returns: + Average edge score, or None if the run failed. + """ + code = TEMPLATE.format(bid=bid, ask=ask) + + with tempfile.NamedTemporaryFile( + mode="w", suffix=".sol", delete=False, dir=STRATEGIES_DIR + ) as f: + f.write(code) + path = f.name + + try: + result = subprocess.run( + ["uv", "run", "amm-match", "run", path, "--simulations", str(sims)], + capture_output=True, + text=True, + timeout=300, + ) + output = result.stdout + result.stderr + match = re.search(r"Edge:\s*([-\d.]+)", output) + if match: + return float(match.group(1)) + print(f" Could not parse output for {bid}/{ask}:\n{output}") + return None + except subprocess.TimeoutExpired: + print(f" Timeout for {bid}/{ask}") + return None + finally: + os.unlink(path) + + +def main() -> None: + """Run the asymmetric fee sweep and print results.""" + sims = 10 + print(f"Running asymmetric fee sweep ({sims} simulations each)\n") + print(f"{'Bid':>6} {'Ask':>6} {'Avg':>6} {'Edge':>10}") + print("-" * 32) + + results: dict[tuple[int, int], float] = {} + for bid, ask in PAIRS: + avg = (bid + ask) / 2 + print(f"{bid:>6} {ask:>6} {avg:>6.0f}", end="", flush=True) + edge = run_strategy(bid, ask, sims) + if edge is not None: + print(f"{edge:>10.2f}") + results[(bid, ask)] = edge + else: + print(f"{'FAILED':>10}") + + print("\n--- Top 5 ---") + sorted_results = sorted(results.items(), key=lambda x: x[1], reverse=True) + for (bid, ask), edge in sorted_results[:5]: + print(f" bid={bid}, ask={ask} → edge={edge:.2f}") + + +if __name__ == "__main__": + main() diff --git a/strategies/README.md b/strategies/README.md index fe9fa8e1..a9b37a0e 100644 --- a/strategies/README.md +++ b/strategies/README.md @@ -21,4 +21,62 @@ Best static fee: **80 bps** (edge 380.06). The normalizer (30 bps) scores 343.60 | # | Strategy | Description | Edge (1000 sims) | Takeaway | |---|----------|-------------|-------------------|----------| -| 01 | Dynamic v1 | Reactive: raise after big trades, decay | — | — | +| 01 | ImpactReactive_v1 | Bump to 150 bps on large trades, decay 3 bps/trade back to 80 bps base | 379.78 | Matches static 80 bps but doesn't beat it — arbs are too rare at this fee level for impact detection to help | + +## Phase 3: Theory-Constrained Search (Paired-Seed Evaluation) + +All candidates evaluated against static 80 bps using paired-seed comparison +(same seeds for both strategies, compute per-seed delta). This reduces +variance and enables detection of smaller improvements. + +### Stage 1: Broad Screen (200 sims, paired vs static 80 bps) + +| Name | Family | Config | Delta | SE | t-stat | Win% | +|------|--------|--------|-------|-----|--------|------| +| A1 | StaticAsym | bid=82, ask=78 | +0.00 | 0.00 | +1.57 | 53% | +| A2 | StaticAsym | bid=84, ask=76 | +0.00 | 0.00 | +1.58 | 54% | +| A3 | StaticAsym | bid=78, ask=82 | -0.00 | 0.00 | -1.70 | 47% | +| A4 | StaticAsym | bid=85, ask=75 | +0.00 | 0.00 | +1.55 | 52% | +| V1 | VolResponsive | base=80, lam=0.94 | -45.40 | 1.03 | -44.28 | 0% | +| V2 | VolResponsive | base=80, lam=0.90 | -45.20 | 1.02 | -44.13 | 0% | +| V3 | VolResponsive | base=80, lam=0.94, scale=1.5 | -45.53 | 1.02 | -44.48 | 0% | +| V4 | VolResponsive | base=78, lam=0.94 | -42.26 | 0.98 | -43.21 | 0% | +| D1 | DirAdjust | base=80, dir=2 | -2.46 | 0.04 | -69.50 | 0% | +| D2 | DirAdjust | base=80, dir=3 | -3.67 | 0.05 | -70.24 | 0% | +| D3 | DirAdjust | base=80, dir=4 | -4.85 | 0.07 | -69.86 | 0% | +| D4 | DirAdjust | base=80, dir=1 | -1.23 | 0.02 | -69.29 | 0% | +| **C1** | **Combined** | **base=80, regime=3, dir=2** | **+25.76** | **0.46** | **+55.75** | **100%** | +| **C2** | **Combined** | **base=80, regime=0, dir=2** | **+25.15** | **0.44** | **+57.26** | **100%** | +| **C3** | **Combined** | **base=80, regime=3, dir=0** | **+28.06** | **0.48** | **+58.61** | **100%** | +| **C4** | **Combined** | **base=78, regime=3, dir=2** | **+26.00** | **0.43** | **+60.03** | **100%** | +| **C5** | **Combined** | **base=80, regime=2, dir=1** | **+26.84** | **0.47** | **+57.35** | **100%** | +| **C6** | **Combined** | **base=82, regime=3, dir=2** | **+25.44** | **0.49** | **+51.54** | **100%** | + +Key findings: +- **Static Asymmetric**: zero impact — asymmetry around 80 bps doesn't help +- **Vol Responsive (absolute EWMA)**: badly miscalibrated, delta -45 — NOMINAL_VAR too low +- **Directional**: harmful, delta -1 to -5 — gives arbs cheaper back-leg trades +- **Combined (dual-EWMA ratio)**: massive improvement, delta +25 to +28 — self-calibrating vol ratio works + +### Stage 2: Narrow (500 sims, top 8) + +| Name | Delta | SE | t-stat | Win% | +|------|-------|-----|--------|------| +| Combined_80_3_0 | +28.25 | 0.30 | +93.98 | 100% | +| Combined_80_2_1 | +26.99 | 0.29 | +92.82 | 100% | +| Combined_78_3_2 | +26.24 | 0.27 | +96.76 | 100% | +| Combined_80_3_2 | +25.96 | 0.29 | +90.18 | 100% | +| Combined_82_3_2 | +25.60 | 0.31 | +83.47 | 100% | +| Combined_80_0_2 | +25.31 | 0.27 | +92.62 | 100% | +| StaticAsym_85_75 | -0.00 | 0.00 | -0.11 | 49% | +| StaticAsym_84_76 | -0.00 | 0.00 | -0.10 | 50% | + +### Stage 3: Final Validation (1000 sims, top 3) + +| Rank | Name | Edge | Delta | SE | t-stat | Win% | +|------|------|------|-------|-----|--------|------| +| **1** | **Combined_80_3_0** | **407.97** | **+27.92** | **0.22** | **+127.91** | **100%** | +| 2 | Combined_80_2_1 | 406.70 | +26.64 | 0.21 | +126.67 | 100% | +| 3 | Combined_78_3_2 | 405.94 | +25.88 | 0.20 | +131.35 | 100% | + +**Winner: Combined_80_3_0** (DualEWMA_VolRegime_80_3) — edge 407.97, delta +27.92 vs static 80 bps. diff --git a/strategies/_best_asym.sol b/strategies/_best_asym.sol new file mode 100644 index 00000000..4a7172aa --- /dev/null +++ b/strategies/_best_asym.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant BID_FEE = 120 * BPS; + uint256 internal constant ASK_FEE = 40 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (BID_FEE, ASK_FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (BID_FEE, ASK_FEE); } + function getName() external pure override returns (string memory) { return "Asym_120_40"; } +} diff --git a/strategies/_fine_77.sol b/strategies/_fine_77.sol new file mode 100644 index 00000000..4781f0e3 --- /dev/null +++ b/strategies/_fine_77.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 77 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static_77"; } +} diff --git a/strategies/_fine_78.sol b/strategies/_fine_78.sol new file mode 100644 index 00000000..e141663e --- /dev/null +++ b/strategies/_fine_78.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 78 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static_78"; } +} diff --git a/strategies/_fine_79.sol b/strategies/_fine_79.sol new file mode 100644 index 00000000..f3a46a8e --- /dev/null +++ b/strategies/_fine_79.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 79 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static_79"; } +} diff --git a/strategies/_fine_81.sol b/strategies/_fine_81.sol new file mode 100644 index 00000000..94cc9bb4 --- /dev/null +++ b/strategies/_fine_81.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 81 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static_81"; } +} diff --git a/strategies/_fine_82.sol b/strategies/_fine_82.sol new file mode 100644 index 00000000..90bfac43 --- /dev/null +++ b/strategies/_fine_82.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 82 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static_82"; } +} diff --git a/strategies/_fine_83.sol b/strategies/_fine_83.sol new file mode 100644 index 00000000..e9762d62 --- /dev/null +++ b/strategies/_fine_83.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 83 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static_83"; } +} diff --git a/strategies/_sweep_75.sol b/strategies/_sweep_75.sol new file mode 100644 index 00000000..5c7a19cd --- /dev/null +++ b/strategies/_sweep_75.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 75 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static"; } +} diff --git a/strategies/_sweep_78.sol b/strategies/_sweep_78.sol new file mode 100644 index 00000000..472f2498 --- /dev/null +++ b/strategies/_sweep_78.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 78 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static"; } +} diff --git a/strategies/_sweep_80.sol b/strategies/_sweep_80.sol new file mode 100644 index 00000000..2504eff1 --- /dev/null +++ b/strategies/_sweep_80.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 80 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static_80"; } +} diff --git a/strategies/_sweep_82.sol b/strategies/_sweep_82.sol new file mode 100644 index 00000000..bc5e054b --- /dev/null +++ b/strategies/_sweep_82.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 82 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static"; } +} diff --git a/strategies/_sweep_85.sol b/strategies/_sweep_85.sol new file mode 100644 index 00000000..d5eb578f --- /dev/null +++ b/strategies/_sweep_85.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 85 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static"; } +} diff --git a/strategies/_sweep_88.sol b/strategies/_sweep_88.sol new file mode 100644 index 00000000..c7de3137 --- /dev/null +++ b/strategies/_sweep_88.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 88 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static"; } +} diff --git a/strategies/_sweep_90.sol b/strategies/_sweep_90.sol new file mode 100644 index 00000000..99c5b92a --- /dev/null +++ b/strategies/_sweep_90.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant FEE = 90 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (FEE, FEE); } + function getName() external pure override returns (string memory) { return "Static"; } +} diff --git a/strategies/_test_asym.sol b/strategies/_test_asym.sol new file mode 100644 index 00000000..960315fc --- /dev/null +++ b/strategies/_test_asym.sol @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; +contract Strategy is AMMStrategyBase { + uint256 internal constant BID_FEE = 115 * BPS; + uint256 internal constant ASK_FEE = 65 * BPS; + function afterInitialize(uint256, uint256) external override returns (uint256 bidFee, uint256 askFee) { return (BID_FEE, ASK_FEE); } + function afterSwap(TradeInfo calldata) external override returns (uint256 bidFee, uint256 askFee) { return (BID_FEE, ASK_FEE); } + function getName() external pure override returns (string memory) { return "Asym"; } +} diff --git a/strategies/asymmetric_v3a.sol b/strategies/asymmetric_v3a.sol new file mode 100644 index 00000000..727a6f3f --- /dev/null +++ b/strategies/asymmetric_v3a.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Asymmetric v3a — Static Asymmetric Bid/Ask Fees +/// @notice Exploits the fact that routing uses direction-specific fees. +/// @dev By setting a low bid fee and high ask fee (or vice versa), we create +/// a lower routing threshold for one direction, capturing more volume +/// on that side at lower margin, while extracting higher margin on the +/// other side. Since buy_prob = 0.5, this creates an asymmetric volume +/// profile. +/// +/// bid = 60 bps → captures ~36% of sell-X orders (threshold ~15 Y) +/// ask = 100 bps → captures ~14% of buy-X orders (threshold ~35 Y) +/// Hypothesis: total revenue may exceed symmetric 80 bps. +/// +/// Storage layout: +/// (no dynamic state — purely static) +contract Strategy is AMMStrategyBase { + uint256 internal constant BID_FEE = 60 * BPS; + uint256 internal constant ASK_FEE = 100 * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + return (BID_FEE, ASK_FEE); + } + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + { + return (BID_FEE, ASK_FEE); + } + + function getName() external pure override returns (string memory) { + return "Asymmetric_v3a_60_100"; + } +} diff --git a/strategies/asymmetric_v3b.sol b/strategies/asymmetric_v3b.sol new file mode 100644 index 00000000..c3973d3c --- /dev/null +++ b/strategies/asymmetric_v3b.sol @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Asymmetric v3b — Aggressive Asymmetric Bid/Ask Fees +/// @notice More aggressive asymmetry: very low bid, very high ask. +/// @dev bid = 40 bps → captures ~71% of sell-X orders (threshold ~5 Y) +/// ask = 120 bps → captures ~8% of buy-X orders (threshold ~45 Y) +/// Tests whether extreme asymmetry captures enough volume to offset +/// the margin imbalance. +/// +/// Storage layout: +/// (no dynamic state — purely static) +contract Strategy is AMMStrategyBase { + uint256 internal constant BID_FEE = 40 * BPS; + uint256 internal constant ASK_FEE = 120 * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + return (BID_FEE, ASK_FEE); + } + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + { + return (BID_FEE, ASK_FEE); + } + + function getName() external pure override returns (string memory) { + return "Asymmetric_v3b_40_120"; + } +} diff --git a/strategies/combined_v4c.sol b/strategies/combined_v4c.sol new file mode 100644 index 00000000..c15eeb32 --- /dev/null +++ b/strategies/combined_v4c.sol @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Combined v4c — Timestamp + Routing-Asymmetry + Directional +/// @notice Combines three insights for maximum edge: +/// +/// 1. ROUTING ASYMMETRY: sell-X orders always route to us (X reserves small), +/// so bidFee can be high. Buy-X orders face a Y-threshold, so askFee +/// should be lower to capture more volume. +/// +/// 2. TIMESTAMP: After the arb trade (new step), the next trades are retail. +/// We can set a slightly more aggressive ask fee in the retail window +/// to attract buy-X flow, then restore after retail trades. +/// +/// 3. DIRECTIONAL: After a buy trade (spot dropped), raise bidFee slightly +/// (penalize continuation) and lower askFee (reward rebalancing). +/// Vice versa for sell trades. +/// +/// Strategy: +/// New step (after arb): +/// bidFee = 88 bps (high — sell-X always routes) +/// askFee = 68 bps (low — maximize buy-X routing in retail window) +/// Same step (after retail): +/// bidFee = 90 bps + directional adjustment +/// askFee = 78 bps + directional adjustment +/// +/// Storage layout: +/// slots[0] = lastTimestamp +/// slots[1] = lastTradeIsBuy (0 = sell, 1 = buy) +contract Strategy is AMMStrategyBase { + // ── Fee Constants ──────────────────────────────────────────────────── + + /// @notice Retail-window fees (after arb, before retail trades) + uint256 internal constant RETAIL_BID = 88 * BPS; + uint256 internal constant RETAIL_ASK = 68 * BPS; + + /// @notice Default fees (after retail, before next arb) + uint256 internal constant DEFAULT_BID = 90 * BPS; + uint256 internal constant DEFAULT_ASK = 78 * BPS; + + /// @notice Directional adjustment magnitude + uint256 internal constant DIR_ADJUST = 3 * BPS; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + // Start with default (high bid, moderate ask) for first arb. + return (DEFAULT_BID, DEFAULT_ASK); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 lastTimestamp = slots[0]; + + if (trade.timestamp > lastTimestamp) { + // New step — first trade (likely arb). Set retail-window fees. + slots[0] = trade.timestamp; + slots[1] = trade.isBuy ? 1 : 0; + return (clampFee(RETAIL_BID), clampFee(RETAIL_ASK)); + } + + // Same step — retail trade just executed. + // Apply directional adjustment based on the trade direction. + uint256 newBidFee = DEFAULT_BID; + uint256 newAskFee = DEFAULT_ASK; + + if (trade.isBuy) { + // AMM bought X → spot dropped. Penalize more buying, attract selling. + newBidFee = DEFAULT_BID + DIR_ADJUST; + newAskFee = DEFAULT_ASK - DIR_ADJUST; + } else { + // AMM sold X → spot rose. Attract buying, penalize more selling. + newBidFee = DEFAULT_BID - DIR_ADJUST; + newAskFee = DEFAULT_ASK + DIR_ADJUST; + } + + slots[1] = trade.isBuy ? 1 : 0; + + return (clampFee(newBidFee), clampFee(newAskFee)); + } + + function getName() external pure override returns (string memory) { + return "Combined_v4c"; + } +} diff --git a/strategies/dir_adjust_v5.sol b/strategies/dir_adjust_v5.sol new file mode 100644 index 00000000..ca747a1f --- /dev/null +++ b/strategies/dir_adjust_v5.sol @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Directional Adjustment Fee Strategy (D1 variant) +/// @notice Adjusts bid/ask fees based on the current trade direction. +/// After a buy (AMM bought X), charges more on next buy and less on sell. +/// After a sell, vice versa. This penalizes repeated same-direction trades +/// (which are more likely informed/arb) and encourages mean-reversion flow. +/// +/// Rationale: +/// - Arb trades tend to be unidirectional (correcting spot to fair price). +/// - Retail is random 50/50. By making the next same-direction trade more +/// expensive, we selectively tax informed flow while being neutral to retail. +/// +/// Storage layout: +/// slots[0] = lastTradeIsBuy (0 = sell, 1 = buy) +/// slots[1] = initialized (0 or 1) +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice Base fee: 80 bps (optimal static fee) + uint256 internal constant BASE_FEE = 80 * BPS; + + /// @notice Directional adjustment: 2 bps + uint256 internal constant DIR_ADJUST = 2 * BPS; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + return (clampFee(BASE_FEE), clampFee(BASE_FEE)); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 bidF; + uint256 askF; + + if (slots[1] == 1) { + // Adjust fees based on current trade direction: + // If current trade is a buy, charge more for buys (same direction) + // and less for sells (opposite direction) + if (trade.isBuy) { + bidF = BASE_FEE + DIR_ADJUST; + askF = BASE_FEE - DIR_ADJUST; + } else { + bidF = BASE_FEE - DIR_ADJUST; + askF = BASE_FEE + DIR_ADJUST; + } + } else { + bidF = BASE_FEE; + askF = BASE_FEE; + } + + slots[0] = trade.isBuy ? 1 : 0; + slots[1] = 1; + + return (clampFee(bidF), clampFee(askF)); + } + + function getName() external pure override returns (string memory) { + return "DirAdjust_80_2"; + } +} diff --git a/strategies/directional_v2b.sol b/strategies/directional_v2b.sol new file mode 100644 index 00000000..25ec60b5 --- /dev/null +++ b/strategies/directional_v2b.sol @@ -0,0 +1,98 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Directional v2b — Asymmetric Fee Strategy (Nezlobin-Inspired) +/// @notice Sets asymmetric bid/ask fees based on last trade direction. +/// @dev After a trade pushes price in direction D: +/// - Same-direction trades (continuation) pay higher fees — likely adverse. +/// - Opposite-direction trades (rebalancing) pay lower fees — benign flow. +/// +/// Rationale: +/// - Nezlobin et al. showed directional fees reduce LVR by 10-13%. +/// - After a buy (AMM bought X, spot price dropped), further buying is +/// potentially adverse. Selling is rebalancing — attract it. +/// - The adjustment is proportional to trade impact (amountY/reserveY), +/// so large trades cause bigger adjustments. +/// - Base fee of 80 bps matches the optimal static fee. Directional +/// adjustments create asymmetric spread around it. +/// - Smaller adjustment scale (0.3x) keeps the fee swing moderate — +/// too large an adjustment undercuts our margin on one side. +/// +/// Storage layout: +/// slots[0] = lastBidFee (WAD) +/// slots[1] = lastAskFee (WAD) +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice Base fee around which directional adjustments are made + uint256 internal constant BASE_FEE = 80 * BPS; + + /// @notice Scaling factor for directional adjustment (0.3x impact in WAD) + uint256 internal constant ADJUSTMENT_SCALE = 3 * WAD / 10; + + /// @notice Minimum fee floor to avoid zero or negative fees + uint256 internal constant FEE_FLOOR = 40 * BPS; + + /// @notice Maximum fee ceiling + uint256 internal constant FEE_CEILING = 150 * BPS; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + slots[0] = BASE_FEE; + slots[1] = BASE_FEE; + return (BASE_FEE, BASE_FEE); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + // Compute trade impact: amountY relative to post-trade reserveY. + uint256 impact = wdiv(trade.amountY, trade.reserveY); + + // Scale adjustment: half of impact (in WAD units, so result is WAD fraction). + uint256 adjustment = wmul(ADJUSTMENT_SCALE, impact); + + // Convert adjustment from WAD fraction to fee units. + // impact is a WAD fraction (e.g., 0.005 = 5e15 for 0.5% trade). + // adjustment is half that. We use it directly as a fee delta. + // For a 0.5% trade: adjustment = 2.5e15 = 25 bps. Reasonable. + + uint256 newBidFee; + uint256 newAskFee; + + if (trade.isBuy) { + // AMM bought X (trader sold X) → spot price dropped. + // Penalize further buying (same direction) with higher bid fee. + // Attract selling (rebalancing) with lower ask fee. + newBidFee = BASE_FEE + adjustment; + newAskFee = adjustment < BASE_FEE ? BASE_FEE - adjustment : FEE_FLOOR; + } else { + // AMM sold X (trader bought X) → spot price went up. + // Attract buying (rebalancing) with lower bid fee. + // Penalize further selling (same direction) with higher ask fee. + newBidFee = adjustment < BASE_FEE ? BASE_FEE - adjustment : FEE_FLOOR; + newAskFee = BASE_FEE + adjustment; + } + + // Apply floor and ceiling. + if (newBidFee < FEE_FLOOR) newBidFee = FEE_FLOOR; + if (newAskFee < FEE_FLOOR) newAskFee = FEE_FLOOR; + if (newBidFee > FEE_CEILING) newBidFee = FEE_CEILING; + if (newAskFee > FEE_CEILING) newAskFee = FEE_CEILING; + + slots[0] = newBidFee; + slots[1] = newAskFee; + + return (clampFee(newBidFee), clampFee(newAskFee)); + } + + function getName() external pure override returns (string memory) { + return "Directional_v2b"; + } +} diff --git a/strategies/escalating_v3c.sol b/strategies/escalating_v3c.sol new file mode 100644 index 00000000..9e5f64ec --- /dev/null +++ b/strategies/escalating_v3c.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Escalating v3c — Within-Step Fee Escalation +/// @notice Starts with a competitive fee after arb trade, escalates with +/// each subsequent trade within the same step. +/// @dev Exploits sequential retail processing: each trade's afterSwap fires +/// before the next trade is routed. By starting low and escalating, +/// we capture the first (most likely) retail order at competitive rates, +/// then charge more for any additional orders in the same step. +/// +/// With Poisson(0.8), ~36% of steps have exactly 1 retail order, +/// ~14% have 2, and ~5% have 3+. +/// +/// Storage layout: +/// slots[0] = lastTimestamp +/// slots[1] = tradeCountInStep +contract Strategy is AMMStrategyBase { + /// @notice Fee for first retail order after arb (competitive) + uint256 internal constant FIRST_FEE = 50 * BPS; + + /// @notice Fee increment per additional trade within the step + uint256 internal constant FEE_STEP = 30 * BPS; + + /// @notice Maximum fee ceiling + uint256 internal constant MAX_STEP_FEE = 150 * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 fee = 80 * BPS; + return (fee, fee); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 lastTimestamp = slots[0]; + uint256 tradeCount = slots[1]; + + if (trade.timestamp > lastTimestamp) { + // New step — first trade (arb or first retail if no arb). + // Set competitive fee for the retail window. + slots[0] = trade.timestamp; + slots[1] = 1; + uint256 fee = clampFee(FIRST_FEE); + return (fee, fee); + } + + // Same step — escalate fee for subsequent trades. + tradeCount = tradeCount + 1; + slots[1] = tradeCount; + + // Fee escalates: FIRST_FEE + (tradeCount - 1) * FEE_STEP + uint256 fee = FIRST_FEE + (tradeCount - 1) * FEE_STEP; + if (fee > MAX_STEP_FEE) fee = MAX_STEP_FEE; + fee = clampFee(fee); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "Escalating_v3c"; + } +} diff --git a/strategies/ewma_vol_v2c.sol b/strategies/ewma_vol_v2c.sol new file mode 100644 index 00000000..8315b70f --- /dev/null +++ b/strategies/ewma_vol_v2c.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title EWMAVol v2c — Volatility-Responsive Fee Strategy +/// @notice Adapts fees based on EWMA-estimated realized volatility per step. +/// @dev Only updates the volatility estimate on step boundaries (timestamp +/// changes) to measure the exogenous price process, not within-step +/// trade-to-trade noise. Uses the spot price after the first trade of +/// each step as the reference price. +/// +/// Rationale: +/// - Optimal AMM fee scales with σ (volatility per step). +/// - Config randomizes σ ∈ [0.000882, 0.001008]. Adapting to realized σ +/// lets us optimize fees for the actual volatility regime. +/// - EWMA with λ = 0.94 gives ~17-step half-life, responsive but smooth. +/// - Fee = BASE + SCALE * sqrt(ewmaVariance), calibrated so nominal +/// σ ≈ 9.4 bps maps to ~80 bps fee. +/// +/// Storage layout: +/// slots[0] = lastTimestamp +/// slots[1] = prevStepSpotPrice (WAD) — spot after first trade of previous step +/// slots[2] = ewmaVariance (WAD-scale) +/// slots[3] = currentFee (WAD) +/// slots[4] = initialized (0 = not yet, 1 = has one step reference) +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice EWMA decay factor λ = 0.94 (in WAD) + uint256 internal constant LAMBDA = 94 * WAD / 100; + + /// @notice 1 - λ = 0.06 (in WAD) + uint256 internal constant ONE_MINUS_LAMBDA = 6 * WAD / 100; + + /// @notice Base fee when volatility is zero + uint256 internal constant BASE_FEE = 40 * BPS; + + /// @notice Scaling factor: maps estimated σ to fee delta. + /// @dev At nominal σ ≈ 9.4 bps (9.4e14 WAD), target fee = 80 bps. + /// 80e14 = 40e14 + SCALE * 9.4e14 → SCALE ≈ 4.25 + uint256 internal constant VOL_SCALE = 425 * WAD / 100; + + /// @notice Default fee before we have volatility data + uint256 internal constant DEFAULT_FEE = 80 * BPS; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + slots[3] = DEFAULT_FEE; + return (DEFAULT_FEE, DEFAULT_FEE); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 lastTimestamp = slots[0]; + uint256 fee = slots[3]; + + if (trade.timestamp > lastTimestamp) { + // New step boundary — first trade of a new simulation step. + uint256 spotPrice = wdiv(trade.reserveY, trade.reserveX); + + if (slots[4] == 0) { + // Very first step: just record reference price. + slots[1] = spotPrice; + slots[4] = 1; + } else { + // We have a previous step reference — compute return. + uint256 prevSpot = slots[1]; + uint256 diff = absDiff(spotPrice, prevSpot); + uint256 relReturn = wdiv(diff, prevSpot); + uint256 returnSquared = wmul(relReturn, relReturn); + + // Update EWMA variance. + uint256 ewmaVar = slots[2]; + ewmaVar = wmul(LAMBDA, ewmaVar) + wmul(ONE_MINUS_LAMBDA, returnSquared); + slots[2] = ewmaVar; + + // Compute std dev (WAD-scale). + // ewmaVar is WAD-scale. sqrt(ewmaVar * WAD) → WAD-scale stdDev. + uint256 stdDev = sqrt(ewmaVar * WAD); + + // Fee = BASE + SCALE * stdDev. + fee = BASE_FEE + wmul(VOL_SCALE, stdDev); + slots[3] = fee; + + // Update reference price for next step. + slots[1] = spotPrice; + } + + slots[0] = trade.timestamp; + } + // Same-step trades: just return current fee (no EWMA update). + + fee = clampFee(fee); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "EWMAVol_v2c"; + } +} diff --git a/strategies/impact_reactive_v1.sol b/strategies/impact_reactive_v1.sol new file mode 100644 index 00000000..fbd02fc2 --- /dev/null +++ b/strategies/impact_reactive_v1.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title ImpactReactive v1 — Dynamic Fee Strategy +/// @notice Bumps fees after large trades (likely arbs), decays toward optimal base. +/// @dev Simple "bump and decay" regime: spike to SPIKE_FEE on large impact, +/// then linearly decay DECAY_AMOUNT per afterSwap call back to BASE_FEE. +/// +/// Rationale: +/// - Static fee sweep showed 80 bps as optimal (edge 380 vs 344 at 30 bps). +/// - At 80 bps, arbs are rare (no-arb band is ~±0.8%, requiring ~8.5σ moves). +/// - Dynamic value comes from protecting during rare volatility spikes. +/// - Trade impact (amountY / reserveY) distinguishes large trades from retail. +/// +/// Storage layout: +/// slots[0] = currentFee (WAD) — current symmetric fee +/// slots[1] = lastTimestamp — last observed trade timestamp +/// slots[2..31] — reserved for future iterations +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice Base fee: optimal static fee from sweep (80 bps) + uint256 internal constant BASE_FEE = 80 * BPS; + + /// @notice Spike fee after detecting a large trade (150 bps) + uint256 internal constant SPIKE_FEE = 150 * BPS; + + /// @notice Trade is "large" if amountY > 0.5% of reserveY + uint256 internal constant IMPACT_THRESHOLD = WAD / 200; + + /// @notice Fee decays by 3 bps per afterSwap call + uint256 internal constant DECAY_AMOUNT = 3 * BPS; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + slots[0] = BASE_FEE; + return (BASE_FEE, BASE_FEE); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 fee = slots[0]; + + // Measure trade impact: amountY relative to post-trade reserveY. + // Safe: reserveY is always > 0 after a valid trade. + uint256 impact = wdiv(trade.amountY, trade.reserveY); + + if (impact > IMPACT_THRESHOLD) { + // Large trade detected (likely arb or large informed flow). + // Spike fee to protect against continued adverse selection. + if (SPIKE_FEE > fee) { + fee = SPIKE_FEE; + } + } else { + // Small trade (likely retail). Decay toward base fee. + if (fee > BASE_FEE + DECAY_AMOUNT) { + fee = fee - DECAY_AMOUNT; + } else if (fee > BASE_FEE) { + fee = BASE_FEE; + } + } + + slots[0] = fee; + slots[1] = trade.timestamp; + + fee = clampFee(fee); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "ImpactReactive_v1"; + } +} diff --git a/strategies/inventory_linear_v4b.sol b/strategies/inventory_linear_v4b.sol new file mode 100644 index 00000000..3122b75e --- /dev/null +++ b/strategies/inventory_linear_v4b.sol @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title InventoryLinear v4b — Inventory-Deviation Fee Adjustment +/// @notice Adjusts fees based on how far reserves have drifted from equilibrium. +/// @dev Inspired by Baggiani et al. (2025): "linear fee approximations capture +/// 95%+ of optimal revenue." When reserves are balanced (near initial ratio), +/// use base fee. When skewed, adjust fees directionally: +/// +/// If we have too much X (reserveX > initialX → spot < fair): +/// - Raise bidFee (discourage more X inflow) +/// - Lower askFee (encourage X outflow / attract buy-X orders) +/// If we have too much Y (reserveX < initialX → spot > fair): +/// - Lower bidFee (encourage X inflow / attract sell-X orders) +/// - Raise askFee (discourage more Y inflow) +/// +/// The adjustment is LINEAR in the log-price deviation: +/// deviation = |ln(spot/initial_spot)| ≈ |spot/initial_spot - 1| +/// +/// Combined with the routing asymmetry insight: use a higher base bid fee +/// (sell-X orders route to us regardless) and lower base ask fee (to capture +/// more buy-X orders that face the Y-threshold barrier). +/// +/// Storage layout: +/// slots[0] = initialSpotPrice (WAD) — set at initialization +/// slots[1] = currentBidFee (WAD) +/// slots[2] = currentAskFee (WAD) +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice Base bid fee (high — sell-X always routes to us) + uint256 internal constant BASE_BID = 85 * BPS; + + /// @notice Base ask fee (lower — to capture more buy-X orders) + uint256 internal constant BASE_ASK = 75 * BPS; + + /// @notice Sensitivity of fee adjustment to inventory deviation + /// @dev At 1% deviation from equilibrium: adjustment = 0.01 * 500 * BPS = 5 bps + uint256 internal constant INVENTORY_SENSITIVITY = 500 * BPS; + + /// @notice Floor and ceiling for fees + uint256 internal constant FEE_FLOOR = 50 * BPS; + uint256 internal constant FEE_CEILING = 120 * BPS; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256 initialX, uint256 initialY) + external override returns (uint256 bidFee, uint256 askFee) + { + // Store initial spot price for deviation calculation. + uint256 initSpot = wdiv(initialY, initialX); + slots[0] = initSpot; + slots[1] = BASE_BID; + slots[2] = BASE_ASK; + return (BASE_BID, BASE_ASK); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 initSpot = slots[0]; + uint256 currentSpot = wdiv(trade.reserveY, trade.reserveX); + + // Compute deviation: |currentSpot - initSpot| / initSpot (WAD fraction). + uint256 diff = absDiff(currentSpot, initSpot); + uint256 deviation = wdiv(diff, initSpot); + + // Compute adjustment: linear in deviation. + uint256 adjustment = wmul(INVENTORY_SENSITIVITY, deviation); + + uint256 newBidFee; + uint256 newAskFee; + + if (currentSpot < initSpot) { + // Spot dropped → we have too much X (or too little Y). + // Raise bid fee (discourage more X), lower ask fee (encourage X sales). + newBidFee = BASE_BID + adjustment; + newAskFee = adjustment < BASE_ASK ? BASE_ASK - adjustment : FEE_FLOOR; + } else { + // Spot rose → we have too little X (or too much Y). + // Lower bid fee (encourage X inflow), raise ask fee (discourage Y inflow). + newBidFee = adjustment < BASE_BID ? BASE_BID - adjustment : FEE_FLOOR; + newAskFee = BASE_ASK + adjustment; + } + + // Apply floor and ceiling. + if (newBidFee < FEE_FLOOR) newBidFee = FEE_FLOOR; + if (newAskFee < FEE_FLOOR) newAskFee = FEE_FLOOR; + if (newBidFee > FEE_CEILING) newBidFee = FEE_CEILING; + if (newAskFee > FEE_CEILING) newAskFee = FEE_CEILING; + + slots[1] = newBidFee; + slots[2] = newAskFee; + + return (clampFee(newBidFee), clampFee(newAskFee)); + } + + function getName() external pure override returns (string memory) { + return "InventoryLinear_v4b"; + } +} diff --git a/strategies/reverse_asym_v4a.sol b/strategies/reverse_asym_v4a.sol new file mode 100644 index 00000000..5014a68d --- /dev/null +++ b/strategies/reverse_asym_v4a.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title ReverseAsym v4a — Routing-Informed Asymmetric Fees +/// @notice Exploits the directional routing asymmetry caused by reserve ratio. +/// @dev With initial reserves x=100, y=10000, routing thresholds differ: +/// - Sell-X orders (bidFee): threshold ≈ x₀ * ε / (r * γ_norm) in X units. +/// At 90 bps: ~0.30 X. Since mean order = 20 X, virtually ALL orders +/// route to us → charge HIGH bid fee for maximum margin. +/// - Buy-X orders (askFee): threshold ≈ y₀ * ε / (r * γ_norm) in Y units. +/// At 60 bps: ~15 Y. With lognormal mean ≈ 20 Y, captures ~36% of +/// buy orders vs ~22% at 80 bps → set LOWER ask fee for more volume. +/// +/// The asymmetry arises because x₀ = 100 (small) → X thresholds tiny, +/// while y₀ = 10000 (large) → Y thresholds meaningful. +/// +/// Storage layout: +/// (no dynamic state — purely static asymmetric) +contract Strategy is AMMStrategyBase { + /// @notice High bid fee — we capture nearly ALL sell-X orders regardless + uint256 internal constant BID_FEE = 90 * BPS; + + /// @notice Low ask fee — lowers buy-X routing threshold, captures more orders + uint256 internal constant ASK_FEE = 70 * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + return (BID_FEE, ASK_FEE); + } + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + { + return (BID_FEE, ASK_FEE); + } + + function getName() external pure override returns (string memory) { + return "ReverseAsym_v4a_90_70"; + } +} diff --git a/strategies/static_asym.sol b/strategies/static_asym.sol new file mode 100644 index 00000000..4c8e44c6 --- /dev/null +++ b/strategies/static_asym.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Static Asymmetric Fee Strategy (A1 variant) +/// @notice Fixed asymmetric bid/ask fees — tests whether mild asymmetry +/// around the 80 bps optimum can capture additional edge. +/// +/// Rationale: +/// - Static 80 bps is the best symmetric fee (edge 380.06). +/// - Retail flow may have a directional bias we can exploit. +/// - By charging slightly more on one side, we may improve +/// revenue without losing meaningful routing share. +/// +/// Storage: none needed (stateless) +contract Strategy is AMMStrategyBase { + /// @notice Bid fee: charged when AMM buys X (82 bps) + uint256 internal constant BID_FEE = 82 * BPS; + + /// @notice Ask fee: charged when AMM sells X (78 bps) + uint256 internal constant ASK_FEE = 78 * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + return (clampFee(BID_FEE), clampFee(ASK_FEE)); + } + + function afterSwap(TradeInfo calldata) + external override returns (uint256 bidFee, uint256 askFee) + { + return (clampFee(BID_FEE), clampFee(ASK_FEE)); + } + + function getName() external pure override returns (string memory) { + return "StaticAsym_82_78"; + } +} diff --git a/strategies/timestamp_two_tier_v2a.sol b/strategies/timestamp_two_tier_v2a.sol new file mode 100644 index 00000000..3f215d1a --- /dev/null +++ b/strategies/timestamp_two_tier_v2a.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title TimestampTwoTier v2a — Dynamic Fee Strategy +/// @notice Exploits within-step timing: arbs trade first, then retail arrives. +/// @dev After a timestamp change (new simulation step), the first trade is +/// likely the arb correcting price. Set LOW fee afterward to attract +/// retail flow in the same step. On subsequent same-step trades (retail), +/// raise fee back to HIGH for the next step's arb. +/// +/// Rationale: +/// - Simulation loop per step: price moves → arb trades → retail arrives. +/// - afterSwap fires after each trade. The fee returned applies to the NEXT trade. +/// - After the arb corrects price, lowering the fee captures retail that would +/// otherwise route to the normalizer (which charges fixed 30 bps). +/// - We undercut the normalizer (29 bps < 30 bps) only in the retail window, +/// then raise back to 80 bps before the next step's arb. +/// +/// Storage layout: +/// slots[0] = lastTimestamp — last observed trade timestamp +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice Low fee set after first trade of a new step (undercut normalizer) + uint256 internal constant LOW_FEE = 29 * BPS; + + /// @notice High fee set after retail trades (protect against next step's arb) + uint256 internal constant HIGH_FEE = 80 * BPS; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + // Start with HIGH_FEE — first trade of step 1 will be an arb. + return (HIGH_FEE, HIGH_FEE); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 lastTimestamp = slots[0]; + + uint256 fee; + if (trade.timestamp > lastTimestamp) { + // New step — this trade was the first of the step (likely arb). + // Set LOW fee to attract retail that follows in this same step. + fee = LOW_FEE; + } else { + // Same step — this trade was retail (or additional arb). + // Set HIGH fee for the next step's arb. + fee = HIGH_FEE; + } + + slots[0] = trade.timestamp; + + fee = clampFee(fee); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "TimestampTwoTier_v2a"; + } +} diff --git a/strategies/timestamp_v3d.sol b/strategies/timestamp_v3d.sol new file mode 100644 index 00000000..7ebff2a2 --- /dev/null +++ b/strategies/timestamp_v3d.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Timestamp v3d — Moderate Two-Tier With Higher Base +/// @notice Alternate between 60 bps (retail window) and 90 bps (arb window). +/// @dev Unlike v2a which undercut the normalizer (29 bps), this uses a more +/// moderate low fee (60 bps) that still captures reasonable margin while +/// having a lower routing threshold. The high fee (90 bps) applies to +/// same-step continuation trades and the next step's arb. +/// +/// At 60 bps: routing threshold ~15 Y, captures ~36% of orders. +/// At 90 bps: routing threshold ~30 Y, captures ~17% of orders. +/// +/// Storage layout: +/// slots[0] = lastTimestamp +contract Strategy is AMMStrategyBase { + uint256 internal constant LOW_FEE = 60 * BPS; + uint256 internal constant HIGH_FEE = 90 * BPS; + + function afterInitialize(uint256, uint256) + external override returns (uint256 bidFee, uint256 askFee) + { + return (HIGH_FEE, HIGH_FEE); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 lastTimestamp = slots[0]; + + uint256 fee; + if (trade.timestamp > lastTimestamp) { + // New step — arb just happened. Set low fee for retail. + fee = LOW_FEE; + } else { + // Same step — retail just traded. Set high fee. + fee = HIGH_FEE; + } + + slots[0] = trade.timestamp; + fee = clampFee(fee); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "Timestamp_v3d_60_90"; + } +} diff --git a/strategies/vol_regime_dir_v5.sol b/strategies/vol_regime_dir_v5.sol new file mode 100644 index 00000000..895090fe --- /dev/null +++ b/strategies/vol_regime_dir_v5.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Dual-EWMA Volatility Regime Strategy +/// @notice Two-layer dynamic fee adjustment based on realized volatility: +/// 1. Volatility scaling: fee = baseFee * (0.5 + 0.5 * shortVar/longVar) +/// 2. Regime detection: +3 bps when vol is increasing, -3 bps when decreasing +/// +/// Key mechanism: A fast EWMA (lambda=0.90) and slow EWMA (lambda=0.98) of +/// squared spot price returns create a self-calibrating volatility ratio. +/// - After large trades (arbs): shortVar spikes → fee increases → arb protection +/// - During quiet periods (retail): shortVar decays → fee decreases → attracts retail +/// - The ratio self-calibrates per simulation, adapting to each market's volatility +/// +/// This beats static 80 bps by ~28 edge points (407.97 vs 380.06, t=127.91, +/// 100% win rate across 1000 paired simulations). +/// +/// Storage layout: +/// slots[0] = lastTimestamp +/// slots[1] = prevSpotPrice (WAD) +/// slots[2] = shortTermVar (WAD, fast EWMA lambda=0.90) +/// slots[3] = longTermVar (WAD, slow EWMA lambda=0.98) +/// slots[4] = initialized (0 or 1) +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice Base fee: 80 bps (optimal static fee from sweep) + uint256 internal constant BASE_FEE = 80 * BPS; + + /// @notice Regime adjustment: 3 bps — added when vol increasing, subtracted when decreasing + uint256 internal constant REGIME_ADJUST = 3 * BPS; + + /// @notice Fast EWMA decay: lambda = 0.90 (responsive to recent vol) + uint256 internal constant LAMBDA_SHORT = 900000000000000000; + uint256 internal constant ONE_MINUS_SHORT = 100000000000000000; + + /// @notice Slow EWMA decay: lambda = 0.98 (smoothed long-term vol) + uint256 internal constant LAMBDA_LONG = 980000000000000000; + uint256 internal constant ONE_MINUS_LONG = 20000000000000000; + + uint256 internal constant HALF_WAD = WAD / 2; + + /// @notice Initial variance estimate: ~2e-6 in decimal (per-trade squared return) + uint256 internal constant INIT_VAR = 2000000000000; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256 initialX, uint256 initialY) + external override returns (uint256 bidFee, uint256 askFee) + { + slots[1] = wdiv(initialY, initialX); // prevSpotPrice + slots[2] = INIT_VAR; // shortTermVar + slots[3] = INIT_VAR; // longTermVar + slots[4] = 1; // initialized + return (clampFee(BASE_FEE), clampFee(BASE_FEE)); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 spotPrice = wdiv(trade.reserveY, trade.reserveX); + uint256 prevSpot = slots[1]; + uint256 shortVar = slots[2]; + uint256 longVar = slots[3]; + + // ── Update EWMA variance estimates ──────────────────────────────── + if (slots[4] == 1 && prevSpot > 0) { + uint256 delta = absDiff(spotPrice, prevSpot); + // Squared return = (delta / prevSpot)^2, all in WAD + uint256 sqReturn = wdiv(wmul(delta, delta), wmul(prevSpot, prevSpot)); + + // Fast EWMA (lambda=0.90): reacts quickly to recent vol + shortVar = wmul(LAMBDA_SHORT, shortVar) + wmul(ONE_MINUS_SHORT, sqReturn); + // Slow EWMA (lambda=0.98): stable long-term vol estimate + longVar = wmul(LAMBDA_LONG, longVar) + wmul(ONE_MINUS_LONG, sqReturn); + + slots[2] = shortVar; + slots[3] = longVar; + } + + slots[0] = trade.timestamp; + slots[1] = spotPrice; + slots[4] = 1; + + // ── Vol-adjusted base fee ───────────────────────────────────────── + // Multiplier = 0.5 + 0.5 * (shortVar / longVar), clamped to [0.5, 2.0] + // When shortVar = longVar: multiplier = 1.0 → fee = baseFee + // When shortVar > longVar (vol increasing): fee > baseFee + // When shortVar < longVar (vol decreasing): fee < baseFee + uint256 fee = BASE_FEE; + if (longVar > 0) { + uint256 ratio = wdiv(shortVar, longVar); + uint256 scaledHalf = wmul(HALF_WAD, ratio); + uint256 multiplier = HALF_WAD + scaledHalf; + if (multiplier > 2 * WAD) multiplier = 2 * WAD; + if (multiplier < HALF_WAD) multiplier = HALF_WAD; + fee = wmul(BASE_FEE, multiplier); + } + + // ── Regime adjustment ───────────────────────────────────────────── + // If short-term vol > long-term vol → vol is increasing → raise fee + // If short-term vol < long-term vol → vol is decreasing → lower fee + if (longVar > 0) { + if (shortVar > longVar) { + fee = fee + REGIME_ADJUST; + } else if (fee > REGIME_ADJUST) { + fee = fee - REGIME_ADJUST; + } + } + + fee = clampFee(fee); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "DualEWMA_VolRegime_80_3"; + } +} diff --git a/strategies/vol_responsive_v5.sol b/strategies/vol_responsive_v5.sol new file mode 100644 index 00000000..23f68ff3 --- /dev/null +++ b/strategies/vol_responsive_v5.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {AMMStrategyBase} from "./AMMStrategyBase.sol"; +import {TradeInfo} from "./IAMMStrategy.sol"; + +/// @title Volatility-Responsive Fee Strategy (V1 variant) +/// @notice Tracks EWMA of squared spot price returns and scales the fee +/// proportionally. Higher realized vol → higher fee (protect from arbs). +/// Lower vol → lower fee (attract more retail). +/// +/// Fee formula: baseFee * (0.5 + 0.5 * ewmaVar / nominalVar) +/// - When ewmaVar = nominalVar → fee = baseFee +/// - When ewmaVar = 0 → fee = baseFee * 0.5 +/// - When ewmaVar = 2*nominal → fee = baseFee * 1.5 +/// Clamped to [50%, 200%] of baseFee. +/// +/// Storage layout: +/// slots[0] = prevSpotPrice (WAD) +/// slots[1] = ewmaVariance (WAD-scaled squared return) +/// slots[2] = lastTimestamp +/// slots[3] = initialized (0 or 1) +contract Strategy is AMMStrategyBase { + // ── Constants ──────────────────────────────────────────────────────── + + /// @notice Base fee: 80 bps (optimal static fee) + uint256 internal constant BASE_FEE = 80 * BPS; + + /// @notice EWMA decay factor: 0.94 (moderate smoothing) + uint256 internal constant LAMBDA = 940000000000000000; + uint256 internal constant ONE_MINUS_LAMBDA = 60000000000000000; + + /// @notice Nominal variance: ~2e-6 in decimal (calibrated per-trade squared return) + uint256 internal constant NOMINAL_VAR = 2000000000000; + + /// @notice Volatility scale: 1.0 (full responsiveness) + uint256 internal constant VOL_SCALE = 1000000000000000000; + + uint256 internal constant HALF_WAD = WAD / 2; + uint256 internal constant MIN_MULTIPLIER = HALF_WAD; + uint256 internal constant MAX_MULTIPLIER = 2 * WAD; + + // ── Interface ──────────────────────────────────────────────────────── + + function afterInitialize(uint256 initialX, uint256 initialY) + external override returns (uint256 bidFee, uint256 askFee) + { + // Initialize spot price and variance estimate + slots[0] = wdiv(initialY, initialX); + slots[1] = NOMINAL_VAR; + slots[3] = 1; + return (clampFee(BASE_FEE), clampFee(BASE_FEE)); + } + + function afterSwap(TradeInfo calldata trade) + external override returns (uint256 bidFee, uint256 askFee) + { + uint256 spotPrice = wdiv(trade.reserveY, trade.reserveX); + uint256 prevSpot = slots[0]; + uint256 ewmaVar = slots[1]; + + // Update EWMA variance from squared spot return + if (slots[3] == 1 && prevSpot > 0) { + uint256 delta = absDiff(spotPrice, prevSpot); + // sqReturn = (delta/prevSpot)^2 in WAD + uint256 sqReturn = wdiv(wmul(delta, delta), wmul(prevSpot, prevSpot)); + ewmaVar = wmul(LAMBDA, ewmaVar) + wmul(ONE_MINUS_LAMBDA, sqReturn); + slots[1] = ewmaVar; + } + + slots[0] = spotPrice; + slots[2] = trade.timestamp; + + // Compute fee multiplier: 0.5 + 0.5 * volScale * ewmaVar / nominalVar + uint256 ratio = wdiv(ewmaVar, NOMINAL_VAR); + uint256 scaledRatio = wmul(VOL_SCALE, ratio); + uint256 multiplier = HALF_WAD + scaledRatio / 2; + if (multiplier < MIN_MULTIPLIER) multiplier = MIN_MULTIPLIER; + if (multiplier > MAX_MULTIPLIER) multiplier = MAX_MULTIPLIER; + + uint256 fee = clampFee(wmul(BASE_FEE, multiplier)); + return (fee, fee); + } + + function getName() external pure override returns (string memory) { + return "VolResponsive_80"; + } +}