Designing an Event-Driven Backtesting Engine
Anurag Singh Kushwah·Apr 28, 2024·15 min read
Vectorized backtests are fine for research sketches. But if you intend to take a strategy live, the simulation has to mirror the way a live system actually experiences the market: one event at a time, with no knowledge of the future. This is the architecture I landed on after several rewrites.
The core loop
Everything is an event: a market data update, a signal, an order, a fill, a timer. A single priority queue orders them by timestamp, and the engine drains it in a loop. Components never call each other directly — they publish events and subscribe to the ones they care about.
while not queue.empty():
event = queue.get()
for handler in handlers[event.type]:
handler(event) # may enqueue new events
This buys you two things. Determinism: the same input stream always produces the same output. And portability: the strategy code that runs against historical events is byte-for-byte the code that runs live, with only the data feed and broker adapters swapped.
The five components that matter
- Data handler — replays ticks or candles as market events, strictly point-in-time.
- Strategy — consumes market events, emits signal events. Pure logic, no I/O.
- Portfolio — turns signals into sized orders, tracks positions, P&L, and exposure.
- Execution simulator — models latency, queues, partial fills, and slippage; emits fill events.
- Results — consumes fills and equity snapshots, produces statistics at the end.
Decisions that saved me later
Model latency from day one. Adding a delay parameter after the fact touches every component. Building it in from the start is nearly free.
Keep the strategy interface tiny. Mine is two methods: on_market_event and on_fill. Everything else — sizing, risk, routing — lives outside the strategy where it can be tested independently.
Persist every event. An append-only event log turns debugging from guesswork into replay. When live behavior diverges from the backtest, you diff the event streams and the discrepancy points at the guilty component.
What I'd skip
Distributed execution, GPU acceleration, config DSLs — none of it mattered. A clean single-process engine in Python comfortably replays years of minute data. Spend the complexity budget on fill realism and data hygiene. That's where backtests are won or lost.