DocsData & MethodologyFills, marks and slippage
Data & MethodologyGuide

Fills, marks and slippage

Every fill in a backtest is priced by the fill mode you choose, capped against the liquidity that was actually quoted, and charged a fixed per-contract commission. Three fill modes exist: mid prices at the quoted midpoint, bid_ask crosses the full quoted spread, and the Backtest.ai fill model estimates where between mid and the touch an order actually fills. A size-aware capacity layer (a lot clamp plus a market-impact term) applies in every mode. Your equity curve is marked at mid the whole time you hold, whatever your fills paid. This page explains how a backtest prices every entry, exit, roll, and per-minute mark, why the two numbers (what you trade at and what you are marked at) differ, and what the engine does and does not model.

Updated Jul 20268 min read

For a task-focused walk through picking a fill mode for a specific backtest, see backtest assumptions and realism. This page is the methodology behind it.

Three fill modes, one required choice

The fill mode is required on every backtest. There is no silent default: you choose it, and it has three values.

mid prices every fill at the quoted midpoint. Opens, closes, rolls, and scale-ins all book at mid, and you pay no spread cost. This is the optimistic bound. Real fills never land exactly at mid, so a mid-mode result flatters a strategy that trades often or trades wide spreads.

bid_ask crosses the full quoted spread on every fill. You buy at the ask and sell at the bid. This is the pessimistic bound, the worst normal case: a marketable order that pays the entire posted spread every time.

The Backtest.ai fill model is the calibrated middle. It estimates, per fill, where between mid and the touch an order actually fills, so the booked price carries a realistic fraction of the spread instead of none of it or all of it. Paper and live trading always price with the fill model, so a backtest run under it prices the way forward trading on the platform does.

Real fills land between the two bounds, and larger orders fill worse than small ones. The fill model estimates the first effect; the capacity layer below handles the second. Use the fill model when you want the backtest to match paper and live, mid for an optimistic bound, and bid_ask for a pessimistic bound.

The (mode, side) pricing table

Which price a fill books depends on the fill mode and on whether the action is a buy or a sell. A buy is opening a long leg or closing a short leg. A sell is opening a short leg or closing a long leg. The per-minute mark used for margin and net liquidation value is a third case, and it is always mid.

Action
mid
bid_ask
Backtest.ai fill model
Open long / close short (a buy)
mid
ask
between mid and the ask, calibrated
Open short / close long (a sell)
mid
bid
between mid and the bid, calibrated
Per-minute mark (margin, NLV)
mid
mid
mid

The quote is read from the contract's quote stream at the fill minute. In bid_ask, a round trip pays the spread twice: once crossing to the ask on the way in, once crossing to the bid on the way out. Under the fill model, a round trip pays the calibrated fraction of the spread each way. On top of the modeled price, the market-impact term below adds a size-dependent penalty in every mode.

Your equity curve is marked at mid

Marks are not fills. Margin, net liquidation value, and the unrealized P&L you see between trades all use the mid quote, regardless of the fill mode. Only the entry and exit transactions pay spread cost, in bid_ask and under the fill model.

So a spread-paying run books its cost on the transactions that open and close a position, but the equity line drawn while you hold that position is the mid-to-mid path. The drawdowns you read off that line are marked at mid, not at what you would realize by liquidating into the bid at that instant. This keeps the marking consistent with how margin is computed: the margin engine values every open leg at mid every minute, so your excess liquidity and your equity curve agree with each other. It also means the gap between a mid run and a bid_ask run is concentrated at the transaction points, not spread across the holding period. When you compare two runs in backtest results, that difference is the round-trip spread cost.

Forced liquidations always cross the spread

A mid-mode run is not purely mid. Two engine-driven exits ignore the fill mode and price at the marketable bid/ask no matter what:

  • A hard-deficit forced liquidation, when excess liquidity goes negative and the engine closes positions to restore it.
  • An equity-halt force-close, when net liquidation value falls to zero or below and the engine flattens everything.

Both cross the spread in every fill mode. The reasoning is conservative on purpose: when a run is stressed enough to trigger a margin liquidation, assuming you got mid on the way out would understate the damage. A stressed run is penalized harder than a clean one, which is the correct bias.

A soft-trim close is the exception among engine-driven exits. When the engine trims positions to hold a user-set minimum excess liquidity threshold, those closes use the run's fill mode, because a soft trim is a threshold you chose, not a forced marketable exit.

