Caloogy Article
中文

Vectorized Backtesting vs. Event-Driven Backtesting

Date
Sep 1, 2025
Series
Quant
Reading
4 min
Topics
QuantQuant devBacktestVectorized

The one-sentence version

There are two fundamental ways to write a backtesting framework:

  • Vectorized: Treating the entire historical dataset as one large table and using array operations to calculate all signals, positions, and returns at once. It is fast to write and fast to run, making it suitable for rapid idea screening during the research phase;
  • Event-Driven: Simulating the passage of real time, advancing one event at a time—such as a single K-line (or a tick, or an order report). The strategy can only see the "past." It is slow to write and slow to run, but it is structurally impossible to look into the future, making it easier to simulate execution, costs, and risk control, and allowing the same strategy code to be used directly for live trading.

This article implements the same strategy using both methods, verifies that the results are identical, and then discusses when event-driven is mandatory.

Disclaimer

This article is for technical educational purposes only and does not constitute any investment advice. The code in this article uses simulated data.


Layer 1: The Intuition

1.1 Watching a Recording vs. Live Broadcast

Vectorized Event-Driven
Analogy Having a recording of the entire match, allowing you to fast-forward, rewind, or view the whole picture at once A live broadcast, where you can only see what has already happened
Risk Accidentally seeing "future plot twists" (look-ahead bias) Structurally unable to see the future
Speed Extremely fast: one line of array operations can process decades of data Slow: requires looping through events one by one
Realism Difficult to simulate complex execution logic Can simulate orders, partial fills, cancellations, and risk control

1.2 Position in the Research Workflow

Idea

Vectorized Backtesting
Rapidly screen hundreds of variants

Event-Driven Backtesting
Fine-grained simulation of execution and costs

Paper Trading
Same code connected to real-time data

Live Trading


Layer 2: Two Implementations

2.1 Vectorized

import numpy as np, pandas as pd
rng = np.random.default_rng(11)
n = 500
price = pd.Series(100 * np.exp(np.cumsum(rng.normal(0.0003, 0.012, n))))

def vectorized(price, fast=10, slow=30):
    sig = (price.rolling(fast).mean() > price.rolling(slow).mean()).astype(float)
    pos = sig.shift(1).fillna(0)                 # Signal from yesterday's close, hold today
    return (pos * price.pct_change().fillna(0)).add(1).cumprod()

The entire backtest is only three lines. The key is shift(1): forgetting this leads to look-ahead bias, one of the seven deadly sins of backtesting.

2.2 Event-Driven

class Strategy:
    def __init__(self, fast=10, slow=30):
        self.fast, self.slow, self.hist = fast, slow, []
    def on_bar(self, close):
        self.hist.append(close)
        if len(self.hist) < self.slow:
            return 0.0
        f = np.mean(self.hist[-self.fast:]); s = np.mean(self.hist[-self.slow:])
        return 1.0 if f > s else 0.0              # target position

class Broker:
    def __init__(self):
        self.pos, self.equity, self.last = 0.0, 1.0, None
    def on_price(self, close):
        if self.last is not None:
            self.equity *= 1 + self.pos * (close / self.last - 1)
        self.last = close
    def set_target(self, target):
        self.pos = target                          # simplified: execute immediately at close, zero cost

def event_driven(price):
    strat, broker, curve = Strategy(), Broker(), []
    for close in price:                            # iterate bar by bar
        broker.on_price(close)                     # settle today's PnL using old position
        broker.set_target(strat.on_bar(close))     # calculate tomorrow's position based on today's close
        curve.append(broker.equity)
    return pd.Series(curve)

v, e = vectorized(price), event_driven(price)
print("Vectorized final value:", round(v.iloc[-1], 6), " Event-driven final value:", round(e.iloc[-1], 6),
      " Max difference:", f"{(v - e).abs().max():.2e}")

Output:

Vectorized final value: 1.102555  Event-driven final value: 1.102555  Max difference: 0.00e+00

The results of the two methods are exactly identical. Note that in the event-driven version, on_price (settlement) must occur before set_target (order placement): this sequence is the structural guarantee against "looking into the future," and the strategy object Strategy only stores past prices.

A practical tip: Implementing the same strategy using two independent methods and comparing the results is an excellent way to discover backtesting bugs (see the "independent implementation" approach in Cross-Validation via Multiple Sources).


Layer 3: When to Use Event-Driven Architecture

3.1 Scenarios where Vectorization is difficult to handle

Scenario Reason
Path-dependent logic Stop-loss, take-profit, trailing stop-loss: today's position depends on the highest price since holding, requiring step-by-step calculation
Capital and position constraints Partial orders when available capital is insufficient; margin and liquidation
Order types Whether a limit order is filled depends on subsequent price action; partial fills, cancellations
Multi-asset interaction Portfolio-level risk control: a loss in one asset leads to reducing positions in others
Cost dependency on state Market impact depends on the volume and order size at that specific time (see Modeling Transaction Costs)
Risk control gates Every order must pass through pre-trade checks (see Pre-trade Risk Control and Circuit Breakers)
Live trading consistency Wanting to use the exact same strategy code for both backtesting and live trading

3.2 A typical Event-Driven architecture

Market Event

Order Intent

Approved Order

Execution Report

Position Status

Data Source
(Historical Playback / Real-time Data)

Strategy

Risk Control

Execution / Simulated Matching

Portfolio and Positions

Logs and Performance

Key design points:

3.3 Hybrid usage

Common practice in the industry:

3.4 Common Open-Source Frameworks

Type Examples
Vectorized Directly using pandas / NumPy; vectorbt
Event-Driven Backtrader, Zipline (and its community-maintained versions), NautilusTrader, LEAN (QuantConnect)

When choosing a framework, focus on: whether it supports your required asset and order types, the convenience of the data interface, whether backtesting and live trading can share code, and the activity of the community.


Summary

Vectorized Event-Driven
Speed Extremely fast Slow
Look-ahead risk High, requires shift discipline Structurally avoided
Complex logic Difficult Natural
Live reuse Difficult Same code
Application Research and screening Fine-grained backtesting, paper trading, live trading