Limit Order Book & Matching Engine
A low-latency matching engine written from scratch in modern C++ using price-time priority. Includes a cache-conscious flat price ladder, order nodes from a custom pool allocator, cancels/modifies through an open-addressing hash map, and a lock-free SPSC ring buffer feeding a separate engine thread. The single-threaded core is compiled to WebAssembly and runs live below; performance numbers and benchmarks come from the native build.
Order Entry
Trade Tape
How Matches Work
Receive
An order arrives (limit, market, or IOC). In the native build, a feed-handler thread decodes it and provides it to the engine over a lock-free queue.
Match
An aggressive order walks the opposite side best-price-first, FIFO within each price, emitting trades until it is filled or the book can’t improve.
Remainder
Any unfilled remainder of a limit order joins the tail of its price level, taking a time-priority ticket behind everything already there.
Report
Fills go out as trades; cancels and modifies find their order in O(1) through the id → node map and unlink it.
Price-time priority
Better prices trade first, and at the same price, the order that arrived first trades first.
Book as a flat price ladder
Prices live on a discrete tick grid, so the book is one contiguous array indexed directly by price. Best bid/ask and any level are O(1) array lookups into cache-resident memory. The alternative, std::map<Price, Level>, is a red-black tree (O(log n)) whose every node is a separate heap allocation reached by pointer, which leads to a cache miss per level touched.
Flat ladder (this engine)
std::map<Price,Level>
Intrusive FIFO levels + pool allocator
Each price level is an intrusive doubly-linked list (the order carries its own prev/next, so a level needs no per-node container allocation). The nodes themselves come from a fixed pool (one contiguous slab, with memory handed out from a free list) so adds and cancels never call malloc on the hot path and neighboring orders stay close in memory.
Lock-free feed handler
One thread decodes the inbound feed and the other runs the engine. They interact via a single-producer / single-consumer ring buffer, using two atomic counters with acquire/release ordering. The head and tail sit on separate cache lines so the producer's writes don't invalidate the consumer's line. This part is native-only (browser WASM is single-threaded).
Native Benchmarks
Measured on the native build (the browser never runs these). Latency is per-message, single threaded; throughput and the lock-free comparison use the threaded feed handler.
Engineering Decisions
Data structure choices
- Flat price ladder over
std::map: a book clusters near the mid, so a direct-indexed array keeps hot levels in cache and makes best-price and level access O(1) and doesn't use pointer hopping - Pool allocator over
new/delete: the hot path allocates a node per add and frees one per fill/cancel; a slab + free list makes both O(1) and more cache-friendly - Open-addressing map over
std::unordered_map: cancels/modifies look up by id frequently, so a flat linear-probed table is useful to avoid a chained map's per-entry allocation and pointer hop - Lock-free SPSC over a mutex queue: exactly one producer and one consumer means the ingest needs only two atomics with acquire/release
Limitations
- Live demo runs single-threaded in WebAssembly (browser WASM has no shared-memory threads without special headers GitHub Pages can't set), so this project only uses the lock-free feed handler in the native benchmarks
- Order feed is synthetically generated rather than a real exchange protocol (ITCH/OUCH/FIX)
- Best-price search uses a short linear scan of the ladder rather than a bitset/SIMD index (fine while the book is concentrated, but not asymptotically ideal)