Tektii just went live. We're shipping fixes daily — spot a bug? Let us know

Backtest Execution & Costs

When your strategy sends an order, the backtest engine fills it against the market data for that instant. Every fill crosses the bid/ask spread — the same spread a live broker would quote. On top of the spread you can optionally model commission, slippage, and funding. All three default to zero, so unless you configure them a backtest pays only the spread.

How orders fill

Every order crosses the spread at the prevailing quote:

  • A buy fills at the ask (the higher quote).
  • A sell fills at the bid (the lower quote).

Crossing the book on every fill, never getting the mid-price for free, is the baseline realism the engine guarantees. It is not the whole of execution realism — see what the engine does not model for the limits.

When your orders are evaluated

Resting orders are checked intrabar — against every tick of market data, in chronological order. They are not held until the end of a bar.

The ticks are real market events: one per broker quote for forex, one per trade for crypto. They are not points derived from candle OHLC, so a "tick" is an instant the market actually printed, not a reconstructed bar corner.

Candles are delivered at the bar close, not the bar open, so your strategy never acts on a bar that has not finished forming.

Fill rules by order type

Tektii uses a tick-through model: an order fills at the price of the triggering tick, on the side of the book it must cross. The order's own limit or stop price is the trigger, not the fill.

Order typeEvaluatedTriggers whenFills atCapped?
Market (buy)immediatelyfirst tickthat tick's askno
Market (sell)immediatelyfirst tickthat tick's bidno
Limit (buy)every ticktick.ask <= limitthat tick's askat the limit
Limit (sell)every ticktick.bid >= limitthat tick's bidat the limit
Stop (buy)every ticktick.ask >= stopthat tick's askno
Stop (sell)every ticktick.bid <= stopthat tick's bidno
Stop-limitevery tickstop trigger, then first tick within the limitthat tick's ask / bidat the limit

Three consequences worth internalising:

  • The stop price is the trigger, never the fill price. A weekend or news gap that overshoots your stop fills at the overshot price — realised slippage equals the gap distance. Stop and market orders have no cap, so a gap slips in full.
  • The limit price is a worst-case cap, not a fill price. It binds only when the tick price (plus slippage) would be worse than your limit. A gap-through can fill you better than the limit price, since the engine does not assume a broker would have rested your order in a book and matched it at exactly the limit.
  • Stop-limit can leave you unfilled. If the stop triggers but no later tick comes back within the limit, the order stays pending indefinitely.

The table describes the default GTC behaviour. IOC is the exception: an IOC order is evaluated against the first available tick only — it fills whatever that tick supports and the remainder is cancelled — so an IOC limit or stop never rests.

What the engine does not model

Fills assume your order is satisfiable at the quoted price. Two absences are worth knowing before you size up a strategy:

  • No order-book depth. There is no size behind the quote for your order to exhaust. Forex tick sizes are indicative, and crypto sizes are a proxy taken from the trade print. Order size affects your fill price only through costs.slippage — that is the only lever that makes a larger order fill worse.
  • No queue position. A resting limit order fills the moment the quote touches your price. A real book would require the market to trade through your level with enough volume ahead of you cleared first, and price frequently touches a level and reverses without filling anyone. This flatters limit-resting, market-making, and mean-reversion strategies in particular.

Where the spread comes from

The spread is taken from real historical market data, not a fixed assumption, so it widens and narrows through the session just as the live market did. How the bid and ask are sourced depends on the asset class.

  • Forex — native broker bid/ask quotes. The historical feed already carries both sides of the book, so your fills use the actual quoted spread for each instant.
  • Crypto — synthetic bid/ask. Crypto venues publish trades rather than a continuous top-of-book quote, so Tektii derives a bid and an ask around the traded price. The quote is centred on the trade price — half the estimated spread either side. The width is estimated from 1-minute price action and smoothed as a rolling median over the previous hour, so it tracks changing conditions without whipsawing on a single bar. Until that window has filled, and across stretches with no trades, the engine falls back to a conservative fixed estimate rather than a measured one. Your fills cross this synthetic spread the same way forex fills cross the native one.

Configurable costs

Beyond the spread, three optional costs can be layered on through the costs object in your scenario config file. Every cost defaults to zero — omit costs (or leave a field at its default) and the backtest pays only the spread: nothing extra is deducted.

  • Commissioncosts.commission. Charge basis points of notional per side (entry and exit) with { "model": "bps_of_notional", "fee_rate_bps": "10" }. The default, { "model": "zero" }, charges nothing.
  • Slippagecosts.slippage. Adds adverse price movement on top of the spread, in basis points — base_slippage_bps, plus a size-impact term and a cap. Defaults to zero.
  • Fundingcosts.funding. Models overnight financing on positions held across the rollover, e.g. { "model": "daily", "long_rate": "-0.03", "short_rate": "-0.01" }. The default, { "model": "disabled" }, charges nothing.

Example — charge 10 bps commission per side and leave slippage and funding off:

{
  "strategyVersionId": "b2c3d4e5-f6a7-4b2c-8d3e-4f5a6b7c8d9e",
  "subscriptions": [{ "instrument": "F:EURUSD", "events": ["candle_1m"] }],
  "startTime": "2024-03-04T13:30:00Z",
  "endTime": "2024-03-08T21:00:00Z",
  "costs": {
    "commission": { "model": "bps_of_notional", "fee_rate_bps": "10" }
  }
}

Which costs apply to which order types

None of the three costs care what kind of order you sent:

  • Slippage applies to all four order types, on both entries and exits. There is no passive-fill or resting-order exemption — a limit order pays slippage just as a market order does. The one order-type difference is the cap: on limit and stop-limit orders, slippage that would push the fill past your limit price is truncated there, so a limit triggered right at its price effectively absorbs none. Stop and market orders are never capped.
  • Commission is charged per side — entry and exit — regardless of order type. There is no maker rebate; passive and aggressive fills are billed identically.
  • Funding depends only on position direction, size, and how long you hold it. The order type that opened the position is irrelevant.

The top-level config keys are camelCase, but costs and its nested fields use snake_case (fee_rate_bps, base_slippage_bps, long_rate) on every surface — see Scenario Configuration for the full config file.

Gross vs net results

Backtest results report both a net and a gross P&L so you can see the impact of commission at a glance:

  • netPnl — your equity change after every cost the engine applied: spread, commission, slippage, and funding.
  • grossPnlnetPnl with commission added back: grossPnl = netPnl + totalCommission.
  • totalCommission — the sum of entry and exit commission across all trades.

For the full definition of every reported metric and the schema of the downloadable result files, see the Results Reference.