---
title: Slippage and Costs
description: Commission (spot), maker/taker fees (perps), flat slippage, and slippageModel="bookEstimate", which prices fills from recorded order-book depth where the platform has it.
verified:
  engine: 3.0.67
  probes:
    - scripts/probes/strategies/book-estimate.ks
---

## 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.7210337119804004` and `makerFeesPaid: 0`.
- **Maker** (`makerFeePercent`): limit-priced fills. A resting limit entry filled later is charged the maker rate.
- `stats.feesPaid` is exactly `makerFeesPaid + takerFeesPaid` on perps.

```javascript title="perps-maker-fee.ks"
//@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: 0` with maker/taker at their `0` defaults.
- 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.442067423960801` at `commissionPercent=0.1`; `makerFeesPaid` and `takerFeesPaid` both `0`). 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](perps.md#funding). For the perps fee routing rules in the leverage model's own terms, see also [Perps and Leverage](perps-and-leverage.md#maker-and-taker-fees).

## 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:

```text
   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 supplied
```

Limit fills stay exempt: a limit price is a bound by definition, so it never slips.

```javascript title="book-estimate.ks"
//@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 `slippageBps` even when using `bookEstimate`: 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 `slippageBps` instead of an estimate that rarely engaged.
