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
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
Key design points:
- Data source is replaceable: Replay history during backtesting and connect to an exchange during live trading; the strategy code remains unchanged;
- Simulated matching must be conservative: A limit order is only filled when the price crosses the limit price, not just when it "touches" it; limit the filled quantity based on a proportion of the volume;
- Strict timestamps: All events are sorted by time; the order of events occurring at the same moment must have clear rules;
- Reproducibility: Random seeds, data versions, and code versions must all be recorded.
3.3 Hybrid usage
Common practice in the industry:
- Use vectorization during the research phase to run hundreds of variants in seconds to eliminate obviously non-viable ideas;
- For a few candidate strategies, use event-driven for fine-grained backtesting, incorporating realistic costs, constraints, and risk control;
- When results between the two differ significantly, it usually means the vectorized version missed a real-world constraint or contains look-ahead bias.
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 |