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 (not signal.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.

DimensionVectorizedEvent-driven
SpeedVery fastSlower (per-event loop)
Look-ahead riskHigh (manual guards)Low (causal by design)
Realism (fills, partials, latency)CrudeHigh
Code complexityLowHigher
Best forResearch, idea triageValidation, pre-production
Path to liveIndirectDirect (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 one OrderEvent, 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:

  1. Prototype vectorized. Triage hundreds of ideas and parameter regions quickly.
  2. Re-implement survivors event-driven. Confirm the edge holds under realistic fills, costs, and strict causality.
  3. 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.
#event-driven backtesting #vectorized backtesting #backtest architecture #simulation #python