A simulator-backed market-making lab — synthetic and live crypto L2 order books, an Avellaneda-Stoikov quoter with inventory-aware skew, a constant-spread benchmark, and a matching engine that surfaces fills, inventory risk, and PnL per snapshot. Built to show what the model does on a real fill stream, not what it promises in a textbook.
Market-making and execution desks live and die on microstructure: where your order sits in the queue, how latency moves your fill probability, what inventory risk you're carrying. A notebook that classifies the next tick is not interesting to them. A simulator-backed study of spread, fills, and inventory under explicit assumptions — and honest about where the textbook model and real-fill dynamics part ways — is exactly their language.
-
Avellaneda & Stoikov (2008), Quantitative Finance — inventory-aware optimal market making; the reservation-price + spread framework this repo implements.
-
Cont, Stoikov & Talreja (2010), Operations Research — a stochastic LOB model that would underpin a calibrated k estimate. v2 roadmap item.
-
Byrd, Hybinette & Balch (2020), ABIDES — agent-based market simulator with pairwise latency and NASDAQ-style protocols (BSD-3-Clause). Heavier install; v2 path beyond the synthetic + Coinbase MVP.
-
Zhang, Zohren & Roberts (2019), DeepLOB, IEEE TSP — LOB data carries signal, but classification accuracy ≠ tradable edge.
-
Master's thesis to reverse-engineer (surfaced — read & vet next):
- KTH, Generative models of limit order books — kth.diva-portal.org
- Companion: Optimal Execution with Reinforcement Learning on ABIDES — arXiv 2411.06389
Math we're stealing: the LOB state representation and an RL execution benchmark inside ABIDES against TWAP/VWAP.
-
Frontier to push beyond (2025–2026): Deep RL for Optimal Trading with Partial Information (arXiv 2511.00190); Deep RL in Non-Markov Market-Making (MDPI Risks, 2025).
Hypothesis: execution edge is structural and measurable in simulation before it's risked in production; the same strategy code must run in backtest, sim, and live without a logic change.
| Source | Provides | Cost | License caveat |
|---|---|---|---|
| Synthetic L2 generator | Deterministic GBM mid + Pareto-tailed depth | Free | None — reproducible under seed |
Coinbase REST /products/{id}/book?level=2 |
Live aggregated L2 snapshot | Free, no key | Polite User-Agent (set COINBASE_USER_AGENT) |
| ABIDES simulator | Synthetic high-fidelity LOB | Free | BSD-3 (roadmap.) |
| Nasdaq Nordic feeds | Real Nordic equity microstructure | Paid | Licensed — roadmap only. |
The synthetic generator models each piece carefully: mid as geometric
Brownian motion, half-spread as log-normal (matching real markets'
right-skewed spread distribution), and depth per level as Pareto-
distributed (matching real L2's heavy-tailed sizes). Determinism under a
seeded np.random.Generator so backtests reproduce exactly.
flowchart LR
A[Synthetic L2 generator<br/>or Coinbase REST] --> B[BookSnapshot<br/>BookSeries]
B --> C[Strategy<br/>AvellanedaStoikov / ConstantSpread]
C --> D[Quote<br/>bid/ask + sizes]
D --> E[Matching engine<br/>crossed-price + fill_probability]
E --> F[Fill log]
F --> G[run_backtest loop<br/>day-by-day equity, inventory, cash]
G --> H[MM Stats<br/>PnL, inventory RMSE, fill rate, Sharpe]
H --> I[CSV + PNG artifacts]
Three disciplines do most of the work:
-
The strategy and matching layers know nothing about each other. Either swap a strategy without touching matching, or swap the data source (synthetic ↔ Coinbase) without touching either. This is what makes the same code runnable against ABIDES later (v2).
-
The matching engine is deliberately simple and named about it. "If next snapshot's best ask crosses our resting bid, the bid was filled, multiplied by
fill_probabilityto stand in for queue position." No partial fills, no latency, no L3 queue model — each is a documented limitation, not a bug. -
A-S's inventory-skew property is unit-tested directly, not just asserted from a backtest.
test_avellaneda_stoikov_long_inventory_skews_quotes_downverifies the reservation price moves below the mid when inventory is long, the way the math says it should. Whether that property wins a synthetic-data backtest depends on calibration (see Results below).
End-to-end CLI on a 2,000-snapshot synthetic stream (BTC-USD, ann.vol=1.5, mean half-spread 6 bps, 1-second snapshots):
microlab simulate --out-dir data/sample/ --seed 42
Produces three artifacts:
data/sample/constantspread_run.csvdata/sample/avellanedastoikov_run.csvdata/sample/comparison.png(equity + inventory + mid 3-panel)
The CLI runs A-S and ConstantSpread side-by-side on the same synthetic book stream and prints the headline MM stats for each. Both strategies work mechanically: they post quotes, the matching engine reports fills, inventory and cash update, the equity curve is mark-to-market.
A-S does NOT beat ConstantSpread on inventory_rmse with naive parameters on synthetic GBM data. This is a real finding worth naming:
- Small γ (the inventory-aversion parameter) makes A-S's optimal half-spread very tight — sub-bp on a $50k asset. A-S then gets more fills than CS (which quotes wider) and consequently accumulates more inventory, not less.
- Large γ would make A-S's optimal spread wider than the book BBO, so it gets no fills at all, and the inventory-skew property is moot.
- The "right" γ depends on calibrating against the actual order-flow intensity k, which our simple GBM+log-normal-spread synthetic doesn't model. Cont/Stoikov/Talreja (2010) is the reference for that calibration.
The inventory-skew property itself is real and unit-tested. Hold
γ fixed, feed A-S a +5 BTC fill, and the reservation price (and both
quote sides) move strictly DOWN — exactly what the math predicts.
That's tested directly in
test_avellaneda_stoikov_long_inventory_skews_quotes_down. The
property exists in the implementation; what's missing is the
calibration that makes it pay off against a benchmark on this
synthetic stream.
A real-data demonstration is the v2 work: estimate k from realised aggressive-order arrivals on a captured Coinbase stream, re-solve for the γ that puts A-S at a comparable fill rate to a named benchmark, then re-run the backtest and report inventory RMSE and PnL.
Verified end-to-end (2026-05-26): fetch_book_snapshot("BTC-USD")
pulls a real BTC-USD book at $76,758.18 mid with $0.01 spread (0.13
bps) and 21,591 bid levels × 24,900 ask levels, ~134 BTC depth
within ±50 bps. The same pydantic models accept both the synthetic
and the live data, so the strategy code is data-source-agnostic.
- Synthetic L2 ≠ real LOB dynamics. GBM + log-normal spread + Pareto depth captures the static properties of an L2 book but not the dynamic correlations (order-flow autocorrelation, spread- vs-vol coupling, regime breaks). Cont/Stoikov/Talreja (2010) is the correct generative model; ABIDES is the calibrated simulator.
- Matching is binary at the crossing price, weighted by
fill_probability. No queue-position model, no partial fills, no latency. Each is named inmatching.pyand roadmap items. - A-S calibration is the unresolved problem. With naive γ/k, A-S either over-fires or under-fires on synthetic data. Calibrating k against a captured Coinbase stream is the v2 fix; until then, the CLI ships A-S as a demonstrably-working implementation of the textbook model rather than as a benchmark-beating strategy.
- No transaction-cost beyond the quote spread itself. No exchange fees, no funding cost, no slippage on hits. Real desks subtract all of those.
- No regime detection. σ² is estimated on a fixed rolling window; a regime-aware vol estimator would help during sharp transitions.
- Crypto L2 ≠ Nordic equities. The strategy code is portable but the spreads, queue dynamics, and order-flow statistics differ. The Nasdaq Nordic-feed integration is a paid roadmap item.
- Calibrated k estimation from captured Coinbase L2 streams. Run a multi-day capture, estimate the aggressive-arrival rate at each depth level, solve for the γ that puts A-S at the benchmark fill rate, re-run the comparison. This is the work that turns "implements A-S" into "A-S beats benchmark by X bps inventory risk on real data".
- ABIDES integration for high-fidelity agent-based simulation with pairwise latency and proper queue modelling — the layer where A-S's edge is most cleanly measurable.
- Queue-aware fill model (replacing
fill_probabilitywith a queue-position estimate based on cumulative resting volume at our level). - Licensed Nordic equity feed so the same strategy code can run against the venues that matter for the local market.
- Kernel-bypass / SBE decoding path for true low-latency execution.
- Shared strategy interface with the backtester + a live FIX
gateway so the same
quote()andon_fill()methods run in backtest, sim, and live.
# install
python3.12 -m venv .venv && .venv/bin/pip install -e .
# (optional) polite User-Agent for live Coinbase calls
cp .env.example .env
# edit .env: COINBASE_USER_AGENT="your-project (Your Name email)"
# run the side-by-side demo (synthetic, 2000 snapshots, both strategies)
microlab simulate --out-dir data/sample --seed 42
# experiment with calibration
microlab simulate --as-gamma 0.005 --as-k 0.5 --cs-half-spread-bps 5
microlab simulate --annualized-vol 2.5 --mean-half-spread-bps 8
# tests
pytest # 60 hermetic tests, no network
pytest -m slow # +1 live Coinbase L2 snapshot testEngineering standards: permissive code (MIT), public data first (Coinbase L2, no key), benchmarked (constant-spread is the named baseline), honest about calibration limits, no claim that exceeds the evidence.