A high-performance exchange-style matching engine written in C++20. Supports add, cancel, modify, and execute operations with price-time priority (FIFO). Processes over 16 million mixed market events per second on Apple Silicon.
Built as a trading systems portfolio project, emphasizing the same engineering constraints found in production HFT systems: zero-allocation hot paths, O(1) cancel, cache-line-aware data layout, and ns-precision benchmarking.
Measured on Apple M1 Pro using mach_absolute_time() (~0.3 ns resolution).
| Operation | Mean | p50 | p99 | p999 | Throughput |
|---|---|---|---|---|---|
| Add (passive limit) | 55 ns | 41 ns | 84 ns | 1792 ns | 18 M ops/s |
| Cancel | 22 ns | <41 ns | 84 ns | 333 ns | 45 M ops/s |
| Match (single fill) | 10 ns | <41 ns | 42 ns | 42 ns | 103 M ops/s |
| Mixed workload | 61 ns | — | — | — | 16 M ops/s |
Mixed workload: 70% passive adds, 20% cancels, 10% aggressive orders — driven by a synthetic feed with geometric Brownian motion mid-price and Poisson arrivals.
MatchingEngine
├── OrderBook
│ ├── BookSide<std::greater> bids (highest price first)
│ └── BookSide<std::less> asks (lowest price first)
│ └── std::map<price, PriceLevel*>
│ └── PriceLevel
│ └── OrderList (Boost intrusive doubly-linked list, FIFO)
│ └── Order (128 bytes, one cache line)
├── MemoryPool<Order>
├── MemoryPool<PriceLevel>
└── std::unordered_map<order_id, Order*>
O(1) cancel via auto-unlink intrusive hook
Each Order carries a boost::intrusive::list_member_hook<auto_unlink> — a pair of prev/next pointers stored directly inside the struct. Cancelling an order requires only:
- Hash map lookup (O(1))
hook.unlink()— pointer surgery on the two adjacent nodes, no list reference needed- Pool free (O(1))
No iterator invalidation, no list traversal. The O(log N) case only occurs when the level itself becomes empty and must be removed from the map.
Cache-line-sized Order struct
Order is exactly 128 bytes — one M1 cache line. All fields accessed in the matching loop (price, quantity, hook) fit in the first half. Iterating a price level's FIFO queue loads one cache line per order.
Zero allocation on the hot path
Both Order and PriceLevel objects are allocated from slab-based MemoryPool<T> instances. Each pool pre-allocates 65536-object slabs and sub-allocates via a bump pointer; freed slots are recycled through an intrusive free list (the freed memory stores the next-free pointer). No malloc/free after warmup.
Branchless fill arithmetic
uint64_t fill_qty = qty < order->quantity ? qty : order->quantity;
// Compiler emits CSEL (ARM64 conditional select) — zero branches.Integer prices
All prices are int64_t ticks throughout. No floating-point arithmetic in the matching core — avoids NaN/inf edge cases and floating-point comparison bugs entirely.
Matches real exchange behavior (Nasdaq/NYSE):
- Quantity decrease, same price → O(1), FIFO priority preserved
- Price change or quantity increase → cancel + re-add, FIFO priority lost
quant_project/
├── include/
│ ├── memory_pool.hpp — slab allocator (bump pointer + intrusive free list)
│ ├── order.hpp — Order struct, Side/OrderType/OrderStatus enums
│ ├── price_level.hpp — PriceLevel + OrderList (Boost intrusive FIFO)
│ ├── order_book.hpp — BookSide<Comp> + OrderBook (cached best-price)
│ ├── matching_engine.hpp — MatchingEngine API, Trade struct
│ └── market_data.hpp — MarketDataGenerator (GBM + Poisson arrivals)
├── src/
│ ├── matching_engine.cpp — add/cancel/modify/match implementations
│ ├── market_data.cpp — synthetic feed generation
│ ├── benchmark.cpp — latency/throughput harness
│ └── main.cpp — 1M event simulation with book snapshot
├── CMakeLists.txt
└── Makefile
Requirements
- C++20 compiler (tested with Apple clang 17, GCC 14)
- Boost headers ≥ 1.74 (Boost.Intrusive is header-only — no linking needed)
- macOS:
brew install boost
- macOS:
# Using Make (no cmake required)
make all
# Produces:
# lob_main — 1M event replay with book snapshot
# lob_benchmark — latency/throughput measurementsmacOS note: The Makefile uses Xcode's clang++ directly. If you see a license prompt, run sudo xcodebuild -license accept once.
With CMake (if installed):
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)# Run the 1M event market simulation
./lob_main=== LOB Matching Engine — Market Simulation ===
Events processed: 1000000
ADD orders: 800021
CANCEL orders: 199979
Trades generated: 693613
Total qty filled: 3652256
Orders resting: 49773
Final order book (top 5 levels per side):
PRICE QTY ORDERS
ASK 7103 24 3
ASK 7102 25 3
...
--- spread: 83 tick(s) ---
BID 7011 41 15
...
Book invariant OK (best_bid < best_ask)
# Run the benchmark suite
./lob_benchmarkMarketDataGenerator produces a realistic order flow:
- Mid-price: geometric Brownian motion —
mid *= exp(σ · Z),σ = 0.05%per event - Arrivals: deterministic 2 µs interarrival (configurable to Poisson)
- Order mix: 70% passive limits, 20% cancels, 10% aggressive (crossing the spread)
- Passive placement: uniform offset [0, 20 ticks] behind best price
- Aggressive placement: priced at or through best opposing price
- Quantities: Poisson distributed, mean = 10 lots
All parameters are configurable via MarketDataParams:
MarketDataParams p;
p.sigma_per_event = 0.001; // higher volatility
p.prob_aggressive = 0.20; // more aggressive flow
p.mean_qty = 5;
MarketDataGenerator gen(p);
auto events = gen.generate(1'000'000);| Operation | Time | Notes |
|---|---|---|
| Add (passive) | O(log N) | Map insert for new price level; O(1) list push |
| Add (aggressive) | O(K log N) | K = levels consumed; O(1) per fill |
| Cancel | O(1) amortized | Hash lookup + intrusive unlink + pool free; O(log N) only if level empties |
| Modify (qty-down) | O(1) | Field update only; FIFO preserved |
| Modify (reprice) | O(log N) | Cancel + re-add |
| Best bid/ask | O(1) | Cached pointer; updated only on level add/remove |
N = number of distinct price levels.