Order Timing
Understanding order timing is critical for writing correct strategies.
The Bar Cycle
Each bar in a backtest follows this exact sequence:
1. Bar i closes 2. onBar(ctx, i) executes — you see Bar i's OHLCV 3. Orders are queued (but NOT filled yet) 4. Bar i+1 opens 5. Pending orders are evaluated against Bar i+1 6. Fills occur at Bar i+1 prices (± slippage) 7. Repeat from step 1
Key Implications
You cannot trade within a bar
When onBar runs, the bar is already closed. Any orders you place will fill on the next bar.
function onBar(ctx, i) { // Bar i: close = 100 ctx.order.market('ASSET', 1, { signal: 'buy' }); // Order fills at Bar i+1 open, NOT at 100 }
Position is not updated immediately
function onBar(ctx, i) { ctx.order.market('ASSET', 1, { signal: 'buy' }); const pos = ctx.position('ASSET'); // Still 0! Order hasn't filled yet. }
Order evaluation order
When multiple orders are pending, they evaluate in this order:
- Stop orders (stop-loss, trailing stops)
- Limit orders
- Market orders
Within each category, orders evaluate in placement order.
Intrabar Price Assumptions
The engine does NOT simulate intrabar price movement. For a bar with:
Open: 100, High: 105, Low: 98, Close: 102
We assume the price path was: Open → High → Low → Close
This affects stop and limit order fills.
Example Timeline
Bar 1: O=100, H=102, L=99, C=101 → onBar(ctx, 1) runs → You place ctx.order.market('ASSET', 1) Bar 2: O=101.5, H=103, L=100, C=102 → Market order fills at 101.5 (± slippage) → onBar(ctx, 2) runs → ctx.position('ASSET').qty is now 1
Why This Matters
This execution model prevents:
- Look-ahead bias — You can't act on data you wouldn't have in live trading
- Unrealistic fills — No filling at exact highs/lows
- Slippage denial — Market orders don't fill at the price you saw
Related
engineexecutiontimingfillsordersqsl