Commission (spot)
On instrument="spot" (the default), commissionPercent is charged on every fill as a percent of the fill's notional value, on entries and exits alike. Fees reduce equity immediately and are reported per trade and in the totals as stats.feesPaid.
Maker and taker fees (perps)
On instrument="perps", fills are charged makerFeePercent or takerFeePercent instead of commission, by how the fill reached the market:
- Taker (
takerFeePercent): market-crossing fills. In a probed run whose every fill was a market order,takerFeesPaid: 0.7210337119804004andmakerFeesPaid: 0. - Maker (
makerFeePercent): limit-priced fills. A resting limit entry filled later is charged the maker rate. stats.feesPaidis exactlymakerFeesPaid + takerFeesPaidon perps.
//@version=3
// Perps maker-fee routing: a limit-bound entry fill is charged makerFeePercent
// (stats.makerFeesPaid > 0); the market close crosses the market and is charged
// takerFeePercent (stats.takerFeesPaid > 0). feesPaid = maker + taker.
strategy(title="Perps Maker Fee", initialCapital=10000,
instrument="perps", leverage=2,
makerFeePercent=0.1, takerFeePercent=0.05,
funding="off",
qtyType="fixed", qtyValue=1, pyramiding=1);
timeseries data = ohlcv(symbol=currentSymbol, exchange=currentExchange);
timeseries fastMa = sma(source=data.close, period=4);
timeseries slowMa = sma(source=data.close, period=9);
var px = data.close;
if (crossover(fastMa, slowMa)) {
// resting limit just under the market: a later bar trades through it -> maker fill
strategy.entry("Long", "long", limit=px * 0.999);
}
if (crossunder(fastMa, slowMa)) {
strategy.close("Long");
}
plotLine(value=strategy.equity(), width=1, colors=["#2563eb"], label=["Equity"], desc=["Strategy equity"]);Observed: the limit entries were charged the maker rate (makerFeesPaid: 0.7247578990925553), the market closes the taker rate (takerFeesPaid: 0.3378412437642613), and feesPaid: 1.0625991428568167, their exact sum. Note limit= takes a number, not a timeseries; passing data.close directly fails with Type mismatch for 'strategy.entry.limit': expected number, got timeseries, hence the var px = data.close; read.
One fee schedule per instrument
Declaring the other instrument's fee params never double-charges; the engine ignores them and says so in the run diagnostics:
- Perps + nonzero
commissionPercent:STRATEGY_PERPS_COMMISSION_IGNORED: strategy(): 'commissionPercent' is ignored for instrument="perps"; fees come from 'makerFeePercent'/'takerFeePercent'. Observed not charged:feesPaid: 0with maker/taker at their0defaults. - Spot +
makerFeePercent/takerFeePercent:STRATEGY_SPOT_MAKER_TAKER_IGNORED: strategy(): 'makerFeePercent' and 'takerFeePercent' apply only to instrument="perps"; spot uses 'commissionPercent'. Observed: the run charged commission only (feesPaid: 1.442067423960801atcommissionPercent=0.1;makerFeesPaidandtakerFeesPaidboth0). Declare only one of the pair and the message names just that one ('makerFeePercent' applies only to instrument="perps"; spot uses 'commissionPercent').
Funding on perps is a holding cashflow, not a fill cost, and has its own stats and series: see Perps: funding. For the perps fee routing rules in the leverage model's own terms, see also Perps and Leverage.
Flat slippage (the default)
slippageBps applies adversely to every fill that crosses the market: market entries, stop entries, protective stops, and trailing stops. Buys fill at price * (1 + bps/10000), sells mirror. Limit fills, including take-profit legs, are exempt: a limit price is a bound and can fill better but never worse.
Flat slippage is honest about being a constant: it neither grows with your order size nor tightens on liquid pairs. When you want size-aware and pair-aware slippage, opt into the book estimate.
Order-book slippage: slippageModel="bookEstimate"
With bookEstimate, a market-crossing fill walks the recorded order book: it consumes displayed liquidity level by level, so bigger orders pay progressively worse prices, exactly as a real market order would:
buy 5 units against the recorded asks
price displayed size consumed
100.3 #### (4 units) 1 unit <- worst fill
100.2 ## (2 units) 2 units
100.1 ## (2 units) 2 units <- best fill
------------------------------------------
average fill price sits between 100.1 and 100.3,
weighted by what each level suppliedLimit fills stay exempt: a limit price is a bound by definition, so it never slips.
//@version=2
strategy(title="Book Estimate Demo", initialCapital=10000, qtyType="fixed", qtyValue=1, slippageBps=5, slippageModel="bookEstimate")
timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)
timeseries fast = sma(source=bars.close, period=5)
timeseries slow = sma(source=bars.close, period=20)
if (crossover(fast, slow)) {
strategy.entry("L", "long")
}
if (crossunder(fast, slow)) {
strategy.closeAll()
}
plotLine(value=fast, width=1, colors=["#4f8cff"], label=["Fast SMA"], desc=["5-period SMA of close"])
plotLine(value=slow, width=1, colors=["#f59e0b"], label=["Slow SMA"], desc=["20-period SMA of close"])With slippageModel="bookEstimate", backtests load the pair's recorded order-book snapshots (one per chart bar) and price every market-crossing fill by walking that depth:
- The walk. A buy consumes the ask side from the best level down, a sell consumes bids, for the order's quantity. The resulting volume-weighted price versus the best level gives an impact fraction, which is applied to the bar-derived fill price. The book contributes its depth shape, never its absolute price levels, so a modestly stale snapshot degrades gracefully instead of teleporting your fill.
- Size awareness. Reversals walk the full quantity they actually cross (the closing leg plus the opening leg). Fills that land on the same bar share that bar's displayed liquidity: a later fill starts below what earlier fills consumed, and exhausting the visible book sends the fill to the declared fallback rather than pretending depth was infinite.
- Adverse-moment stress. Protective stop and trailing fills consume twice their quantity from the book. They execute into adverse moments, and a calm snapshot understates that cost.
- Limit exemption. Limit fills, including take-profit legs, remain exempt, same as the flat model.
When the book cannot answer
The walk refuses to invent numbers. A fill falls back to the declared slippageBps and is counted when:
- the pair has no recorded book history at all (the run details say so),
- no snapshot covers that bar (gaps, or the most recent minutes of a live chart),
- the order's (stressed) quantity exceeds the visible depth,
- the snapshot's best level sits more than 5% from the fill price (a stale or mismatched book is worse than no book).
This is an estimate by construction: aggregated snapshots cannot express queue dynamics or replenishment. That is exactly why the counts ship next to the number.
Reading the slippage disclosure
The Strategy Tester's run-details popover discloses how slippage was priced:
- "Slippage: order-book depth estimate, avg N bps across M fills": every market-crossing fill was priced from recorded depth.
- "Slippage: order-book depth estimate on N of M fills, declared rate for the rest": partial book coverage over the replay window.
- "Slippage: order-book depth not recorded for this pair, declared rate applied": the model fell back entirely; the result is equivalent to
slippageModel="fixed".
The model is declared in the script, not toggled in the UI, so a shared strategy reproduces the same way for everyone who runs it. Book data loads for plus-tier backtests; pairs and epochs without recorded books degrade to the declared rate with the disclosure above.
Practical guidance
- Set
slippageBpseven when usingbookEstimate: it is your fallback rate wherever the book cannot answer, so make it realistic for the pair. - Size matters now. A strategy that trades 0.1% of equity and one that trades 50x leverage will see very different book impacts on the same signals, which is the point.
- If the disclosure reports mostly fallback fills on a pair you care about, prefer the flat model with a defensible
slippageBpsinstead of an estimate that rarely engaged.