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.
A trade that opens and closes immediately still loses the spread, exactly as it would live. The wider an instrument's spread, the more each round trip costs — and the spread comes from real historical data, so it moves through the session just as the live market did. Commission, slippage, and funding are added only if you set them.
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.
Subscribing to candle_1h changes only how often your strategy is woken up. Your resting orders are still evaluated against every tick inside that hour, regardless of the timeframe you subscribed to. A strategy that wakes hourly and rests a stop entry gets the same fill resolution as one that wakes every minute — so grid, breakout, and stop-entry strategies stay viable on coarse timeframes.
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 type | Evaluated | Triggers when | Fills at | Capped? |
|---|---|---|---|---|
| Market (buy) | immediately | first tick | that tick's ask | no |
| Market (sell) | immediately | first tick | that tick's bid | no |
| Limit (buy) | every tick | tick.ask <= limit | that tick's ask | at the limit |
| Limit (sell) | every tick | tick.bid >= limit | that tick's bid | at the limit |
| Stop (buy) | every tick | tick.ask >= stop | that tick's ask | no |
| Stop (sell) | every tick | tick.bid <= stop | that tick's bid | no |
| Stop-limit | every tick | stop trigger, then first tick within the limit | that tick's ask / bid | at 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.
A buy stop triggers on the ask and fills on the ask. Nothing extra is charged at fill time, and outside a gap the fill lands within a hair of your stop price — which is why a stop entry can look like it cost nothing. It didn't: you paid the spread by transacting on the ask at all, exactly as a market buy does.
The trap here is calibration, not a hidden fee. The ask reaches any given level about half a spread before the mid does, so a stop level you picked off a mid-priced chart triggers earlier, and higher, than you intended. Set stop levels against the side you will actually trade on.
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.
The rules above describe exactly what the engine does; they are a committed contract, not an approximation of it. What is unmeasured is how closely those fills track a specific live broker. Two known gaps: retail forex and CFD brokers that synthesise their own quotes internally route fills against spreads that never appear in the public tick stream; and crypto synthetic quotes are centred symmetrically because the source trade data carries no taker-side flag, which biases each fill by roughly half a spread against the true touch. Treat backtested fills as a faithful model of the tick stream, not a promise about your broker.
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.
- Commission —
costs.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. - Slippage —
costs.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. - Funding —
costs.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.grossPnl—netPnlwith commission added back:grossPnl = netPnl + totalCommission.totalCommission— the sum of entry and exit commission across all trades.
grossPnl separates out commission and nothing else. Spread, slippage, and funding stay embedded in your fill prices and equity curve — they are not itemized and not added back. So grossPnl is your P&L before commission, not a cost-free figure.
For the full definition of every reported metric and the schema of the downloadable result files, see the Results Reference.