A C++ limit-order-book engine and market-making simulator, driven from a
Python research layer. See
docs/specs/2026-07-03-quotient-design.md.
Honest framing: quotient is a deterministic, single-threaded event-replay simulator. Reported performance numbers are engine microbenchmarks (ns/op) and replay throughput (events/s) — not production/colocated HFT wire latency.
cmake -B build -DQUOTIENT_BUILD_TESTS=ON -DQUOTIENT_BUILD_APPS=ON -DQUOTIENT_BUILD_BENCHMARKS=ON
cmake --build build -j
ctest --test-dir build --output-on-failure
uv venv
uv pip install -e ".[dev]"
uv run pytest -v
The recorder/ package captures Kraken's live L2 order-book feed (WS v2 book
channel) to Parquet, scaling every price/qty to integers and validating each
message against Kraken's CRC-32 checksum live, via the same L2Book C++
reconstructor used by the engine (verified against the real feed — 0
mismatches). recorder/replay.py reads a capture back and re-validates every
checksum offline — deterministic, no network, CI-testable. A small real
capture is checked in at data/samples/btcusd_book_sample.parquet and
replayed by tests/test_replay.py. See recorder/ for the schema,
normalization, precision, capture (record.py), and replay code.
Two implementations of the same price-time-priority book, built with
-DCMAKE_BUILD_TYPE=Release on Apple M4 Max / macOS (Apple Silicon). OrderBook is the
correctness-first baseline (std::map levels + std::list FIFO); FlatOrderBook
is the cache-optimized version (flat tick-indexed level arrays + a pooled,
index-linked intrusive FIFO). Both pass the identical golden + differential-fuzzer
suite. These are single-threaded operation microbenchmarks, not production
wire latency (see the honesty note above).
| Operation | OrderBook (baseline) |
FlatOrderBook (optimized) |
Speedup |
|---|---|---|---|
| rest one limit order | 53.1 ns/op | 28.2 ns/op | 1.88× |
| cancel one order | 34.2 ns/op | 15.2 ns/op | 2.25× |
| clear a 10k-order book | 37.4 M orders/s | 60.6 M orders/s | 1.62× |
FlatOrderBooktrades generality for speed: it requires a bounded price range known at construction (realistic per instrument). Level access is O(1) and cache-friendly; the cursor rescan on best-level depletion is amortized O(1) for liquid books but degrades on pathologically sparse ones — a hierarchical-bitmap price index would remove that, and is possible future work.
L2Book reconstructs a Kraken depth-10 book from the WS book feed and validates
every message against Kraken's CRC-32 checksum (verified against Kraken's published
example). Release build, Apple M4 Max / macOS (Apple Silicon):
| Operation | Result |
|---|---|
| apply one L2 update (depth-10 book) | 21.0 ns/op |
| compute Kraken CRC-32 (depth-10 book) | 962.5 ns/op |
An event-driven market-making simulator (Simulator::run in
engine/include/quotient/simulator.hpp / engine/src/simulator.cpp) replays a captured
Kraken L2 feed (MarketEvents) through a priority-queue event loop that merges market
updates with our own order actions under a configurable latency budget
(SimConfig::latency_ns — the delay between a strategy's decision and its action landing
on the book). Ordering is deterministic by (time, seq) — no RNG.
A Strategy (on_book_update / on_fill → place/cancel Actions) drives the sim; M3
ships one trivial reference strategy, FixedSpreadStrategy, which quotes a fixed
half-spread around the mid. The Python driver (recorder/sim_driver.py) wires a recorded
Parquet capture straight into the C++ Simulator — running FixedSpreadStrategy over the
real 991-message Kraken sample (data/samples/btcusd_book_sample.parquet) produces 2
fills, inventory = 2, mtm_pnl = 235, deterministically, with inventory conserved across
the run (enforced by tests, not just asserted here).
Honest framing — the fill model is a documented approximation, not an exchange match:
- Passive fills consume our resting order's queue position as the displayed L2 depth at that price level decreases. Aggregated L2 diffs can't distinguish a trade from a cancel, so this is optimistic — it assumes all depth reduction at our level was a trade against us.
- Aggressive fills (a crossing
Place) walk the displayed book depth with slippage, but are non-impacting shadow fills — they don't remove liquidity from the replayed book, since the book only evolves from the recorded feed, never from our own orders.- Depth-window edge: the book is truncated to a fixed depth (top 10 by default), so a resting order at a price that scrolls outside the visible window reads as zero depth and would be treated as consumed. The shipped
FixedSpreadStrategyquotes near the mid, inside the window, so this doesn't arise here; a strategy resting deeper (or at a smaller depth) would need the passive model to guard the visible-band boundary.This is an honestly-documented L2-only proxy for fills, not a claim of exact exchange-match fidelity (that would require L3, order-level data).
Simulator throughput, Release build (-DCMAKE_BUILD_TYPE=Release), Apple M4 Max /
macOS (Apple Silicon), single-threaded microbenchmark (benchmarks/bench_simulator.cpp) —
not wire latency, see the honesty note at the top of this README:
| Benchmark | Result |
|---|---|
Simulator::run (FixedSpreadStrategy, synthetic 10k-event market) |
~14.9M events/s |
AvellanedaStoikovStrategy (engine/include/quotient/avellaneda_stoikov.hpp /
engine/src/avellaneda_stoikov.cpp) is the first real Strategy implementation, replacing
M3's trivial FixedSpreadStrategy with the classic Avellaneda–Stoikov (2008)
inventory-skewed quoting model. Given the current mid s, inventory q, and params
(σ, γ, τ, k), it computes a reservation price that shifts the quote away from the mid
in the direction that reduces inventory:
r = s − q·γ·σ²·τ
and an optimal half-spread (independent of inventory) trading off inventory risk against order-arrival intensity:
δ = ½·γ·σ²·τ + (1/γ)·ln(1 + γ/k)
with the final quotes bid = round(r − δ), ask = round(r + δ). The strategy re-quotes
(cancel + replace) whenever the target tick moves, and tracks its own inventory via
on_fill.
Honest framing:
- Only σ is calibrated from data.
recorder/calibrate.pyestimates σ as the population std-dev of successive integer-mid changes (ticks/message-step) over a reconstructed book from a real capture — σ = 1.3628 over the 991-message sample. γ, τ, k are model parameters, not calibrated — a full order-flow-intensity (k/A) fit needs trade-level data (Kraken'stradechannel, arrival rates by price offset), which the recorder does not yet capture. That's explicit M5 work, not silently assumed.- Float math, integer book.
randδare computed in double precision (the closed-form A-S formulas aren't naturally integer), then the finalbid/askare rounded to the nearest tick (std::llround) before touching the book — the order book, fills, and PnL all stay integer-only, per the M1 design.- Real sample vs. synthetic scenario. Run end-to-end (
recorder/sim_driver.py'srun_as_over_capture) over the real, checksum-validated 991-message Kraken sample with calibrated σ, the A-S maker produces 3 fills, inventory −1, cash 631791, mtm_pnl −135 — deterministic, but too short a sample (991 messages, one symbol) to see the inventory skew mechanism do meaningful work. The inventory-control behavior (quotes skewing away from the mid as inventory grows) is instead demonstrated with a synthetic scenario inengine/tests/test_avellaneda_stoikov.cpp. A rich PnL/adverse-selection study across longer captures and a latency sweep is M5, not this task.- Throughput is a microbenchmark, not wire latency. Same caveat as M3's simulator number above — single-threaded, no network/exchange round-trip modeled.
A-S maker throughput, Release build, Apple M4 Max, single-threaded microbenchmark
(benchmarks/bench_avellaneda_stoikov.cpp) — Simulator::run with
AvellanedaStoikovStrategy over the same synthetic 10k-event market as the M3 benchmark
above:
| Benchmark | Result |
|---|---|
Simulator::run (AvellanedaStoikovStrategy, synthetic 10k-event market) |
~3.5M events/s |
The A-S maker is slower than FixedSpreadStrategy (~14.9M events/s) because every book
update now does floating-point reservation-price/spread math (including a log) instead
of a fixed offset, plus more frequent cancel/replace churn from re-quoting on every tick
move — still a single-threaded microbenchmark, not a claim about production or
colocated-HFT latency.