2
Event Driven Backtesting
Brummel edited this page 2026-06-15 14:58:46 +02:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Event-Driven Backtesting

Backtesting can be built two structurally different ways. The vectorized model applies whole-series array operations and is fast but easy to leak the future. The event-driven model processes one timestamped market event at a time in strict chronological order — slower, but realistic and reusable for live trading because the very same loop also consumes a live feed. This page contrasts the two models, describes the data-driven clock that drives an event loop, explains how heterogeneous sources are merged once at ingestion, and states the backtest/live symmetry that the event model buys. Narrative context on the engine side lives on Backtesting Engine Architecture; the causal guarantees on Look-Ahead Bias and Causality.

Markers: [L] law/exact, [C] convention, [CORR] corrected.

A backtest, generically, is the testing of a predictive model on historical data: it "seeks to estimate the performance of a strategy or model if it had been employed during a past period" (Backtesting) [L]. The two models below differ only in how that estimate is computed — not in what it estimates. The choice has consequences for speed, for how easily look-ahead bias creeps in, for how faithfully path-dependent execution is modelled, and for whether the same code can be redeployed live.

Related pages: Reactive Streaming Dataflow (the node/stream model an event loop drives), Determinism and Reproducibility (why the merge and the loop must be deterministic), Metric Catalogue and Metric Verification Log (what is computed from the resulting equity curve).


1. Two models of backtesting

1.1 The vectorized model

A vectorized (also called array-based or, in its simplest form, for-loop) backtester treats the whole price history as columns and applies array operations across the entire series at once: compute an indicator over the full column, compare two full columns to derive a signal column, multiply the signal column by a forward-return column to get a P&L column. The attraction is raw speed and brevity — "For-Loop backtesters are straightforward to implement in nearly any programming language and are very fast to execute" (QuantStart: Should You Build Your Own Backtester?) [C]. That speed makes vectorized code the natural tool for early research and for sweeping many parameter combinations cheaply.

The model has two structural weaknesses, both of which are easy to introduce and hard to see:

  • Look-ahead leakage through indexing. Because the whole series is in scope at once, a single off-by-one — "should you have used i, i+1 or i-1?" — silently lets a signal at time t read data from t+1 (QuantStart: Should You Build Your Own Backtester?) [C]. The computation has no built-in notion of "now"; nothing structurally prevents a cell from referencing a future cell. See Look-Ahead Bias and Causality for why this is the dominant failure mode of array backtests.
  • Coarse execution. Vectorized P&L typically assumes a fill at the next bar's open or close and ignores intra-bar path detail (slippage, partial fills, the bidask spread, order-book state). Path-dependent execution — stops, trailing exits, position-dependent sizing — does not express cleanly as a whole-column operation, because each step's action depends on the realized outcome of the previous step.

These are conventions about practice, not laws: a disciplined vectorized backtester can avoid both with careful shifting and explicit lag columns. The point is that the model gives no structural help — correctness rests on the author's vigilance.

1.2 The event-driven model

An event-driven backtester instead simulates the passage of time as a queue of discrete events consumed one at a time. Each incoming market record (a tick, a completed bar, a news item) is "treated as an 'event' that must be acted upon" (QuantStart: Event-Driven Backtesting with Python) [L]. A loop pulls the next event, lets the strategy react to it, lets any resulting orders be handled, and only then advances to the following event. QuantStart describes the canonical two-loop shape: an outer loop that "give[s] the backtester a heartbeat" and an inner loop that "actually handles the Events from the events Queue object" (QuantStart: Event-Driven Backtesting with Python) [C].

The defining advantage is that, by construction, the strategy can only see events that have already arrived. "With an event-driven backtester there is no lookahead bias as market data receipt is treated as an 'event'" (QuantStart) [L for the mechanism; the practical guarantee is [C]] — more precisely, "by virtue of its message-passing design, Event-Driven systems are usually free from Look-Ahead Bias, at least at the trading level" (QuantStart: Should You Build Your Own Backtester?) [C]. Because the engine is "drip fed" one event at a time, the future is not in scope; the strategy literally cannot index into it.

The same one-event-at-a-time discipline is what permits realistic execution: "event-driven backtesters allow significant customisation over how orders are executed and transaction costs are incurred" (QuantStart) [C]. Path-dependent logic is natural here, because each event handler runs after the previous one's effects are fully realized.

1.3 The trade-off, stated plainly

The cost is speed. Event-driven systems "are slower to execute compared to a vectorised system" because "optimal vectorised operations are unable to be utilised when carrying out mathematical calculations" (QuantStart) [C]. The two models therefore occupy opposite ends of a spectrum: vectorized for fast, exploratory research where a leaked bar is a tolerable risk; event-driven where realism, path-dependence, and live-reuse matter (QuantStart: Should You Build Your Own Backtester?) [C].

