~/writing
Project BreakdownsSeries: Backtesting Engine

Building a Backtester From Scratch

A walkthrough of designing an event-driven backtesting engine, and the traps of vectorized shortcuts.

I’ve rebuilt a backtesting engine three times now, and each rebuild was motivated by the same realization: the fast, vectorized version I started with was lying to me in ways I couldn’t see until real money was on the line.

Why vectorized backtests are seductive and dangerous

A vectorized backtest — compute signals for the whole dataframe at once, shift by one bar, multiply by returns — is fast to write and fast to run. It’s also very easy to accidentally use information from the future, because nothing forces you to respect the actual sequence of “signal arrives, then order is placed, then fill happens, then next bar.” Order of operations bugs hide extremely well inside a .shift(-1) typo.

The event-driven alternative

An event-driven backtester processes one event at a time — a new bar, a fill confirmation, a signal — through the same code paths a live trading system would use. It’s slower, but the slowness buys something important: there’s no way for the strategy to see data it shouldn’t have, because the engine literally hasn’t handed it to the strategy object yet.

Core pieces:

  • An event queue ordering market data, signals, orders, and fills strictly by time.
  • A strategy interface that only ever receives a on_bar(event) style callback — it has no way to reach into “future” rows of a dataframe.
  • A simulated broker that models fills with realistic slippage and latency assumptions, not instant fills at the closing price.

The tradeoff that matters

Yes, this is slower than a vectorized approach, and yes, most strategy research happens in a fast vectorized loop first. But the event-driven engine is the one that gets the final answer before anything goes live, specifically because it’s structurally incapable of the lookahead bugs that make a vectorized backtest lie to you.