Quant Memo
Advanced

Building a Matching Engine Simulator

A backtest that just checks whether the last price crossed your limit isn't simulating a market, it's simulating a coin flip. A matching engine simulator replays the actual rules an exchange uses to decide who trades with whom.

Prerequisites: Order Book Mechanics, Market vs. Limit Orders

A team builds a first-pass limit-order backtest: for every historical minute, check whether the traded price touched the strategy's limit price, and if so, mark the order filled. It's a natural first instinct and it's wrong in a way that isn't obvious until you compare it to reality — a fill in this model happens the instant the tape prints a matching price, regardless of whether the strategy's order was actually first in line to trade at that price. In one backtested session, the naive model fills a resting buy order the moment the market ticks down to its limit, at 9:47:03. Replayed against the actual exchange message log for that name, the real queue at that price level had 40,000 shares ahead of the strategy's order, and the price only traded 12,000 shares at that level before moving away — the order was never filled at all. The naive model invented a fill that never happened.

A matching engine simulator fixes this by implementing the actual rule an exchange uses to decide who trades: price-time priority. Orders are matched in strict order — best price first, and among orders at the same price, whoever arrived earliest trades first. A resting order doesn't fill just because the market touched its price; it fills only once every order ahead of it in the queue has either traded or been cancelled.

What the engine has to track

At minimum, a matching engine simulator maintains, for every price level on both sides of the book, an ordered list of resting orders by arrival time — not just a total size at that level, but the queue itself. When a new order arrives, the engine checks whether it crosses the best price on the opposite side; if so, it matches against the resting orders there, oldest first, until either the incoming order or the resting orders are exhausted. If it doesn't cross, it joins the back of the queue at its own price level, and its position in that queue — see Tracking Queue Position in a Simulator — is what will eventually determine whether and when it fills.

Every order also needs an explicit lifecycle — new, resting, partially filled, filled, cancelled — tracked consistently, because a matching engine is really a state machine: see The Order Lifecycle State Machine for how those transitions are usually modeled and where naive implementations create phantom fills or double-counted cancels.

Worked example: replaying a queue by hand

The book at $50.00 has three resting sell orders, in arrival order: A for 5,000 shares, B for 3,000, C for 8,000. A strategy's sell order, D, for 2,000 shares, joins behind them — the queue is now A(5,000), B(3,000), C(8,000), D(2,000), a total of 18,000 shares waiting at that price. A buy market order for 9,000 shares arrives. Under price-time priority it matches A in full (5,000, using up all of A's size), then B in full (3,000, all of B's size, running total 8,000), then 1,000 of C's 8,000, and stops — total 9,000 shares matched. D never trades. The naive touch-price model, by contrast, would have seen the trade print at $50.00 and marked D "filled," when in truth D is still resting with 16,000 shares of the original 18,000-share queue still ahead of it.

A 5,000 B 3,000 C 8,000 D 9,000-share buy order matches this far →
Price-time priority matches strictly front-to-back. The incoming order exhausts itself partway through C; D, at the back, gets nothing — a naive touch-price model would have filled it anyway.

A price touching your limit is necessary for a fill but nowhere near sufficient. Whether you actually trade depends on your position in the queue at that price, which a matching engine simulator has to track explicitly rather than infer from the tape.

Before trusting a matching engine implementation, replay one historical trading session against a known exchange message log and check that its own trade prints line up. Any systematic mismatch there means the priority logic has a bug, and every fill downstream of it is suspect.

Doing it properly

Build the queue as an explicit, ordered data structure per price level, not a running total, since the total alone can't answer "would I have traded." Feed the simulator real historical order and cancel messages where available, so queue depletion ahead of your own order is driven by actual market activity rather than an assumed cancellation rate. And treat your own simulated order as capable of changing the very book it trades against once you're modeling market impact at all — see Simulator Fidelity Versus Speed for the resulting tradeoff between this level of realism and how fast the simulator can run.

Handle order types beyond a plain resting limit order deliberately rather than as an afterthought — iceberg orders that only display part of their size, stop orders that convert to market orders once triggered, and pegged orders that reprice automatically all interact with price-time priority differently, and a matching engine that only implements the simple case will misprice queue position for any market that uses the others. Also decide explicitly how the engine handles orders that arrive at the exact same timestamp, since real exchanges break that tie with a defined rule — often sequence number of arrival at the matching engine itself — and a simulator that resolves ties arbitrarily can hand a simulated order a queue position it would never have earned live.

Related concepts

Practice in interviews

Further reading

  • Harris, Trading and Exchanges
  • Cartea, Jaimungal & Penalva, Algorithmic and High-Frequency Trading
ShareTwitterLinkedIn