QQ 301, 8 min read

Fills and costs

Every cost option defaults to off. A run with no cost options fills at the bar open for free. Nothing here is charged unless you set it (or run the "realistic" preset in 2.10).

Units used below: 1 basis point (bp) = 0.01% = 0.0001 of price.

2.1 Order in which costs are applied

For each fill, in this order:

  1. Start from the fill price: the named bar's open, or the script's explicit price.
  2. slippage_bps moves it against the trader.
  3. Spread (fixed or from data) plus volatility slippage move it again.
  4. Square-root impact moves it once more (it depends on size, so it is applied while the account is being sized).
  5. Commission is charged on the notional, in cash, not in the price.
  6. Financing is charged in cash for the time the position is held.

Each price move is multiplicative. For a long entry:

fill = open × (1 + slippage_bps/10000) × (1 + spread + vol_slip) × (1 + impact)

A long exit and a short entry use (1 − …); a short exit uses (1 + …).

2.2 Spread

OptionMeaningDefaultUnits
spread_bpsHalf the bid/ask spread, paid on every fill. A round trip pays twice this.0bps of price
spread"data" takes the half-spread from the bars' own bid and ask columns. Absent (or "bps") means use spread_bps.absent
spread_data_scaleMultiplies the data half-spread. Cost stress uses it.1multiple

Fixed: half = spread_bps / 10000.

From data, per bar: half = (ask − bid) / (ask + bid), which is half the spread as a fraction of the mid. A bar whose quote is missing, non-positive or crossed reuses the last good quote (never a later one). A fill on a bar before the first good quote is an error.

spread: "data" together with a non-zero spread_bps is an error, because it would charge the spread twice.

Note: none of the data providers shipped with the terminal pass bid/ask columns to the engine, so spread: "data" is reachable only when the engine is given a DataFrame that has them.

2.3 Slippage

OptionMeaningDefaultUnits
slippage_bpsMoves every fill against the trader. Applies to explicit prices too. Charged in every slippage model, as a fixed floor.0bps of price
slippage_model"fixed", "vol" or "sqrt_impact"."fixed"
slippage_kOnly with "vol": slippage per fill = k × bar volatility.0.1multiple of σ
slippage_vol_barsOnly with "vol": bars in the volatility window. Whole number, at least 2.20bars

"fixed" charges slippage_bps and nothing else.

"vol" adds, at each fill bar:

vol_slip = slippage_k × σ_bar
σ_bar    = sample standard deviation of the last N close-to-close log returns

σ_bar is the value as it stood before the fill bar opened (the rolling series is shifted one bar). Before N returns exist it uses all returns so far; before two returns exist it is 0, so the earliest bars pay no volatility slippage. vol_slip is a fraction of price, not bps.

Giving slippage_k, slippage_vol_bars or impact_c with a model that does not read them is an error, not silently ignored.

2.4 Square-root market impact

With slippage_model: "sqrt_impact":

impact = min(0.5, impact_c × σ_daily × sqrt(notional / ADV$))
TermHow it is computed
impact_cOption. Default 1.0.
σ_dailyσ_bar over a window of max(20, round(20 × bars_per_day)) bars, times sqrt(bars_per_day).
ADV$Mean of volume × close per bar over the same window (bars with zero volume or price are left out; at least one bar is enough), times bars_per_day.
bars_per_day86400 / median bar spacing in seconds, measured on the first 20,000 bars.
notionalAt entry: the free cash balance for a default-sized trade, or qty × entry price for an explicit size. At exit: exit price × qty.

Both estimates are the values known before the fill bar. The result is capped at 0.5 (50% of price). A fill with no traded volume before it is an error. A dataset without a volume column cannot use this model. A forced liquidation at ruin (2.9) is not charged impact.

The code's own caveat: the volume column is taken as units traded. On FX and CFD data it is usually tick count, "which makes the answer an order of magnitude, not a quote".

2.5 Latency

OptionMeaningDefaultUnits
latency_barsEvery entry and exit moves N bars later and fills at that bar's open.0whole bars

With latency on, a price the script wrote down is dropped (it belonged to the bar the decision was made on). An entry pushed past the last bar never fills and the trade is dropped. An exit pushed past the last bar closes on the last bar.

2.6 Financing

OptionMeaningDefaultUnits
financing_bps_annualAnnual carry on notional held, both sides.0bps per year
financing_long_bpsOverrides it for longs.value of financing_bps_annualbps per year
financing_short_bpsOverrides it for shorts.value of financing_bps_annualbps per year

A negative rate is a credit. For each bar i the position is held (from the entry bar up to, not including, the exit bar):

charge_i = close_i × qty × (rate_bps / 10000) × (ts[i+1] − ts[i]) / (365.25 × 86400)

