Skip to content

Latest commit

 

History

10 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Limit Order Book + Matching Engine

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.


Benchmark Results

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.


Architecture

Data structures

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*>

Key design decisions

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:

  1. Hash map lookup (O(1))
  2. hook.unlink() — pointer surgery on the two adjacent nodes, no list reference needed
  3. 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.

Modify semantics

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

Project structure

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

Building

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
# Using Make (no cmake required)
make all

# Produces:
#   lob_main        — 1M event replay with book snapshot
#   lob_benchmark   — latency/throughput measurements

macOS 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)

Running

# 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_benchmark

Synthetic market data

MarketDataGenerator 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);

Complexity summary

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages