DocsBacktest Engineexecution

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:

  1. Stop orders (stop-loss, trailing stops)
  2. Limit orders
  3. 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