Discrete-Event Scheduling in Backtest Engines
Under the hood, a serious backtest engine isn't a loop over rows of a spreadsheet — it's a discrete-event simulation, where a priority queue keyed on timestamp decides exactly which of many pending events happens next, including events the simulation itself schedules for the future.
Prerequisites: The Simulation Clock and Event Loop
A basic backtest can get away with looping through bars in order and calling it a day. But once a simulation needs to model things that don't happen at a market data timestamp — an order that fills 3 milliseconds after it's sent, a timer that cancels an unfilled quote after 500ms, a stop-loss that should trigger the instant a price is crossed rather than only at bar close — a simple loop has no natural place to put them. Discrete-event scheduling is the general-purpose fix: represent everything that happens as an event with a timestamp, hold all pending events in a single structure ordered by time, and always process whichever one is soonest, even if that event was scheduled by another event that hasn't happened yet.
The mechanism: a priority queue of future events
The core data structure is a priority queue (typically a min-heap) keyed on event time. At any point, the simulation clock sits at the time of the last processed event, and the queue holds every event known to occur after that — some drawn directly from historical market data, others created dynamically by the simulation itself, like "this order times out at " scheduled the moment the order is sent. Processing an event can push new events onto the queue with times later than the current clock but earlier than events already queued, and the engine handles that correctly simply because the queue always serves the earliest timestamp, recomputed fresh each time, not a fixed iteration order decided in advance.
Worked example: a self-scheduling cancel timer
An order is sent at with a 500ms time-in-force. The engine doesn't need a separate "check for timeouts" loop running on every tick — it just schedules a cancel event at the moment the order is created. Suppose the queue at that instant holds:
| Time | Event |
|---|---|
| 100.120 | quote update |
| 100.310 | quote update |
| 100.500 | order timeout (cancel) — scheduled dynamically |
| 100.510 | quote update |
The engine processes these strictly in timestamp order — two quote updates, then the cancel, then the next quote — regardless of the fact that the cancel event didn't exist in the data feed at all and was created only 100.000s into the run. If, before 100.500s, a quote update causes the order to fill, the fill handler simply removes the now-obsolete cancel event from the queue; the scheduling structure doesn't care that an event was "invalidated," only that the queue is kept consistent with reality as new information arrives.
What this means in practice
Discrete-event scheduling is what lets a backtest engine model timers, order lifecycles, and latency without bolting on special-case logic for each — anything that "happens later" is just another entry in the same queue. It also composes: adding a new kind of delayed effect (an exchange acknowledgment lag, a queue-position decay check) never requires restructuring the main loop, only scheduling one more event type into the existing queue.
Discrete-event scheduling processes every occurrence in a simulation — market data, fills, timeouts — through a single priority queue ordered by timestamp, including events the simulation schedules for its own future. This is what lets a backtest correctly model things that don't happen exactly on a market-data tick, like order timeouts and latency delays.
Related concepts
Practice in interviews
Further reading
- Law, Simulation Modeling and Analysis, ch. 1