You don't trade the open or the close

The engine never fills at the session open or the session close. By default, the first fill of a position lands 5 minutes after the open, at 09:35. On the day a contract expires, the engine closes it 5 minutes before that session's close, at 15:55 on a normal day or 12:55 when the session ends at 13:00 on a half-day. Positions are never carried into the expiration-day settlement in-book.

These 5-minute bounds are execution offsets, and both are configurable. They are not a trim of the data window: the accessor spine still runs 09:31 to the session close, so a signal reads every minute of the session even though the engine will not execute in the first or last few. The offsets exist because the opening and closing minutes carry the widest and least reliable quotes, and filling there would flatter a result against prices you could not actually work. For how these offsets interact with entry and exit signals, see entry, exit, and timing.

Point-in-time fills

A fill resolves against the quote stream as-of-backward: it uses the latest quote with a timestamp at or before the fill minute. If a minute is missing from the stream, the fill carries the prior quote forward. A stale stream can therefore fill against a quote that is several minutes old, which understates true slippage, since the real market moved while the quote sat still.

Requesting a fill before a contract's first quote raises an error rather than borrowing a later price. The engine will not reach forward in time to price a fill, so there is no look-ahead in the fill path. This is the same point-in-time guarantee the rest of the data layer holds to: a value is only ever built from quotes that existed at or before the moment it is read.

The Backtest.ai fill model in detail

The fill model is calibrated: it predicts where between mid and the far quote an order actually fills, and books that price. For any single fill, the ordering holds by construction: the mid price is at least as good as the modeled price, which is at least as good as the bid_ask price.

That per-fill ordering does not carry over to whole-run P&L. Two feedback effects break it: worse fills lower net liquidation value, which sizes later trades smaller, so less exposure in a drawdown can end higher; and margin-linked deficit triggers fire at different minutes under different modes. So the three modes give you per-fill bounds and three point estimates, not a guaranteed "optimistic / realistic / worst" P&L envelope. Do not read the three equity curves as a band.

Capacity and market impact

A size-aware layer keeps a small order and a very large one from filling identically, and it applies in every fill mode. It has two parts. A lot clamp at sizing time caps how many lots an order may be: it reads participation against average daily volume, open interest, and quoted depth, and caps the order to the most binding of them. An additive market-impact penalty at fill time worsens the booked price as order size grows.

The thresholds in this layer are labeled assumed, not calibrated: nothing in historical data can validate them until enough real fills exist to measure against, so the platform presents them as assumptions rather than validated numbers. Because the clamp reads the same liquidity signals regardless of fill mode, it caps every mode identically. This layer is where the lot count you request meets the size the market could actually absorb; the request itself comes from position sizing and capital.

Commissions

Every backtest charges a fixed per-contract commission in dollars. The charge is configurable and can be set to zero when you want a gross result. On 0DTE and other high-turnover strategies, per-contract commissions can dominate the edge, so compare the zero-commission and commissioned runs before trusting a thin margin.

What the engine models, and what it does not

Item
How it is handled
Spread cost
mid books none, bid_ask books the full spread, the Backtest.ai fill model books a calibrated fraction of it.
Market impact
An additive fill-time penalty that grows with order size. Applies in every fill mode.
Capacity
A lot clamp at sizing time against participation, open interest, and quoted depth. Applies in every fill mode.
Commissions and fees
A fixed per-contract commission in dollars, settable to zero.
Benchmark
Every run carries a benchmark comparison, S&P 500 total return by default.
Partial fills
Not modeled. Fills are all-or-nothing at the resolved lot count.
Fill probability, adverse selection
Not modeled. Every action executes instantly at its minute at the modeled price.

The largest remaining gap for an active strategy is the order lifecycle: a resting order that would sometimes sit unfilled in reality always fills here, instantly and in full. Read a high-frequency result with that in mind.

Exact fixed-point math

Every cash, price, strike, and quantity in the engine is an exact fixed-point number at micro-unit precision (one part in a million), rounded banker's-style (half to even). There is no floating-point accumulation error across a long run: a backtest over ten years of minute bars carries the same exactness at the last fill as the first. Two runs of the same configuration produce byte-identical cash and P&L. Where an inherently imprecise input enters the math, such as implied volatility or a greek, it is multiplied in at the boundary and the result is rounded once, so imprecision never compounds silently.

Was this page helpful?