Quant Memo
Core

Event-Driven Backtest Architecture

A backtest built as a loop over timestamped events, where the strategy only ever sees what had already arrived. It is slower to write than a vectorised backtest and far harder to cheat with, which is the entire point.

Prerequisites: Backtest Design, Look-Ahead Bias

Most first backtests are three lines of pandas. Compute a signal column, multiply it by the return column, take the cumulative sum. It runs in a second and it produces a beautiful equity curve. It is also, almost always, wrong — not because the arithmetic is wrong, but because a whole dataframe sitting in memory has no notion of when each number became knowable. Every row can see every other row. An event-driven backtest is the architecture that removes that possibility by construction.

The idea: a strict script, read one line at a time

Think of a play where each actor is handed only their own next line, at the moment they need to say it. Nobody can read ahead. A vectorised backtest hands every actor the whole script at once and then politely asks them not to look forward. They always look forward, usually by accident.

An event-driven backtest replaces the dataframe with a queue of timestamped events, processed strictly in order. Four components take turns:

  • Data handler — releases the next market event (a bar, a quote, a trade, an earnings release) when its clock reaches that timestamp, and nothing after it.
  • Strategy — consumes market events, updates its state, and emits signal events.
  • Portfolio — turns signals into sized order events, checking cash, existing positions and risk limits.
  • Execution handler — turns orders into fill events at a modelled price, with cost and latency.

Fills go back to the portfolio, which updates positions and equity. Then the loop pops the next event. The strategy never holds a reference to the future because the future has not been created yet.

DATA STRATEGY PORTFOLIO EXECUTION bar signal order fill one pass = one timestamp, then the clock advances
Each arrow is an event with its own timestamp. Nothing is computed until its event is popped, so the strategy physically cannot read a price that has not been published yet.

Worked example: the same signal, two architectures

A daily momentum rule on 500 US large caps: go long the top decile by 20-day return, rebalance daily, equal weight. The vectorised version is one line — position = rank(ret_20d) > 0.9, then pnl = position * ret_today.

It reports 61% a year, volatility 15%, Sharpe 4.1, worst drawdown 6%. Nobody sane believes that, but it is easy to miss why. The ret_20d window ends at today's close, and ret_today is today's return. The rule is buying names that already went up today. It is not a strategy, it is the identity function.

Now the event-driven version, with no other change. The bar event for day tt is released after the close, the strategy emits a signal stamped tt, the portfolio sizes it, and the execution handler fills at the next event that could accept an order — the open of day t+1t+1. Same signal, same universe, same period: 4.2% a year, volatility 16%, Sharpe 0.26, and a 31% drawdown. Subtracting 8 bps a side of round-trip cost against roughly 180% annual turnover takes another 2.9 points off, and the strategy is flat to negative.

Nothing about the idea changed. All that changed was that the simulator refused to let the strategy act on a number at the instant it was computed.

A vectorised backtest asks you to promise not to use the future; an event-driven backtest makes it impossible. The Sharpe difference between the two on the same signal is usually not a rounding error — 4.1 to 0.26 in the example above is typical of one-bar leakage.

What the architecture buys you beyond honesty

  • Cash and inventory are real. The portfolio object knows you cannot buy 500 names with the proceeds of sales that have not settled, or short something you have no borrow on. Vectorised P&L quietly assumes infinite balance sheet.
  • Partial fills and rejects exist. The execution handler can fill 300 of your 1,000 shares and leave the rest working, which is what actually happens. See Order Fill Modeling in a Backtest.
  • Latency is expressible. A signal stamped 09:30:00.000 can produce an order that arrives at 09:30:00.004. In a dataframe there is nowhere to put four milliseconds.
  • The same strategy object runs live. Swap the historical data handler for a market data feed and the simulated execution handler for a broker API. Everything between them is unchanged, which removes an entire class of "it worked in research" bugs.

The most common leak is not exotic. It is executing at the same bar whose close produced the signal, and full-sample preprocessing — z-scoring a feature, fitting a scaler, winsorising outliers, or picking a universe using statistics computed over the whole history. Every one of those is a future read, and an event loop only protects you if the feature is computed inside the loop too.

Cheap audit without rewriting anything: shift your signal back by one extra bar and re-run. An honest strategy loses a little. A leaking one collapses. If one bar of extra lag destroys the edge, the edge was the lag.

The cost, and when to skip it

Event loops are slow — often 50 to 500 times slower than the vectorised equivalent, because you are running Python per event instead of C per column. That matters when you want to sweep 10,000 parameter combinations. The practical compromise most desks land on: explore with a vectorised harness that has been deliberately built with lagged inputs, then re-run every survivor through the event-driven simulator before anyone puts money on it. Any strategy whose numbers move materially between the two has a bug in the fast version, and the fast version is the one that was lying.

Related concepts

Practice in interviews

Further reading

  • López de Prado, Advances in Financial Machine Learning (Ch. 11)
  • Chan, Algorithmic Trading (Ch. 1–2)
  • Halls-Moore, Successful Algorithmic Trading
ShareTwitterLinkedIn