An exchange-grade limit order book with price-time priority matching, built from scratch in modern C++ as a study of low-latency systems design. The project is developed in stages, each one introducing a technique used in real high-frequency trading systems and measuring its impact.
| Stage | Focus | State |
|---|---|---|
| 1 | Correct single-threaded matching engine + full test suite | ✅ done |
| 2 | Cache-friendly data structures + benchmarks (before/after) | ✅ done |
| 3 | Latency instrumentation (p50/p99/p99.9), throughput | planned |
| 4 | Market-data feed handler (binary protocol parsing) | planned |
| 5 | Lock-free SPSC queue between feed and matching threads | planned |
| 6 | Custom pool allocator, zero hot-path allocations | planned |
- Limit orders with price-time priority (best price first, FIFO within a price)
- Add / cancel / modify, with correct priority semantics on modify
- Multi-level sweeps, partial fills, and price improvement (trades execute at the resting/maker price)
- O(1) cancel/modify via an order-id index
- ~12 M messages/s add+cancel throughput, flat from 10 to 10,000 price levels
- 14 unit tests covering the matching semantics (GoogleTest)
Same benchmarks, same machine, same flags — only the book's internals changed. Full table and analysis in BENCHMARKS.md.
| Benchmark (10,000 price levels) | std::map + std::list |
flat ladder + arena |
|---|---|---|
Level lookup (quantity_at) |
250 ns | 6.7 ns |
| Add + cancel round trip | 713 ns | 164 ns |
| Sweep 64 levels | 18.5 µs | 8.1 µs |
The point isn't the multiplier, it's the flat line: Stage 1 got slower as the book got deeper, Stage 2 doesn't — add+cancel measures 171 / 168 / 186 / 164 ns across a 1000× range of book depth.
- Integer fixed-point prices, never
double— exact and fast to compare. - Flat price ladder. Price levels live in a
std::vector<Level>indexed byprice - base_, so finding a level is arithmetic, not a search. ALevelis 16 bytes — four per cache line. - Order arena with an intrusive freelist. Every resting order lives in one
std::vector<OrderNode>(32 bytes each); cancelled slots are recycled, so a steady-state book performs no allocation at all. Stage 1 did up to threemallocs per resting order, which the baseline showed was ~90% of its cost. - Links are indices, not pointers. A
std::vectorreallocates as it grows and every pointer into it dies; a 32-bit slot number survives. Levels hold head/tail slot numbers, and the id index mapsOrderId→ slot number. - Price band. The ladder covers a growing window of prices, capped at ~1M ticks; orders outside it are rejected, the same way a real venue's limit-up / limit-down bands reject them.
- The engine is a library; the demo app, tests, and benchmarks link the same code.
cmake -S . -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
./build/Release/order_book_test.exe # tests
./build/Release/demo.exe # demo scenario
./build/Release/order_book_bench.exe --benchmark_min_time=0.35s # benchmarksOn Linux/macOS, drop the -G/-A flags and use --config only if your
generator is multi-config.
include/ types.hpp, order_book.hpp — public API
src/ order_book.cpp, main.cpp — engine + demo
tests/ order_book_test.cpp — GoogleTest suite
benchmarks/ order_book_bench.cpp — Google Benchmark suite
BENCHMARKS.md — frozen before/after numbers