Basic Prerequisites: 01 · From Zero (why the vectorized backtest lies). Object-oriented Python.
1. Intuition & Practical Objective
An event-driven engine is five components that talk to each other only by placing events on a queue. That constraint - no direct calls, all communication through the queue - is what makes the simulation honest: a component can only ever react to an event whose timestamp has already arrived, so it is structurally incapable of seeing the future. The architecture is not decoration; it is the mechanism that enforces causality.
The decomposition is the one popularised by QuantStart and mirrored (with far more machinery) by production engines like NautilusTrader:
| Component | Consumes | Produces | Responsibility |
|---|---|---|---|
| Event queue | - | - | Total order on events; the single source of “now” |
| DataHandler | external bars/ticks | MARKET | Replays historical data in time order; the only thing allowed to know the data file |
| Strategy | MARKET | SIGNAL | Turns prices into a desired position; knows nothing about cash or brokers |
| Portfolio | SIGNAL, FILL | ORDER | Position/cash bookkeeping; turns desired position into the order delta |
| ExecutionHandler | ORDER, MARKET | FILL | Simulates the broker: latency, fill condition, capacity, cost |
The objective is to internalise one sentence: each arrow in that table is a type-checked contract, and every look-ahead bug is a violation of an arrow (usually Strategy reaching into the market data it was not given).
2. Mathematical Ground Truth & Derivations
The event type. An event is a 4-tuple with a total order defined on it:
where is a monotonically increasing arrival counter. The third key exists for one reason: determinism. If two events share a timestamp and a priority, something must break the tie, and it must break it the same way on every run - otherwise your backtest is not reproducible and your results are not a fact.
The priority lattice. The four kinds are ranked so that a market update is always processed before the orders it triggers, and orders before the fills they cause:
This ordering is what makes the trace below legible: at any timestamp , all market data for is handled first, then the strategy’s reaction, then the portfolio’s order, then fills.
The loop invariant. The single most important property of the whole engine:
Every component contract exists to preserve this invariant. Break it once - let Strategy peek at bars[t+1], let the ExecutionHandler fill at the mid of the bar that generated the order - and the entire backtest becomes fiction.
The component signatures (the arrows of the table, as functions):
Note that the strategy emits a target position , never an order. The mapping from target to order (the delta, and the netting of working orders) belongs to the Portfolio - which is exactly why the naive engine in 03 double-submits if you forget it.
3. Computational Implementation - the five components, traced
A complete mini-engine over 8 bars with all five components wired through one queue. The log is the point: watch a single signal travel Strategy → Portfolio → Execution → Fill, and watch the price move against it while the order waits out its latency.
Four things the trace proves, each a first principle:
- Causality holds. The order is accepted at and fills at . It never fills at .
- Latency costs money. The signal fired at mid ; the fill happened at - the price ran bps away while the order waited. That is the execution shortfall of the hub lookup, appearing spontaneously.
- The
workingcounter matters. The Portfolio nets in-flight quantity out of the target (d = target − (pos + in-flight)) so it never re-submits size for an order that is already live. In this trace the strategy emits only on change, so dropping the term changes nothing visible (the SELL is simply skipped,d = 0); but an engine whose strategy re-emits its target every bar - the common case - would stack a fresh order on top of each unfilled one, multiplying the intended position. Nettingworkingis what makes the engine idempotent to repeated signals. - The SELL order at is stuck in flight. Activates at , data ends at , so the position never closes. A real engine must decide what to do with unfilled orders at the last bar - a detail that silently changes reported P&L.
4. Failure Modes & First-Principles Breakdowns
- Strategy reaching into the market data. If
Strategyis handed the wholebarsarray “for convenience”, it can index forward and read the future. The architecture’s value is precisely that it doesn’t get the array. - Shared mutable state between components. A
Portfoliothat mutatesStrategy’s history, or a globalposition, breaks the queue’s guarantee that only the component receiving an event reacts to it. Keep one writer per piece of state. - Missing the in-flight netting. Emitting a target position without subtracting working orders duplicates every order whose fill has not yet returned (trace point 3).
- The unresolved end-of-data order. Live order books have defined terminal states (filled / cancelled / expired); a backtest that just stops leaves phantom orders and mis-states final inventory (trace point 4). See FIX & Exchange Connectivity for the real state machine.
- Deliberately bypassing the queue for speed. Some engines shortcut market → strategy. It is faster and it is how look-ahead re-enters: the shortcut skips the timestamp check. If you need the speed, batch within a timestamp, never across one.
5. References
- Halls-Moore, Michael, *QuantStart
- NautilusTrader Docs (nautilustrader.io)
- Hilpisch, Yves, Python for Algorithmic Trading (O’Reilly, 2020)
- Backtrader Docs (backtrader.com)
6. Connected Graph Bridges
- Back: 01 · From Zero · Index Hub
- Continue: 03 · The Event Loop - the scheduler that drives these components
- Execution model: Execution, Backtesting & Simulation · Queue Position & Fill Probability
- Storage: Tick-Level Databases & Time-Series - what sits behind the DataHandler