Skip to content
python.financial

An event-driven backtest changes system state by processing discrete events such as market data, timers, signals, order submissions, acknowledgments, fills, cancellations, and account updates. Each event is handled at a simulated time and may schedule more events.

This architecture is well suited to stateful strategies and order lifecycles. It does not make a backtest realistic by itself. Realism depends on what the historical data can reveal and on explicit fill, liquidity, latency, fee, margin, and venue rules.

A typical event sequence

A simplified sequence for one market decision is:

market data becomes available
    -> strategy callback updates state
    -> strategy submits an order
    -> simulated latency advances order arrival
    -> venue accepts, rejects, or holds the order
    -> later market data may trigger a fill
    -> fill event changes cash and position
    -> risk and account state are recalculated

Engines also need a deterministic rule for events with the same timestamp. A timer, bar close, cancel request, and exchange fill processed at 10:00:00 can produce different outcomes in a different order. An event queue therefore uses the timestamp, an event priority, and often an insertion sequence as the final tie-breaker. Document that ordering whenever a strategy can react within the same timestamp.

Backtrader example

The example below uses six synthetic daily bars. A market buy is submitted from the January 2 next() callback and fills at the next bar's January 3 open. A later close request follows the same decision-then-fill sequence. Commission is 0.1% on each fill.

import backtrader as bt
import pandas as pd


bars = pd.DataFrame(
    {
        "open": [100, 101, 102, 104, 105, 103],
        "high": [101, 102, 104, 105, 106, 104],
        "low": [99, 100, 101, 103, 102, 101],
        "close": [100, 102, 103, 104, 103, 102],
        "volume": [1_000] * 6,
    },
    index=pd.bdate_range("2025-01-02", periods=6),
)


class RoundTrip(bt.Strategy):
    def next(self):
        date = self.data.datetime.date(0)
        if len(self) == 1:
            print(f"decision {date}: submit buy")
            self.buy(size=10)
        elif len(self) == 4 and self.position:
            print(f"decision {date}: submit sell")
            self.close()

    def notify_order(self, order):
        if order.status == order.Completed:
            date = bt.num2date(order.executed.dt).date()
            print(
                f"fill {date}: {order.getordername()} "
                f"size={order.executed.size:g} price={order.executed.price:.2f} "
                f"commission={order.executed.comm:.2f}"
            )


engine = bt.Cerebro(stdstats=False)
engine.broker.setcash(10_000)
engine.broker.setcommission(commission=0.001)
engine.adddata(bt.feeds.PandasData(dataname=bars))
engine.addstrategy(RoundTrip)
engine.run()
print(f"final value: {engine.broker.getvalue():.2f}")

Executed with Backtrader 1.9.78.123, it prints:

decision 2025-01-02: submit buy
fill 2025-01-03: Market size=10 price=101.00 commission=1.01
decision 2025-01-07: submit sell
fill 2025-01-08: Market size=-10 price=105.00 commission=1.05
final value: 10037.94

The final value is hand-checkable: (105 - 101) * 10 - 1.01 - 1.05 = 37.94. Backtrader's official order documentation specifies the next available price, normally the next bar's open, for a simulated market order. Its execution documentation also states that volume does not participate in the default matching logic.

This fixture checks callback order, pending market orders, fill notifications, position state, commission, and account value. It does not test volume limits, spread, slippage, partial fills, queue position, latency, corporate actions, borrow, or a live broker. The constructed gain is not evidence of an edge.

Event-driven does not automatically mean realistic

Layer What an event-driven engine can represent What must still be supplied or assumed
Clock and queue Ordered timestamps, priorities, and scheduled callbacks Correct exchange calendars, release times, and tie-breaking rules
Market data Bars, trades, quotes, depth updates, and reference events Point-in-time, correctly timestamped, sufficiently granular history
Orders Submission, acknowledgement, modification, cancellation, expiry Venue-specific validation, tick size, sessions, and order semantics
Execution Partial fills, book consumption, latency, and fees Calibrated liquidity, queue, impact, spread, and rejection models
Portfolio Cash, positions, realized and unrealized PnL Corporate actions, funding, borrow, margin, settlement, and FX rules
Live adapter Reuse of message and strategy interfaces Network failures, concurrency, broker state, reconciliation, and recovery

Daily OHLC bars cannot reveal the order of the high and low, actual spread, trade sizes, or queue. An event-driven loop can process a stop before a target because its model chose that ordering, but the bar alone does not prove which happened first. More detailed architecture cannot recover information absent from the data.

Chronology reduces some errors but does not prevent look-ahead

Delivering data only when its event time is reached makes accidental future indexing less convenient. Look-ahead remains possible when a data loader exposes future rows, a feature was computed on the full sample, a fundamental value is timestamped before publication, a callback queries unrestricted history, or a same-bar fill uses information that arrived after the decision.

The engine must distinguish event time, exchange time, receive time, and when information became actionable. A chronological loop over incorrectly timestamped data is still a look-ahead-biased backtest.

Shared code does not make backtests and live trading equal

Some frameworks can run the same strategy class in simulation and live modes. That reduces translation work and lets order-state callbacks be exercised before deployment. The runtime environments still differ:

  • a backtest usually processes deterministic historical events as fast as possible,
  • live data and broker events can arrive concurrently, late, duplicated, or out of order,
  • simulated venue state is local while live state must be reconciled with an external broker,
  • simulated acknowledgements and fills follow configured rules while live outcomes belong to the venue,
  • process restarts, credentials, rate limits, disconnects, and manual account activity affect live operation.

The QuantConnect order-event documentation illustrates explicit order-state callbacks, while its event-handler documentation documents differences between backtest queuing and live scheduled-event threading. Shared APIs do not erase those differences.

Speed depends on the job

An event loop adds per-event dispatch and state transitions. Repeating a full simulation for each parameter combination can make broad searches expensive. That does not mean every event-driven engine is inherently slow. Compiled loops, batched ingestion, parallel independent runs, efficient books, and Rust or C++ cores can process large event streams quickly.

Likewise, vectorized and event-driven are not mutually exclusive. VectorBT PRO broadcasts broad research grids, supports stateful compiled callbacks and row-wise portfolio simulation, and can continue supported simulations from saved state. An effective workflow may use array operations for signal discovery, a stateful simulator for portfolio constraints, and a detailed event engine for venue behavior.

When event-driven simulation is the right fit

Use it when results depend on order state, multiple asynchronous feeds, intraday sequence, latency, partial fills, shared capital, dynamic risk, or the exact interaction among orders. NautilusTrader is designed around simulated and live event streams and exposes configurable fill, fee, latency, and margin behavior. Its official behavioral-model documentation explicitly notes that simulated fill and latency models do not determine live venue fills.

Use a simpler vectorized method when the research question is primarily about signal logic on aligned bars and the assumed execution can be expressed transparently. See vectorized versus event-driven backtesting for the decision criteria. The architecture should follow the information and state the strategy actually needs, not serve as a proxy for realism.

Choose which optional services may run. You can change these settings at any time.