Event-Driven vs Vectorized Backtesting
The way you structure a backtest determines which lies it lets you tell yourself. A vectorized backtest computes signals and returns across an entire price array at once — blazing fast, perfect for research sweeps, and dangerously easy to contaminate with look-ahead bias. An event-driven backtest processes one event at a time, mirroring how a live system actually receives data and sends orders — slower, more code, but far harder to cheat in and a natural bridge to production. Knowing when to use which, and how to keep the fast one honest, is core craft. This article contrasts the two architectures, dissects the look-ahead traps in vectorized code, lays out the components of an event-driven engine, and gives a Python skeleton.
Vectorized backtesting
In a vectorized backtest you hold the whole history in arrays (typically pandas/numpy) and express the strategy as array operations. A moving-average crossover becomes a few lines:
import numpy as np
import pandas as pd
def vectorized_ma_crossover(prices, fast=20, slow=50):
fast_ma = prices.rolling(fast).mean()
slow_ma = prices.rolling(slow).mean()
signal = (fast_ma > slow_ma).astype(int)
# CRITICAL: shift signal by 1 bar — you trade on the NEXT bar's open,
# using only information available at signal time.
position = signal.shift(1).fillna(0)
returns = prices.pct_change()
strat_returns = position * returns
return strat_returns
This runs over decades of data in milliseconds, which is why it dominates the research phase — parameter sweeps, walk-forward optimization, and quick idea triage. The cost is realism: positions, fills, and costs are approximations applied uniformly.
The look-ahead traps
The single most dangerous bug in vectorized code is using information you would not have had at decision time. The most common forms:
- No signal lag. Computing
position = signal(notsignal.shift(1)) trades on the same bar that produced the signal — you act on a close before it has happened. - Full-sample statistics. Normalizing with the full-sample mean/std, or fitting parameters on all data, embeds the future into the past. This is a classic backtesting bias.
- Survivorship and restated data. Using today's index constituents or revised fundamentals as if they were known historically.
- Unrealistic fills. Assuming you trade at the exact close at zero cost ignores transaction costs and slippage, which can erase a high-turnover edge.
Vectorized code makes these mistakes invisible because nothing forces causality — the array just computes. You have to impose discipline manually (the .shift(1) above is the canonical guardrail).
Event-driven backtesting
An event-driven engine flips the model: it walks forward through time, emitting and consuming events in a loop. Nothing in the future is reachable because future bars simply have not been produced yet. The same engine that replays historical bars can, by swapping the data source, consume a live feed — which is why event-driven backtests are the natural precursor to a trading bot.
| Dimension | Vectorized | Event-driven |
|---|---|---|
| Speed | Very fast | Slower (per-event loop) |
| Look-ahead risk | High (manual guards) | Low (causal by design) |
| Realism (fills, partials, latency) | Crude | High |
| Code complexity | Low | Higher |
| Best for | Research, idea triage | Validation, pre-production |
| Path to live | Indirect | Direct (swap data handler) |
Components of an event-driven engine
A clean engine separates concerns into a handful of cooperating pieces communicating through an event queue:
- Data handler — emits
MarketEvents bar by bar (or tick by tick) from history or a live feed. - Strategy — consumes market events, computes signals, emits
SignalEvents. - Portfolio — converts signals into
OrderEvents with sizing and risk management, and tracks positions and equity. - Execution handler — turns orders into
FillEvents, modeling slippage, commission, and partial fills.
Events flow Market → Signal → Order → Fill, and the loop repeats for the next bar. Because each stage only sees events already on the queue, causality is enforced structurally.
from collections import deque
from dataclasses import dataclass
@dataclass
class Event:
type: str # 'MARKET' | 'SIGNAL' | 'ORDER' | 'FILL'
payload: dict
class Backtester:
def __init__(self, data_handler, strategy, portfolio, execution):
self.events = deque()
self.data = data_handler
self.strategy = strategy
self.portfolio = portfolio
self.execution = execution
def run(self):
while self.data.has_more():
self.events.append(self.data.next_market_event()) # advance time
while self.events:
ev = self.events.popleft()
if ev.type == 'MARKET':
self.portfolio.mark_to_market(ev)
sig = self.strategy.on_market(ev)
if sig: self.events.append(sig)
elif ev.type == 'SIGNAL':
order = self.portfolio.on_signal(ev)
if order: self.events.append(order)
elif ev.type == 'ORDER':
fill = self.execution.execute(ev) # models slippage/cost
if fill: self.events.append(fill)
elif ev.type == 'FILL':
self.portfolio.on_fill(ev)
return self.portfolio.equity_curve()
This skeleton is deliberately minimal, but the structure scales: add a clock, multiple symbols, order types, and a latency model without changing the core loop. Mature Python backtesting frameworks such as backtrader and zipline are elaborations of exactly this pattern.
Modeling fills and costs realistically
The execution handler is where an event-driven engine earns its keep, because it is the one place you can model the friction a vectorized backtest waves away. A credible execution model accounts for several effects:
- Slippage. Market orders rarely fill at the last printed price. A simple model adds a fixed number of basis points; a better one scales slippage with order size relative to available liquidity, echoing the market impact ideas from optimal execution.
- Commission. Per-share, per-contract, or percentage fees, plus any exchange and clearing costs, applied on every fill.
- Partial fills. Large orders may fill across several bars; the execution handler can emit multiple
FillEvents for oneOrderEvent, forcing the portfolio to track remaining quantity. - Latency. A delay between signal and fill, so the price you trade at is the one prevailing after your decision, not at the instant of it.
Because the engine processes events in order, layering these effects in is natural — each fill simply carries a worse price and a cost, and the portfolio's equity curve reflects reality rather than an idealized fantasy.
A hybrid workflow
The pragmatic answer is to use both, in sequence:
- Prototype vectorized. Triage hundreds of ideas and parameter regions quickly.
- Re-implement survivors event-driven. Confirm the edge holds under realistic fills, costs, and strict causality.
- Reconcile the two. If the event-driven result is dramatically worse, you have probably found a look-ahead bug or a cost you ignored — investigate before trusting either. See how to build a backtest in Python for an end-to-end example.
Common mistakes
- Forgetting the signal lag in vectorized code — the number-one source of phantom alpha.
- Trusting a vectorized result alone for a high-turnover or intraday strategy where fill assumptions dominate.
- Over-engineering the engine before the idea has shown any edge; start vectorized.
- Inconsistent cost models between the two backtests, so they can never be reconciled.
- Hidden state in the strategy object that leaks across the event loop (e.g. caching future-dependent values).
Key takeaways
- Vectorized backtests are fast and ideal for research but make look-ahead bias invisible — always lag signals and avoid full-sample statistics.
- Event-driven backtests process one event at a time, enforce causality structurally, and model fills realistically.
- An event-driven engine's data handler, strategy, portfolio, and execution components map directly onto a live system.
- Use vectorized backtests to triage and event-driven backtests to validate; large discrepancies signal a bug or an ignored cost.
- The event-driven architecture is the shortest path from research to a real trading bot.
