Related articles: The Agentic Quant Desk · Part 1: Building the Harness
Aerodrome (Base) LP management + sniping primitives, with a dashboard — in one repo. Discover pools, read gauge emissions, compute position PnL & impermanent loss, mint/stake/remove on-chain, and see it all in a Streamlit UI.
Extracted from a private trading desk. Mechanics only — pool discovery, gauge/PnL/IL reads, an on-chain executor, and a dashboard. The tuned sniping selection, range-sizing, and rebalance triggers (the edge) stay private; sniping is shown here as primitives + a trivial example rule you replace.
src/aero_lp/ on-chain primitives
pool discovery · gauge/emission · PnL & IL · mint/stake/remove executor
deployment registry · math · backtest CL valuation
src/aero_dashboard/ Streamlit UI
JSONL loaders · equity/drawdown/metrics components
metrics.py SessionAnalyzer · live.py glue → aero_lp
examples/ manage_demo.py (live PnL) · snipe_demo.py (rank + dry-run mint)
backtest_lp.py (offline sim) · sample-snapshots/ (demo data)
pip install -e . gives you both: import aero_lp to act on-chain, aero-dashboard (or streamlit run) to visualize.
pip install -e .
streamlit run src/aero_dashboard/app.py # 3 bundled samples: equity, drawdown, metrics, hedgeKeys are encrypted at rest (Ethereum keystore — scrypt + AES-128-CTR). Only the password is needed at runtime; no plaintext key ever touches disk or the env.
python -m aero_lp.aerodrome.cli_keystore generate # new wallet → keystore (prompts password)
python -m aero_lp.aerodrome.cli_keystore encrypt # or encrypt a key you already have
python -m aero_lp.aerodrome.cli_keystore info # show address (no decrypt)
python -m aero_lp.aerodrome.cli_keystore verify # test decryptionDefault keystore file is base_wallet.json (override with KEYSTORE_PATH). At runtime:
export KEYSTORE_PASSWORD='...' # only secret in the env; key stays encrypted on diskThe executor resolves a key automatically, in order: explicit private_key= → encrypted keystore
(KEYSTORE_PATH + KEYSTORE_PASSWORD) → plaintext BASE_PRIVATE_KEY. So once the keystore + password
are set, construct AerodromeExecutor(...) with no private_key= and it signs from the keystore.
Helpers are re-exported for convenience:
from aero_lp import load_key, get_keystore_address, get_default_wallet_address
addr = get_default_wallet_address() # read address without decrypting.gitignore already excludes *keystore*.json and base_wallet*.json. A generated wallet still needs
manual funding (ETH for gas + pair tokens on Base).
from aero_dashboard.live import live_positions_frame # toolkit → dashboard frame
df = live_positions_frame(rpc_url, [{"token_id": 1073536, "gauge": "0x61E0B104..."}])
# df has equity_usd, il_pct, in_range, aero_per_day — render it in the dashboard
from aero_lp import GaugeMonitor, AerodromeExecutor, SLIPSTREAM_V3_CONTRACTS
gm = GaugeMonitor(rpc_url=rpc) # read emissions / PnL
ex = AerodromeExecutor( # mint/stake (dry-run)
dry_run=True, nfpm_address=SLIPSTREAM_V3_CONTRACTS["nfpm"], rpc_url=rpc,
)See examples/manage_demo.py and examples/snipe_demo.py. Writes default to dry-run; supply RPC +
keystore/private key (.env.example) to send. Verify gauge.nft() == executor.nfpm before staking a
migrated pair.
The dashboard reads the same snapshot schema the toolkit (and the private bots) produce. live.py is the
bridge: it calls aero_lp.GaugeMonitor.get_position_pnl(...) and shapes the result into the exact
DataFrame the dashboard components render — so the same UI shows backtest samples (offline) or your live
book (RPC).
Full walkthrough — discover → range → mint → stake → monitor → rebalance → exit — for WETH/cbBTC
CL50 using either an S1-style sniper (emission-ranked entry) or an S17-style directional LP
(hold + rebalance): docs/running-lp-strategies.md.
A CL valuation engine ships in aero_lp.backtest (position value as price moves, IL vs HODL) and a
session scorer in aero_dashboard.SessionAnalyzer (return, Sharpe, drawdown, time-in-range,
rebalance efficiency — the same metrics used on paper/live data). What's not shipped is the price
feed and the rebalance rule — those are yours (real candles + your edge).
python examples/backtest_lp.py # synthetic price path → snapshots.jsonl → printed metrics
# → return / sharpe / max drawdown / time-in-range / rebalances, renderable in the dashboardfrom aero_lp.backtest import LPPosition, create_position, hodl_value
pos = create_position(price=20.0, capital_usd=10_000, range_width_ticks=10, tick_spacing=50, ts=0)
v = pos.value_at(22.0) # CL value at a new price
il = v - hodl_value(10_000, 20.0, 22.0) # LP vs holding the tokens (impermanent loss)
from aero_dashboard import SessionAnalyzer
SessionAnalyzer.load("examples/sample-snapshots/sharpe-optimal-hedge").summary() # score a runRunnable examples, same engine, one per strategy family:
examples/backtest_lp.py— S1 sniper: neutral centered range, re-center on out-of-range.examples/backtest_s17.py— S17 directional: asymmetric range placement (create_position(direction=, offset_pct=)) leaned by a stub momentum signal + TP/SL exits.examples/backtest_s2_hedge.py— S2 / Sharpe-hedge delta-neutral: LP + perp short, sweeping hedge ratio h ∈ {0, h*, 1}. The Sharpe-optimal h* is the closed-formcompute_h_star(Hane 2026), clamped at 0.70 — the sameh_actual=0.7the dashboard's hedge sample-snapshots show.examples/backtest_compare.py— S1 vs S17 vs HODL on one path;walk_forward.py— many windows.
All examples fetch real Bybit prices (public klines, no API key) and fall back to synthetic GBM only if offline:
from aero_lp.backtest import pair_price_path
prices = pair_price_path("WETH", "cbBTC", "1h", 240) # → ETHBTCUSDT closes (cross-pair)
prices = pair_price_path("WETH", "USDC", "1h", 240) # → ETHUSDT closes (direct)Pair→symbol resolution mirrors the live desk: direct (X/stable → that symbol), cross (WETH/cbBTC → ETHBTCUSDT), synthetic (ratio of two USDT legs).
Walk-forward (examples/walk_forward.py) — one path is one noisy number; slice a long history into
many windows and judge against the distribution:
from aero_lp.backtest import pair_price_path, sliding_windows, bootstrap_windows, summarize
prices = pair_price_path("WETH", "USDC", "1h", 1000)
rets = [run_s1(w) for w in sliding_windows(prices, window=168, stride=24)] # 7d win, 1d step
summarize(rets) # → {mean, median, p5, p95, win_rate, ...}Your own signal/rule still replaces the stubs. (scenario_builder regime labelling / signal-driven sims
/ tuned policies stay private.)
Real APR, not a guess (examples/live_apr_backtest.py): emission APR is range/time-specific, so fetch
it from a live position rather than hardcoding —
from aero_lp import GaugeMonitor
from aero_lp.backtest import live_position_apr # = aero_per_day_usd * 365 / position_value_usd
apr = live_position_apr(GaugeMonitor(rpc_url=RPC), token_id, gauge)["apr_pct"] # e.g. 192%live_gauge_inputs(...) returns the liquidity-share inputs (gauge AERO/day, AERO price, active-tick TVL)
for range-sweeps; GaugeMonitor.rank_pools_by_emission(t0, t1) gives APR for pools you haven't entered.
The tuned sniping logic — snipe_engine (efficiency scoring / pool selection), vol_range (adaptive
range sizing), the rebalance/dust triggers, and the backtest's tuned layer (scenario_builder regime
construction, signal-driven sims, rebalance policies). Those are the alpha. You get the primitives + a
"pick the highest-emission pool" / "re-center on OOR" stub to replace with your own.
pip install -e ".[dev]" && pytest # primitives + IL math + dashboard render + alpha-absenceMIT.