Skip to content

Repository files navigation

Nordic Microstructure Lab

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.

License Status Tests Data

Why this matters

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.

Research basis

  • 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):

    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.

Data

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.

System design

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]
Loading

Three disciplines do most of the work:

  1. 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).

  2. 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_probability to stand in for queue position." No partial fills, no latency, no L3 queue model — each is a documented limitation, not a bug.

  3. A-S's inventory-skew property is unit-tested directly, not just asserted from a backtest. test_avellaneda_stoikov_long_inventory_skews_quotes_down verifies 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).

Results

What runs

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:

What the demo shows

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.

What the demo doesn't (yet) show — the honest version

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.

Live Coinbase data path works

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.

Risks & limitations

  • 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 in matching.py and 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.

What I'd build next for a real firm

  1. 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".
  2. 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.
  3. Queue-aware fill model (replacing fill_probability with a queue-position estimate based on cumulative resting volume at our level).
  4. Licensed Nordic equity feed so the same strategy code can run against the venues that matter for the local market.
  5. Kernel-bypass / SBE decoding path for true low-latency execution.
  6. Shared strategy interface with the backtester + a live FIX gateway so the same quote() and on_fill() methods run in backtest, sim, and live.

Reproducibility

# 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 test

Engineering 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.

About

Avellaneda-Stoikov market-maker + constant-spread benchmark on synthetic and live Coinbase L2; honest about the textbook calibration tension.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages