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:
- Start from the fill price: the named bar's open, or the script's explicit price.
slippage_bpsmoves it against the trader.- Spread (fixed or from data) plus volatility slippage move it again.
- Square-root impact moves it once more (it depends on size, so it is applied while the account is being sized).
- Commission is charged on the notional, in cash, not in the price.
- 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
| Option | Meaning | Default | Units |
|---|---|---|---|
spread_bps | Half the bid/ask spread, paid on every fill. A round trip pays twice this. | 0 | bps 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_scale | Multiplies the data half-spread. Cost stress uses it. | 1 | multiple |
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
| Option | Meaning | Default | Units |
|---|---|---|---|
slippage_bps | Moves every fill against the trader. Applies to explicit prices too. Charged in every slippage model, as a fixed floor. | 0 | bps of price |
slippage_model | "fixed", "vol" or "sqrt_impact". | "fixed" | |
slippage_k | Only with "vol": slippage per fill = k × bar volatility. | 0.1 | multiple of σ |
slippage_vol_bars | Only with "vol": bars in the volatility window. Whole number, at least 2. | 20 | bars |
"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$))
| Term | How it is computed |
|---|---|
impact_c | Option. 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_day | 86400 / median bar spacing in seconds, measured on the first 20,000 bars. |
notional | At 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
| Option | Meaning | Default | Units |
|---|---|---|---|
latency_bars | Every entry and exit moves N bars later and fills at that bar's open. | 0 | whole 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
| Option | Meaning | Default | Units |
|---|---|---|---|
financing_bps_annual | Annual carry on notional held, both sides. | 0 | bps per year |
financing_long_bps | Overrides it for longs. | value of financing_bps_annual | bps per year |
financing_short_bps | Overrides it for shorts. | value of financing_bps_annual | bps 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
| Option | Meaning | Default | Units |
|---|---|---|---|
commission_pct | Charged on the notional of each fill, entry and exit. | 0 | percent 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
| Option | Meaning | Default | Units |
|---|---|---|---|
capital (or initial_capital) | Starting cash. Must be a positive finite number. | 100,000 | account currency |
size_scale | Multiplies every explicit qty. Must be above 0. Used by the capacity check. | 1 | multiple |
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
qtyis 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:
| Component | Preset value |
|---|---|
| Spread | A 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. |
| Slippage | slippage_model: "vol", slippage_k: 0.1, unless you already chose "vol" or "sqrt_impact". |
| Latency | latency_bars: 1, unless yours is already 1 or more. |
| Financing | 500 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:
| Class | Half-spread | Class | Half-spread |
|---|---|---|---|
| Major FX pair | 0.75 | Index CFD / future | 1.0 |
| FX cross / minor | 1.5 | Large-cap stock | 1.5 |
| Gold | 1.5 | Crypto | 4.0 |
| Silver / other metal | 3.0 | Unclassified | 2.0 |
| Energy CFD | 3.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).