Backtest Execution Flow
The backtest loop processes market state before strategy callbacks, then settles commands generated at the same timestamp.
Data and message sequencing
In the main backtesting loop, new market data is processed for order execution before being dispatched to actors/strategies via the data engine.
Main loop flow
For each data point the engine runs three phases:
- Exchange processes data. The simulated exchange updates its order book from the incoming market data and iterates the matching engine. This fills any existing orders that now match against the new market state.
- Strategy receives data. The data engine dispatches the data point to actors
and strategies via their callbacks (e.g.
on_quote,on_bar). Strategies may submit, cancel, or modify orders during these callbacks. - Settle venues. The engine drains all queued venue commands and then iterates
matching engines to fill newly submitted orders. This loop repeats until no
eligible commands remain, so cascading orders (e.g. a hedge submitted from
on_order_filled) settle within the same timestamp. Earlier latency-delayed commands follow the instrument-scoped rules under command settling.
The three phases ensure resting orders see the incoming market before newly submitted orders do.
Timer events use the same settle mechanism but batch by timestamp: all callbacks at timestamp T execute first, then venues are settled for T before advancing to T+1. For timer behavior used by internally aggregated bars, see internal bar aggregation timing.
Deferred option settlement
At an option's expiration timestamp, automatic expiry checks close its market, cancel open orders,
and reject new orders. Position settlement waits until all market data at that timestamp has been
processed, so settlement sees the latest underlying price available for that timestamp. An explicit
InstrumentClose with InstrumentCloseType::ContractExpired attempts settlement immediately.
Streaming batches must keep all data for a timestamp together; BacktestNode does this automatically.
SimulatedVenueConfig.defer_option_settlement defaults to true. The backtest engine schedules
settlement after all market data at the expiry timestamp, without waiting for the next timestamp.
When driving SimulatedExchange directly, schedule expiry processing after that timestamp's data,
or explicitly set defer_option_settlement to false for immediate settlement. Immediate settlement
can use an older underlying price if an update with the same timestamp has yet to be processed.
Command settling
Same-cycle commands
An order fill can trigger a strategy callback that submits another order, such as a stop-loss from
on_order_filled. The engine drains venue command queues and processes any commands generated by
their events until no command eligible for the current cycle remains. Commands created at that
timestamp and already due, including zero-latency and same-tick commands for another instrument,
settle within the same cycle. Simulation modules run once, after the command loop completes.
Latency-delayed commands
A LatencyModel places each command in the venue's inflight queue with an arrival timestamp. Once a
command is due, the settlement point determines whether the engine releases it:
| Settlement point | Due commands released |
|---|---|
| Market data | Same-timestamp commands and older commands for the data's instrument. |
| Timer | All commands due at the timer timestamp. |
| Funding-rate settlement | All commands due at the funding settlement timestamp. |
| Shutdown drain | All commands due as the clock advances through the final inflight arrival. |
Market data for another instrument does not activate an older command against stale market state. Commands with a future arrival timestamp remain in the inflight queue.
Sandbox inbound latency
SandboxExecutionClientConfig.latency_model accepts a StaticLatencyModel, mirroring the existing
fee_model field. A submit, modify, or cancel is deferred by the model's insert, update, or delete
leg before it reaches the matching engine. Venue-generated events (accepts, fills, cancels,
expirations) are not delayed, so the model covers the inbound leg only. Without a latency model the
client is unchanged and its events take the runner's execution channel as before.
from nautilus_trader.adapters.sandbox import SandboxExecutionClientConfig
from nautilus_trader.execution import StaticLatencyModel
from nautilus_trader.model import Money
from nautilus_trader.model import Venue
config = SandboxExecutionClientConfig(
venue=Venue("BINANCE"),
starting_balances=[Money.from_str("10_000 USDT")],
latency_model=StaticLatencyModel(base_latency_nanos=1_000_000_000),
)Every event the client emits takes the runner's execution channel exactly as it does without a
latency model, in emission order: an order's OrderSubmitted precedes its venue events, and a
fill from market data precedes the response to any command released after it. A command is
applied before any market data processed after its due time, since the client drains its queue
ahead of each tick it receives, and the client's clock alert releases a queue no data is flowing
to. A command whose latency leg is zero is applied on arrival, unless a command is already due
and not yet released, in which case it joins the queue behind it. A cancel-all reaching the venue
cancels only orders the venue has received: an order whose submit is still in transit is left
alone until the venue processes its submit.
In both backtest and sandbox, contingent actions also respect venue receipt. Fills, updates, expirations, and cancellations cannot activate, amend, or cancel a linked order the venue has not yet received. Each submit list arrives as a unit. An OTO child already received by the venue can still activate when its parent fills. A late submit is checked against the current state of its linked orders and may be rejected if a linked order has already closed. Contingent quantity changes skipped before receipt are not replayed when the submit arrives; the order retains its submitted quantity unless another applicable rule changes it.
Stopping the client discards anything still in flight. A discarded submit, modify, or targeted
cancel is rejected (OrderRejected, OrderModifyRejected, OrderCancelRejected) so its order
does not stay SUBMITTED or pending forever; the sandbox generates no order status reports, so
nothing else would resolve it. A discarded CancelAllOrders is dropped, since the strategy marks
no order PENDING_CANCEL for it and so there is no pending state to release.
Shutdown semantics
BacktestEngine::end() is separate from the shutdown_on_error configuration in backtest APIs and
repeated runs. It invokes each strategy's on_stop handler,
drains and settles any commands it emits (e.g. close_all_positions, cancel_all_orders), then
stops the engines.
on_stopcommands use normal venue queuing and latency. They do not get priority over earlier inflight commands.- If a pre-stop order reaches the venue before an
on_stopcancel, it may still fill. A later reduce-only close can then reject if the fill changed net exposure. - Strategies that need deterministic flattening should enter an exit-only state before stopping and avoid new opening orders while cancel and close commands are in-flight.
- Strategy event handlers do not fire for the resulting events: the strategy is already
Stopped, soOrderFilledand similar events log but bypasson_order_filledand friends. Logic that reacts to fills must run beforeon_stopreturns. - Simulation modules do not re-run at shutdown.
SimulationModule::processis once per timestamp; re-invoking would double-apply side effects like FX rollover interest. - A
LatencyModeladds its configured delay to trailing commands (those emitted on the final data tick or inon_stop). The shutdown path advances the engine clock to the latest inflight arrival timestamp so those commands still settle before the engines stop.
Timer-only backtests
The backtest engine supports runs with timers but no market data. This is useful for scheduled operations or testing timer-based logic. Timers fire in chronological order.
Deterministic trade IDs
The simulated exchange (used by both backtest and sandbox execution) emits a deterministic TradeId
for each generated fill. The ID is formatted as T-{hash:016x}-{count:03d}, where the 16-character
hex is an FNV-1a hash of (venue, raw_id, ts_init) and the trailing counter distinguishes multiple
fills at the same ts_init (e.g. several legs of a bar-driven fill).
Deterministic trade IDs have these properties:
- Deterministic across runs: the same replayed data produces the same
TradeIdevery time, so downstream dedup and golden-output comparisons stay stable. - Collision-safe across resets:
ts_initis pinned in backtest data and monotonic in live/sandbox, so aBacktestEngine.reset()(or an in-memoryIdsGeneratorreset in a sandbox with persisted orders) cannot mint aTradeIdthat collides with one already in the cache. - Bounded length: the hash keeps the identifier under the 36-character
TradeIdcap regardless of venue name length.
The use_random_ids venue flag still governs VenueOrderId and PositionId generation, but
TradeId is always deterministic and is not affected by the flag.
Backtest Data and Venues
Historical data advances the backtest clock, updates simulated market state, and drives strategy callbacks. The venue's book_type determines which data can...
Fill Prices and Matching
The backtest matching engine treats recorded order book and trade data as immutable. Simulated fills do not edit the historical book. This preserves the...