A from-scratch electronic exchange core, in modern C++23: order entry, a price/time-priority limit order book, FIFO matching, pre-trade risk controls, a binary market-data feed, and the plumbing (lock-free ring buffers, a slab allocator, a wire protocol) that makes it all run without allocating or locking on the hot path.
Built to be measured, not asserted: every number below came from bench/
running on a pinned core, reproducible via tools/. See
docs/tradeoffs.md for why each design decision was
made, with comparative data, not opinion.
A simulated exchange with a real price/time-priority matching engine, deterministic replay, and measured (not claimed) latency is core market-infrastructure tooling — trading firms, exchanges, and vendors all need exactly this kind of harness to test strategies and infrastructure before touching a real venue. But the underlying systems-engineering discipline transfers well beyond finance: building a deterministic simulation with a measured, realistic latency/throughput profile, and treating every performance and correctness claim as something to benchmark and test rather than assert, is exactly what's needed for any tech domain that requires a trustworthy test harness for a latency-sensitive distributed system — a matching engine is, structurally, just a demanding single-writer state machine fed by concurrent producers, a shape that recurs constantly outside trading. The same discipline applies directly to industrial control-system testing too: simulating realistic network and latency conditions to validate SCADA/ICS software under load is the same class of problem — a deterministic, measurable stand-in for a real, hard-to-instrument production environment.
Frame 151/300 (order #3,020) of a real seeded session — every number on screen is
actual jane::matching::MatchingEngine output, nothing drawn for effect.
This is a static capture of
artifacts/book_animation.html, a
self-contained, interactive replay: download it and open it in a browser
(no server, no build needed) to scrub through all 300 frames, hover the
depth chart for tooltips, and watch the trade tape fill in. It's produced
by apps/bookview, which drives the real
pipeline — ReplayEngine over MatchingEngine + RiskEngine +
MarketDataPublisher — through seeded synthetic order flow and can also
render the same book live as an ANSI terminal ladder:
./build/apps/bookview --seed=2026 --orders=6000 --interval=20 --depth=10flowchart TD
C0["Client 0"] --> Q0["SPSC queue"]
C1["Client 1"] --> Q1["SPSC queue"]
CN["Client N"] --> QN["SPSC queue"]
Q0 --> SEQ["FanInSequencer, round-robin drain"]
Q1 --> SEQ
QN --> SEQ
SEQ --> RISK["RiskEngine.check_new_order"]
RISK -->|reject| REJ["ExecutionReport: Rejected"]
RISK -->|accept| MATCH["MatchingEngine.submit, FIFO match then rest remainder"]
MATCH --> FILLS["Fills"]
FILLS --> RECORD["RiskEngine.record_fill, both sides"]
FILLS --> MD["MarketDataPublisher, trade plus level deltas"]
MATCH --> EXR["ExecutionReport: New, PartialFill, or Fill"]
MD --> SINK["Sink: InMemorySink, FileSink, or socket"]
REJ --> SINK
EXR --> SINK
Everything from the sequencer's drain onward runs on one thread —
MatchingEngine, OrderBook, and RiskEngine contain no locks or atomics
anywhere in their implementation. Concurrency is deliberately pushed to the
ingestion edge (one wait-free SPSCRingBuffer per client, fanned in by
round-robin) specifically so the matching core's correctness invariants only
ever have to be proven against sequential input, not concurrent mutation.
Full reasoning, the alternative MPSC/CAS ingestion design, and a
message-by-message trace of one order through the whole pipeline:
docs/architecture.md.
- Limit order book: price/time priority, FIFO per price level
- Order types: limit, market, cancel, replace
- FIFO matching with partial fills
- Market data: full snapshots + incremental deltas
- Deterministic replay (historical log or seeded synthetic flow) for bug repro
- Pre-trade risk: max order size, max position, max loss, kill switch
- Strategy simulators: market maker, VWAP, arbitrage
- Latency benchmarks: p50/p99/p99.9 per operation, measured not claimed
- Correctness suite: volume conservation, strict FIFO, no crossed/impossible trades
Every box above is backed by tests, not just present in source.
Machine: AMD Ryzen 9 5900X (12C/24T), Ubuntu, GCC 15.2, one physical core
pinned, -O3 -march=native, LTO, no sanitizers. Full methodology (why two
different measurement techniques are used, and for what) and every
comparative trade-off benchmark: docs/benchmarks.md.
Full pipeline, per order — sequencer → pre-trade risk → matching → market-data publication, one call per sample, 500,000 samples of realistic synthetic flow:
| Percentile | Latency |
|---|---|
| p50 | 110 ns |
| p99 | 1.69 µs |
| p99.9 | 4.06 µs |
| max (of 500,000 samples) | 179 µs |
Component level (Google Benchmark aggregate mean):
| Operation | Latency |
|---|---|
| Order book add + cancel round trip | 25 ns |
Order book best_bid() query |
6.7 ns |
| Matching engine: non-crossing submit + cancel | 40 ns |
| Matching engine: crossing submit (1 fill) | 288–530 ns |
| Matching engine: sweep 10 resting orders | ~1.1–1.3 µs |
Protocol encode / decode (NewOrderMessage) |
0.87 ns / 7.3 ns |
| SPSC ring buffer, single-threaded round trip | 1.4 ns |
| SlabPool alloc + dealloc (immediate cycle) | 1.1 ns |
One comparative result worth calling out here: the multi-producer ingestion
path fanning N SPSCRingBuffers into one sequencer beats a single
CAS-based MPSC ring buffer at every producer count tested, reaching
22.2M items/s at 8 producers vs. 6.7M items/s for the shared-buffer
design — the concrete payoff for pushing concurrency to the edge described
in Architecture above. Every other head-to-head (SPSC cached-vs-naive index,
price-index ladder-vs-tree-vs-vector, SlabPool-vs-pmr-vs-new/delete) is
in docs/tradeoffs.md, with the reasoning, not just the
numbers.
Reproduce all of it:
./tools/run_benchmarks.sh # builds Release, pins to an isolated core, writes artifacts/*.json
python3 tools/analyze_benchmarks.py artifacts/*.json # human-readable summaryA benchmark number is worthless if the thing being measured is wrong.
Every property below is checked by an automated test — not just claimed —
and this project's one real data-corruption bug was caught by the
property-based session test, not by any example-based unit test. Full
write-up, with the exact test for every claim:
docs/correctness.md.
| Property | How it's checked |
|---|---|
| Conservation of volume — a price level's cached aggregate quantity always equals what its FIFO list actually contains | An independently-walked structural check after every action in an 8,000-iteration randomized session, cross-checked against a fully separate per-order ledger the test maintains itself |
| Strict FIFO (price/time priority) — orders at a level match in arrival order, unmoved by partial fills | Structural tests on the intrusive linked list, plus end-to-end tests asserting fill order and FIFO position surviving a partial fill |
No crossed or impossible trades — the book never shows best_bid >= best_ask; every fill executes at the resting order's price (price improvement only, never degradation) |
Checked on every step of the same 8,000-iteration property test; MatchingEngine::replace() specifically tested against the one realistic way to rest a crossed book |
| Fill-or-Kill is genuinely all-or-nothing | Read-only pre-trade liquidity check before any mutation; tested at, above, and below the exact liquidity boundary |
| Pools never silently misbehave under exhaustion | Typed error, never corruption; 1,000-round randomized allocate/deallocate cycle |
| Lock-free structures are actually race-free | Verified under ThreadSanitizer, not just reasoned about |
174 test cases, 10,584,859 assertions — clean under both a default build
and ASan/UBSan/-Werror, plus a dedicated TSan pass over the concurrency
primitives.
include/jane/ public headers (the engine is largely header-only templates)
core/ strong types: Price, Quantity, OrderId, Sequence, ClientId, SymbolId, Nanos, PnL, Order
concurrency/ SPSC/MPSC ring buffers, multi-client fan-in sequencer
memory/ slab allocator / pmr pools
protocol/ binary wire messages, encode/decode
book/ limit order book (ladder / flat-vector / std::map backends)
matching/ matching engine
risk/ pre-trade risk gate
marketdata/ snapshot + incremental feed publisher
replay/ replay engine + synthetic order-flow generator
strategy/ market maker / VWAP / arbitrage simulators
metrics/ latency histograms, counters
correctness/ structural invariant checker (volume, FIFO, no crossed book)
src/ non-template implementation
apps/ bookview: live terminal order-book viewer + JSON session recorder
tests/ Catch2 unit + property/invariant tests
bench/ Google Benchmark micro + end-to-end harness
tools/ benchmark runner (shell) + results analyzer (Python)
artifacts/ benchmark results (JSON) and the recorded-session animation (HTML)
docs/ architecture, benchmarks, trade-offs, correctness write-ups
Dependencies: a C++23 compiler (developed against GCC 15), CMake >= 3.25, Ninja, Catch2 3, Google Benchmark.
sudo apt-get install -y ninja-build catch2 libbenchmark-dev
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build -j"$(nproc)"
ctest --test-dir build --output-on-failureUseful CMake options (see root CMakeLists.txt):
| Option | Default | Purpose |
|---|---|---|
JANE_NATIVE_ARCH |
ON |
-march=native; disable for portable binaries / CI |
JANE_ENABLE_LTO |
ON |
interprocedural optimization on Release-like configs |
JANE_ENABLE_ASAN |
OFF |
Address+UB sanitizers (Debug) |
JANE_ENABLE_TSAN |
OFF |
ThreadSanitizer (Debug, exclusive with ASan) |
JANE_WERROR |
OFF |
warnings as errors |
MIT — see LICENSE.
