Simulator for concentrated-liquidity range strategies on Arbitrum (Uniswap V3 and forks), built to answer one question before any bot or contract exists: does an actively managed narrow position beat holding, and beat a static wide position, after every cost?
The kill criterion was fixed before any result was seen (see docs/SPEC.md). For stable pairs: net return of at
least 15% per year in at least 2 of 3 consecutive 30-day months, and better than a static ±10% position in at
least 2 of 3 months.
8 Uniswap V3 stable pools on Arbitrum, 3 position sizes ($50, $1k, $10k), 61 strategies each: 1,464 runs, none passes. Window: 90 days, blocks 478,933,741 to 509,690,000.
Best average monthly net return per pool (any size, any strategy), against the 15% threshold:
| Pool | Best average | Strategy |
|---|---|---|
| GHO/USDC 0.05% | 8.9% at $50 (2.8% at $10k) | static |
| USDe/USDT0 0.05% | 2.1% | static |
| USDT0/USDC.e 0.01% | 2.1% | static |
| USDC/USDT0 0.01% | 1.8% | static ±0.05% |
| USDC/DAI 0.01% | 1.6% | static |
| DAI/USDC.e 0.01% | 1.6% | recenter with delay |
| DAI/USDT0 0.01% | 1.2% | recenter with delay |
| USDC/USDC.e 0.01% | 1.0% | recenter |
- The best strategies are static ranges or slow recentering. The tight autobalancer (1 to 5 ticks) is worse than doing nothing: on USDC/USDT0 it makes 403 rebalances in 90 days and loses $94 on $1k and $629 on $10k, because every rebalance pays the pool fee and price impact.
- The limit is the size of the fee pool, not the strategy. After the Uniswap protocol fee, all LPs together earn roughly $53k a year in USDC/USDT0, $1.7k in GHO/USDC and $340 in USDe/USDT0 (swap volume in the window times pool fee, annualised).
- Camelot V3 (Algebra, adaptive fee) is not better: its fee read 25 ppm at every one of 91 sampled blocks, four times below Uniswap's 0.01%, so it earns less per unit of volume on stable pairs.
- Volatile (BTC) pairs were dropped by the owner and only partly run: on the 0.3% pools none of the strategies reached even 0% over HODL in 2 of 3 months except two at $1k.
Raw results: results/2026-09-29/*.json, summary: results/2026-09-29/summarize.py, grid runner:
results/2026-09-29/run_grid.sh.
Stable pools have tickSpacing 1, so the liquidity in the swap event is a poor stand-in for the liquidity at each
tick. lp-sim therefore rebuilds the exact active liquidity from Mint/Burn events plus the pool state at the
block before the window (lp-sim fetch-state), and stops with an error when that data is missing instead of
falling back to an approximation.
- Replayed liquidity equals the liquidity in the Swap event on every swap of the pools that matter (0 mismatches over 62,484 swaps in USDC/USDT0, also 0 in GHO/USDC and USDe/USDT0).
- Fee growth against on-chain
feeGrowthGlobal(lp-sim validate): USDC/USDT0 1.0000 and 0.9992, GHO/USDC 1.0000 and 1.0000, USDe/USDT0 0.9971 and 1.0001 (simulator divided by chain). - Known limit: the walk visits every tick where Uniswap steps only between initialised ticks, so on pools whose price ran through thousands of empty ticks the fee per unit of liquidity is over-counted by up to 1 wei per tick. That is at most about 0.1 token in 90 days, so the dollar results are unaffected, but the fee-per-liquidity validation is not meaningful for USDT0/USDC.e and the DAI/USDC.e pools.
lp-sim fetch-state --pool <pool> --block <block> --db data/lp.db # needs LPSIM_RPC_42161
lp-sim run --pool <pool> --strategy ALL --size 1000 --db data/lp.db --json
lp-sim validate --pool <pool> --from-block <b> --to-block <b> --db data/lp.db
Swap logs come from mev-scout's fetch_swaps; Mint/Burn logs were fetched with the same explorer client.
Pools with a protocol fee need their slot0.feeProtocol read on chain before they are added to the registry.
A new stable pool is only worth testing if the fee pool is much larger than the ones above. Base (Aerodrome
Slipstream) is blocked by the lack of a free source for Base logs, and Arc has no stable pair with volume yet.
Tests: pytest -q (87).