Event-Driven vs. Vectorized Backtests

A vectorized backtest computes a whole strategy as array operations over a dataframe: signals, positions, and returns are columns, and the answer arrives in milliseconds. An event-driven backtest walks forward one observation at a time, maintaining explicit state — cash, positions, open orders — and only ever sees data up to the current moment. The first is a research tool; the second is a simulator. Confusing them is one of the more expensive mistakes in strategy research.

Educational material, not trading advice. Algorithmic crypto trading is high-risk and most retail algo traders lose money.

The vectorized shape

A vectorized backtest expresses the strategy as a function of the whole series at once. The canonical skeleton:

signal   = (close > close.rolling(50).mean()).astype(int)
position = signal.shift(1)                 # act next bar
pnl      = position * close.pct_change()
equity   = (1 + pnl).cumprod()

Its virtues are real. It is fast enough to sweep hundreds of parameter combinations, which makes exploratory research practical. It is short enough to audit by reading. And it composes naturally with the rest of the analytical stack you already have — see the Python tooling stack for crypto quant research.

Its limitation is structural rather than cosmetic: because every column is available at every row, nothing in the language prevents the code from reading the future. The shift(1) above is a convention you remembered, not a constraint the framework enforced. Every leak described in auditing your code for lookahead bias is one careless line away in this style, and the code that leaks looks exactly like the code that doesn’t.

What vectorization cannot represent

Some mechanics are not expressible as elementwise arithmetic on a position column. The important ones:

  • Order state. A limit order that may or may not fill, may fill partially, and expires or persists across bars has a lifecycle. A position column has no place to store “we have a resting order at this level”.
  • Path-dependent exits. A stop or a trailing stop depends on the intrabar path and on the entry price of the specific open position. Approximating it with a column comparison is where a great many optimistic backtests come from, because the array version happily “fills” at exactly the stop level, which real markets do not promise.
  • Portfolio constraints that couple positions. Total exposure caps, per-asset limits, cash availability, and rebalancing to targets are constraints across columns at each timestamp, resolved sequentially. Applying them after the fact changes the positions that generated them.
  • Sizing that depends on realized equity. Fixed-fractional sizing means position size at time t depends on equity at t, which depends on all prior positions. That is a recursion, and recursions are what loops are for. See position sizing and risk management basics.
  • Multiple concurrent, individually managed positions. Anything with per-trade state — scaling in, per-position stops, partial exits — needs objects, not columns.

You can force some of these into vectorized form with enough cleverness. The result is usually harder to verify than the loop it replaced, which defeats the purpose.

The event-driven shape

An event-driven backtest is a loop over a queue, with state outside the loop. Conceptually:

for bar in feed:                 # feed yields data in time order, nothing ahead
    broker.mark(bar)             # revalue positions, check stops, expire orders
    for order in strategy.on_bar(bar, portfolio.snapshot()):
        broker.submit(order)     # fills resolve on the next bar

The key property is that feed physically cannot hand the strategy anything after bar. Lookahead becomes difficult rather than merely discouraged — you would have to go out of your way to pass future data in. That single architectural fact is worth more than any amount of discipline applied to vectorized code.

The costs are equally concrete. It is orders of magnitude slower, which constrains parameter search. It is much more code, and the fill logic in particular is where bugs hide: whether an order fills at the open or close of the next bar, whether a stop fills at its trigger or at a worse price, whether a limit fills when the bar merely touches the level. Every one of those is a modelling assumption you now have to make explicitly — and being forced to make them explicitly is, again, the point.

Using both

The productive arrangement is vectorized for search, event-driven for confirmation. A workflow that reflects that:

  1. Explore vectorized. Sweep parameters and feature ideas cheaply, with costs deducted crudely but not omitted. Reject the majority here.
  2. Re-implement the survivors event-driven. Not as a formality — as an independent test. A discrepancy between the two runs is information: either the vectorized version was leaking or the fill assumptions were doing work you hadn’t noticed.
  3. Investigate every discrepancy. The temptation is to trust whichever result you prefer. The two implementations disagreeing means at least one is wrong, and finding out which is the most productive debugging you will do.
  4. Validate the survivor properly. Costs, walk-forward, and an honest count of how many things you tried — see how walk-forward validation works and multiple testing and strategy selection.

A useful discipline for step 2 is to write the event-driven strategy so the same class can be pointed at a live feed later. The gap between a simulator and a live system is mostly plumbing, but only if the strategy was written to consume events rather than dataframes — see what sits between a backtest and a live bot.

Choosing per problem

A rough decision rule. Vectorized is adequate when the strategy is a position determined solely by past data, rebalanced on a fixed schedule, with no per-trade state and sizing that doesn’t depend on realized equity — a large fraction of cross-sectional and simple time-series strategies fit. Event-driven is required when stops, limit orders, partial fills, cash constraints, or equity-dependent sizing are part of the strategy rather than an afterthought.

The framing worth keeping is that these are not competing tools of different quality. A vectorized run answers “does this relationship exist in the data?” An event-driven run answers “could this have been executed?” Both questions need answering, and a strategy that passes only the first is a finding about history rather than something anyone could have traded.