The charge is booked on bar i + 1. close_i is the last positive close (the entry fill price before any close exists). Time is wall-clock, so a position held over a weekend pays for the weekend. A trade that enters and exits on the same bar pays no financing.

2.7 Commission

OptionMeaningDefaultUnits
commission_pctCharged on the notional of each fill, entry and exit.0percent of notional per side (0.01 = 1 bp)
entry fee = entry fill × qty × commission_pct / 100     (cash, on the entry bar)
exit fee  = exit fill  × qty × commission_pct / 100     (cash, on the exit bar)

2.8 Capital, sizing and size_scale

OptionMeaningDefaultUnits
capital (or initial_capital)Starting cash. Must be a positive finite number.100,000account currency
size_scaleMultiplies every explicit qty. Must be above 0. Used by the capacity check.1multiple

Sizing at each entry:

free = max(0, cash − committed)
qty  = trade's qty                      if the script gave one
qty  = free / entry fill price          otherwise

cash is the starting capital plus every closed P&L and fee so far. committed is the entry notional of positions still open. So:

  • With no qty, each trade uses the whole free balance and the account compounds trade to trade.
  • Two overlapping default-sized trades cannot spend the account twice: the second sees nothing free and is sized at zero.
  • An explicit qty is taken as given. Nothing checks it against the account, so explicit sizes can be leveraged.

On one bar, exits of trades opened earlier are settled first, then entries, then exits of trades opened on that same bar.

Per-trade result:

gross   = (exit − entry) × qty            long
gross   = (entry − exit) × qty            short
pnl     = gross − entry fee − exit fee − financing
pnl_pct = pnl / (entry fill × qty) × 100

pnl_pct is the return on the trade's own notional, not on the account.

2.9 Ruin and liquidation

The equity curve marks every open position to each bar's close. The first bar on which marked equity is at or below zero is the ruin bar. Then:

  • every position open on that bar is closed there at the mark (that bar's close, or the last close that printed), with commission and financing up to that bar but no further fill costs;
  • no trade that enters after the ruin bar is filled;
  • nothing before the ruin bar changes, and equity stays where the liquidation left it to the end.

The stats report ruined (true/false), ruinBar (bar index) and ruinTs.

There is no margin model before that point: no margin call, no stop-out at a maintenance level. The only liquidation rule is equity reaching zero.

2.10 The "realistic" preset

There is a preset, used by the execution checks in the institutional suite (4.5). It is not a run option you set by name; the checks build it from your options. It keeps your commission, slippage_bps and capital and adds:

ComponentPreset value
SpreadA half-spread by asset class (table below), unless you set your own spread_bps or spread: "data". If every bar has a usable bid/ask quote and you set no spread_bps, the data spread is used.
Slippageslippage_model: "vol", slippage_k: 0.1, unless you already chose "vol" or "sqrt_impact".
Latencylatency_bars: 1, unless yours is already 1 or more.
Financing500 bps a year long, 300 bps a year short, unless you set any financing option.

Half-spread by asset class, in bps of price per fill:

ClassHalf-spreadClassHalf-spread
Major FX pair0.75Index CFD / future1.0
FX cross / minor1.5Large-cap stock1.5
Gold1.5Crypto4.0
Silver / other metal3.0Unclassified2.0
Energy CFD3.0

The asset class is guessed from the symbol name alone (for example anything containing XAU or GOLD is gold; one of seven listed pairs is a major). The code describes these as "conservative defaults by asset class, not venue quotes".

Source. Option parsing and defaults: backtest/runner.py:823-962 (_fill_model; spread 890-894, 920-925; model 895-909; latency 910-913; financing 914-916; size_scale 917-919; vol 948-959; impact 960-961). slippage_bps: backtest/runner.py:799-821. Data half-spread: backtest/runner.py:964-977. Bar volatility: backtest/runner.py:985-1003. Impact: backtest/runner.py:1005-1050, cap at 862, bars_per_day at 979-983. Applying spread and vol slippage, size_scale: backtest/runner.py:1052-1084. Latency in _normalize: backtest/runner.py:693-701. Financing: backtest/runner.py:1086-1105. Capital and commission parsing: backtest/runner.py:368-378. Sizing, fees, P&L, event order: backtest/runner.py:1109-1212. Ruin: backtest/runner.py:412-417, backtest/runner.py:434-466. Preset: backtest/execution.py:47-74, backtest/execution.py:149-192; asset class: backtest/execution.py:76-146. Guaranteed by tests: tests/test_execution.py:87-260 (spread hand-computed, data spread, vol slippage scaling, latency shifting by exactly N bars, financing over a known holding time including a weekend, impact scaling with the square root of size, preset values).