Ansh Dani

C++ limit order book and market-making simulator

I built a deterministic, single-threaded C++20 limit order book with price-time priority, and a simulator that runs a naive symmetric market maker and an Avellaneda–Stoikov inventory-aware one against synthetic and ITCH-calibrated order flow. The lesson for passive-quote backtests: the flow model mattered far more than the strategy.

The engine

It handles partial fills, cancel and modify (a price change or size increase loses time priority), self-trade prevention that rejects the incoming order, and executions against named resting orders, which I used to replay 12,423 Nasdaq ITCH messages for QQQ. Two book layouts (std::map and a fixed tick-range array) share one matching path and pass the same tests.

Speed

On 1,000,000 synthetic events (seed 42, map book, Apple M3) it processed 3,723,012 events per second, with 125 ns median and 1,208 ns 99th-percentile latency. Timing covers only the matching call, with no I/O. The macOS timer ticks about every 41.7 ns, so 125 ns is three ticks. Reruns on the same M3 model ranged from 1.44M events per second under heavy load to 5.01M at low load.

Against Liquibook

I compared it with Liquibook, an open-source C++ order book, under a protocol fixed before any timed run: same 1M-event synthetic stream, flags, harness and timer. Both produced the same 372,120 trades, in order, and the same final book. Over five interleaved runs each, my book's median was 5.32M events per second against Liquibook's 2.09M (five-run ranges did not overlap), and median latency 125 ns against 250 ns. The gap comes mainly from this stream's very deep queues (the book grows to about 145,000 resting orders): Liquibook walks the queue at a price to find an order to cancel or replace. In an exploratory test on a near-empty book, Liquibook was faster.

Market-making results

Across 30 seeds of hand-chosen high-volatility flow with no inventory cap, Avellaneda–Stoikov quotes cut mean inventory variance from 74.2 million to 25.8 million and raised risk-adjusted P&L (net P&L after fees and any inventory penalty, divided by maximum drawdown) by 1.06, 95% paired interval 0.62 to 1.50. Net P&L did not separate, and under ITCH-calibrated flow there was no risk-adjusted gain in any regime.

The flow drove fills. With the event mix and order sizes taken from the ITCH data, fill rates for both strategies fell from 60–74% to 1.5–1.9% (ten seeds, 2,500-event runs). Sparse executions dominate: about 10 external executions per run instead of about 600. Placing quotes first in the queue did not rescue them; raising execution intensity raised fills steadily.

Line chart of inventory over 200,000 simulated events for naive quotes (blue) and Avellaneda–Stoikov quotes (red). The naive inventory drifts much further from zero and ends near 10,000 units; the Avellaneda–Stoikov inventory ends near 1,200.
Inventory over one 200,000-event simulated run (one seed, hand-chosen high-volatility flow, no inventory cap): under Avellaneda–Stoikov quotes (red) it ends at 1,178 units, under naive quotes (blue) at 10,270. One seed only illustrates the effect; the 30-seed result is in the text.

Limits

This is a one-instrument simulation, not live trading. The ITCH calibration uses only the event mix and order sizes from a prefix of one public sample day with 57 executions; prices, regimes and queue dynamics stay synthetic. My quote size (10) was not recalibrated to ITCH order sizes (about 1,300 on average), so calibrated fill rates also reflect deep queues ahead of small quotes. Speed numbers come from one laptop, one thread and synthetic streams, and the hot path still allocates memory: this is not a production design.

Reproduce it

Build and test with CMake, then run the speed benchmark and the two market-making checkpoints from the README. The 30-seed statistics have their exact commands in docs/stage5c_seed_statistics.md; the Liquibook comparison is in benchmarks/ext3/ on branch ext3-named-baseline.

cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release
ctest --test-dir build --output-on-failure
python3 benchmarks/run_benchmark.py --events 1000000 --warmup 20000 --seed 42 --output-dir benchmarks/results/stage2_local --build-dir build/stage2_benchmark
python3 simulations/run_market_maker.py --strategy naive --events 200000 --markout-horizon 50 --curve-sample-stride 100 --output-dir benchmarks/results/stage3_naive_checkpoint --build-dir build/stage3_market_maker
python3 simulations/run_market_maker.py --strategy avellaneda-stoikov --events 200000 --markout-horizon 50 --curve-sample-stride 100 --output-dir benchmarks/results/stage3_avellaneda_stoikov_checkpoint --build-dir build/stage3_market_maker

GitHubResearch noteFill-rate diagnosticsLiquibook comparison results