Order Lifecycle
How submitted orders become fills, cash, positions, NAV and audit evidence.
Order mode is a bar-by-bar execution simulation. A strategy submits orders; the fill engine stores them as pending orders, tests them against later OHLCV bars, emits fills, updates cash and positions, and records evidence.
backtester/core.py + backtester/execution/fill_engine.py + backtester/execution/order_types.pyOrder objects, OHLCV bars, slippage model, commission model, volume capexecution_mode="orders" + _compute_orders(...)State Transition
Timing Rules
- Pending orders are processed before
_compute_orders(...)runs for the current bar. - Orders submitted by
_compute_orders(...)are pending for future bar processing. - Market orders fill at
bar.openby default, orbar.closewhenFillEngine(fill_at="close")is configured. - A submitted order does not change cash, position or NAV until a fill is emitted.
- A bracket parent creates child exits only after the entry order fills.
Fill Priority
The same-bar priority is explicit:
STOP -> STOP_LIMIT -> STOP_TRAIL -> STOP_TRAIL_LIMIT -> LIMIT -> MARKET -> BRACKET -> OCOThis is conservative for protective exits on daily bars. If TP and SL are both touched inside one daily candle, this priority is the documented convention.
Order State And Queries
| API | Meaning |
|---|---|
self.fill_engine.pending_orders | active orders still eligible to fill |
self.fill_engine.order_history | all submitted orders, including inactive historical orders |
self.fill_engine.fill_history | all emitted fills |
self.fill_engine.last_fill(inst) | last fill object for one instrument |
self.fill_engine.last_fill_price(inst) | last actual fill price after slippage |
self.fill_engine.cancel(order_id) | cancel one active order |
self.fill_engine.cancel_all(instrument=inst) | cancel active orders for one instrument |
Sizing Helpers
Use helpers inside _compute_orders(...) when you want Zipline-like sizing without manual share arithmetic.
self.order_value("AAPL", 25_000)
self.order_percent("AAPL", 0.15)
self.order_target_percent("AAPL", 0.20)
self.close_position("AAPL")
self.bracket_percent("AAPL", weight=0.20, tp=0.10, sl=0.05)Partial Fills And Capacity
FillEngine(max_volume_participation=...) caps per-bar fill quantity as bar volume times the configured participation rate. If the cap is lower than remaining quantity, the fill status is PARTIAL and the order remains active. This is volume participation, not exchange queue modeling.
Minimal Order Strategy
from backtester import Backtester
from backtester.execution import OrderType
class OneEntry(Backtester):
def _compute_orders(self, date, bars, current_positions, nav):
if self.fill_engine.order_history:
return
self.order_percent("SPY", 0.10, order_type=OrderType.MARKET)
strategy = OneEntry(
instruments=["SPY"],
backtest_period={"start": "2020-01-01", "end": "2025-01-01"},
execution_mode="orders",
)
await strategy.run_strategy()Failure Modes
- Submitting a duplicate entry while already long.
- Treating a submitted order as a filled position.
- Leaving stale stop, limit, bracket or OCO orders active after a signal exit.
- Comparing daily-bar order results without checking same-bar priority.
- Ignoring partial fills when a volume participation cap is configured.
- Using daily bars for a strategy whose edge depends on intraday order sequence.
Audit Checklist
- Is
execution_mode="orders"set? - Are open, high, low, close and volume available, or did the engine fall back to close?
- Which orders are pending after each bar?
- Which fills changed cash and positions?
- What slippage and commission model produced each fill price?
- Did any order expire by
TimeInForce,expire_atorexpires_after_bars?