A common and reasonable workflow is to prototype vectorized, validate event-driven [C]: use array operations to scan the idea space cheaply, then re-run the survivors through an event loop that cannot cheat and that models execution faithfully. This is practitioner guidance, not a rule — some teams build event-driven from the start to keep a single codebase.

dimension vectorized event-driven
unit of work whole series / column one event at a time
speed fast (array ops) [C] slower (per-event loop) [C]
look-ahead risk high — no structural guard [C] low — future not in scope [L mechanism]
execution realism coarse (next-bar fill) [C] fine (custom fills, frictions) [C]
path-dependence awkward natural
live reuse not directly [C] same loop consumes a live feed [L]

2. The data-driven clock

2.1 Markets are an irregular event stream, not a grid

Real market activity is "an irregular sequence of timestamped events": ticks arrive when trades happen, bars complete on their own cadence, news lands whenever it lands. There is no global metronome. An event-driven engine therefore takes its clock from the data: it advances one input record = one logical step, in global timestamp order. This is exactly the next-event model of discrete-event simulation, in which a system is modelled as "a discrete sequence of events in time" and "each event occurs at a particular instant in time and marks a change of state in the system" (Discrete-event simulation) [L].

2.2 Next-event time advance vs a fixed grid

The simulation clock does not tick at a fixed rate. In next-event time progression, "between consecutive events, no change in the system is assumed to occur; thus the simulation time can directly jump to the occurrence time of the next event" (Discrete-event simulation) [L]. The alternative — incremental or fixed-increment time advance — divides time into uniform slices and processes each one; this "approach typically runs slower than next-event simulation because many time slices require processing" (Discrete-event simulation) [L].

For a backtest this distinction is not academic:

  • A fixed grid that is too coarse clumps events. Several records that fall in the same slice get processed together, collapsing their order and destroying exactly the per-event causality the model exists to protect.
  • A fixed grid that is too fine wastes empty steps. Most slices contain no event; the loop spends its time advancing a clock past nothing. The next-event model "skip[s] periods where nothing happens" (Discrete-event simulation) [L], which is both more efficient and a closer match to how markets actually behave.

Advancing one record at a time, in timestamp order, sidesteps both failure modes: every event is processed exactly once, in its true position, and no clock cycles are spent on emptiness. See Determinism and Reproducibility for why this stepping must also be reproducible, and Reactive Streaming Dataflow for how each step propagates through a graph of nodes.


3. Merging heterogeneous sources

3.1 The problem: many cadences, one timeline

A realistic strategy reads from several streams at once — high-rate ticks, lower-rate completed bars, and intermittent, irregular news or sentiment events. Each stream is individually sorted by time, but they have wildly different cadences. For the event loop to honour causality, these must be presented as a single chronological sequence: at every step, the engine must process the globally earliest unprocessed record, whichever stream it came from. Otherwise a slower stream could deliver an event "late" relative to wall-clock processing and let a later strategy step see something it should not have, or miss something it should have.

3.2 The k-way merge

Combining several already-sorted sequences into one sorted sequence is exactly the k-way merge (multiway merge) problem. K-way merge algorithms "are a specific type of sequence merge algorithms that specialize in taking in k sorted lists and merging them into a single sorted list" (K-way merge algorithm) [L]. More generally, merge algorithms "take multiple sorted lists as input and produce a single list as output, containing all the elements of the inputs lists in sorted order" (Merge algorithm) [L]. The k-way case is the generalization of the binary merge step at the heart of merge sort: "a K-way merge method, a generalization of binary merge, in which k sorted sequences are merged" (Merge sort) [L].

Heterogeneous-cadence sources are merged once, by timestamp, into a single ordered event stream.

flowchart LR
  tick["tick stream, irregular"]
  bars["one-minute bars"]
  news["daily news bias"]
  merge["k-way merge by timestamp, min-heap"]
  timeline["one chronological event stream"]
  strat["strategy graph"]
  tick --> merge
  bars --> merge
  news --> merge
  merge -->|"ties break by source order"| timeline
  timeline --> strat

The efficient implementation uses a priority queue / min-heap keyed by the front element of each stream. One maintains "a min-heap h of the k lists, using the first element as the key" (Merge algorithm) [L]; the heap repeatedly yields the globally smallest front element, that record is emitted, and its stream is advanced by one and re-inserted. A priority queue is the right abstraction here: "each element has an associated priority, which determines its order of service [and the queue] serves highest priority items first" (Priority queue) [L] — with the ordering chosen so that the earliest timestamp has highest priority.

Complexity [L]. Merging k sorted sequences with a total of n elements via a binary heap runs in O(n log k) time, versus O(k·n) for the naive scan-all-fronts approach (K-way merge algorithm, Merge algorithm). The heap holds at most k entries, so the extra space is O(k). A binary heap gives "O(log n) performance for inserts and removals" (Priority queue) [L], which is what yields the log k factor when the heap holds k stream fronts.

[CORR] Heap vs tournament tree. A frequent claim is that the min-heap is the fastest practical k-way merger. The k-way-merge reference corrects this: "a tournament tree is faster in practice. A heap uses approximately 2·log(k) comparisons in each step ... [a] tournament tree only needs log(k) comparisons" (K-way merge algorithm). Both are O(n log k); the constant factor differs. Treat "use a min-heap" as the standard, correct, and simplest choice [C], not as the comparison-optimal one.

3.3 The tie-break for equal timestamps

Two records from different streams can carry the same timestamp. The merge must then apply a deterministic tie-break, because the relative order of equal-keyed records is not fixed by the key alone. The plain merge routine "is not stable: if equal items are separated ... they will become interleaved" (Merge algorithm) [L] — i.e. without an explicit rule the order of simultaneous events is unspecified. A stable merge, by contrast, preserves a defined order of equal elements: most merge-sort implementations "are stable, which means that the relative order of equal elements is the same between the input and output" (Merge sort) [L].

For a backtest the practical rule [C] is to make the tie-break total and fixed — for example, break ties by a stable per-source priority (a stream rank), then by per-source arrival order — so that equal-timestamp events are always processed in the same order on every run. Which order is chosen matters less than that it is deterministic; ambiguity at equal timestamps is a classic source of irreproducible results (see Determinism and Reproducibility).

3.4 Why merge once, at the ingestion boundary

A load-bearing architectural choice is to perform this merge exactly once, at the ingestion edge, producing a single ordered timeline that everything downstream consumes. The reasons are causal and semantic:

  • One ordered timeline is what makes heterogeneous-rate sources combinable without leaking the future. Once every record sits in one global order, "see only the past" reduces to "see only records earlier in this one sequence." A fast and a slow stream are reconciled by position in the merged order, not by ad-hoc rate matching. The next-event clock of §2 then simply walks this sequence.
  • Merging only at the boundary keeps the computation semantics simple. If alignment of differently-timed streams were instead done inside the computation — scattered as-of joins or "most-recent value as of t" lookups at many nodes — every such site would be a fresh opportunity to misalign and a fresh place where a future value could leak in. Concentrating all cross-stream ordering at one boundary means the downstream graph never performs a temporal join at all; it only ever reacts to the next event handed to it. This is the dataflow discipline described on Reactive Streaming Dataflow and is closely tied to the structural look-ahead guarantees on Look-Ahead Bias and Causality.

In discrete-event terms, the merged stream is the event list of the simulation, and the loop is next-event time advance over it (Discrete-event simulation) [L].


4. Backtest / live symmetry

The deepest payoff of the event-driven model is that backtest and live trading share identical event semantics; only the origin of the events differs. In a backtest the events are replayed from historical records; live, they arrive from a market feed. The handling loop — react, order, advance — is the same. An event-driven backtester "by design, can be used for both historical backtesting and live trading with minimal switch-out of components" (QuantStart: Event-Driven Backtesting with Python) [L], and such systems "are much more akin to live-trading infrastructure implementations" (QuantStart: Should You Build Your Own Backtester?) [C]. In practice only the data origin and the execution endpoint are swapped: replace the historical event source with a live feed and the simulated fill model with a real broker connection, and the strategy logic is untouched.

This symmetry is structurally identical to event-driven architecture in software at large, where an event is "a significant change in state" (Event-driven architecture) [L], emitters "detect, gather, and transfer events" without knowing their consumers, and consumers (sinks) apply "a reaction as soon as an event is presented" (Event-driven architecture) [L]. A backtest engine and a live engine are two emitters feeding the same consumer graph. Because the strategy sees only the event interface — never the origin — a strategy validated in the loop is the same artifact that runs live, eliminating the research-to-production rewrite that plagues vectorized pipelines.

Two consequences follow [C]:

  • Less code to trust. A single event-handling path is exercised by both backtests and live trading, so bugs in that path surface during research rather than in production.
  • Fidelity is bounded by the source and the execution model, not the loop. Since the loop is shared, any backtest-vs-live divergence is localized to the two swapped components — the event origin (was the recorded data faithful?) and the fill model (does the simulated execution match the broker?). The realistic execution model is discussed on Backtesting Engine Architecture.

A caveat [C]: symmetry of semantics does not by itself guarantee symmetry of results. Live feeds carry latency, gaps, and revisions that a clean historical replay omits; the recorded source must be captured faithfully (and deterministically — see Determinism and Reproducibility) for the backtest to be a fair proxy for the live system.


References

Backtesting and the two models (§1, §4)

The data-driven clock (§2)

Merging heterogeneous sources (§3)

Event-driven architecture (§4)