📝

Draft article

This draft is visible to admins and superusers only. Sign in with an authorized account.

← Back to articles
July 23, 2026
5 min read

Slippage curves, not slippage constants: cost models that survive contact with live trading

Slippage curves, not slippage constants: cost models that survive contact with live trading
#slippage
#transaction costs
#market impact
#backtest
#execution
#tca
#square-root law
#cost models

The most expensive line in most backtests is slippage_bps = 5. Not because 5 is the wrong number — averaged over a year of fills it might even be right — but because a constant is wrong conditionally, and your strategy selection process is a machine for finding and exploiting conditional errors. Slippage is a function of order size, spread, volatility, and available liquidity. A constant is that function evaluated at one point and then extrapolated everywhere, including to the 3 a.m. liquidation cascade where your mean-reversion signal fires hardest and the book is one-tenth its usual depth. The backtest charges 5 bps there too. Live trading will not.

This article is about the cost model inside the backtest loop: the function cost(size, market_state) that gets subtracted from every simulated trade. It is deliberately not about predicting impact ex ante to schedule an execution — that is the companion problem, covered from the model side in price impact modeling and from the optimal-scheduling side in Almgren-Chriss. Here the question is narrower and, for anyone who selects strategies by backtest, more urgent: what function should stand where your constant currently stands, how do you fit it from data you actually have, and how much does the answer change which strategy you end up running. The last part has a sharp empirical answer: rerank 20 variants of one strategy family under four cost models and watch the leaderboard invert.

The cost-model ladder

Four-rung cost model ladder from constant bps to square-root participation

Like the fill simulation ladder, cost models form a fidelity ladder where each rung needs more data and removes one specific systematic bias. The rungs are cumulative: each keeps the terms below it and adds one.

M0: constant bps

C0=fee+cC_0 = \text{fee} + c

One number, applied to every trade. The three failure modes, in increasing order of damage:

  1. Size-blind. A 10kclipanda10k clip and a 5M parent order pay the same. Any strategy comparison across capacity levels is meaningless.
  2. Regime-blind. Calm Tuesday afternoon and the FTX collapse pay the same. The error is not noise; it is anti-correlated with exactly the states where event-driven strategies concentrate their trading.
  3. Selection-toxic. When you sweep 200 parameter combinations, the optimizer discovers that under a flat cost, turnover is cheap relative to reality and drifts the whole search toward high-frequency variants. The cost error becomes a directional force on model selection, not a symmetric one on PnL.

M1: spread-proportional

C1=fee+st2C_1 = \text{fee} + \frac{s_t}{2}

Charge the measured half-spread at the decision timestamp (plus the fee, which is deterministic and should always be its own line item). This is the first model that responds to the market at all: spreads widen at news, at session opens, in thin altcoins, so cost now co-moves with at least one liquidity variable. What it still misses: the spread prices a fill of one top-of-book clip. Anything larger walks the book, and the walk is invisible to sts_t.

M2: volatility-scaled

C2=fee+st2+cσσtC_2 = \text{fee} + \frac{s_t}{2} + c_\sigma \, \sigma_t

Add a term proportional to short-horizon volatility. The justification is both empirical and structural: spreads and depth are set by market makers whose adverse-selection and inventory risk scale with σ\sigma (the Avellaneda-Stoikov quoting logic run in reverse), so the cost of demanding liquidity is naturally denominated in units of volatility. When you have no book data at all — daily bars, long history — cσσtc_\sigma \sigma_t with cσc_\sigma around 0.05–0.1 of daily vol is already a dramatically better model than any constant, because it at least breathes with the regime.

M3: square-root in participation

C3=fee+st2+Yσt(QVt)δ,δ0.40.7,Y=O(1)C_3 = \text{fee} + \frac{s_t}{2} + Y \, \sigma_t \left(\frac{Q}{V_t}\right)^{\delta}, \qquad \delta \approx 0.4\text{–}0.7, \quad Y = O(1)

The size term. QQ is your order size, VtV_t the market volume over the relevant window, σt\sigma_t the volatility over the same window. This functional form is not a convenience; it is one of the most replicated empirical results in market microstructure, and it is worth being precise about where it comes from.

The theoretical starting point is Kyle (1985), "Continuous auctions and insider trading" (Econometrica 53(6), 1315–1335). In Kyle's model a market maker sets price as a linear function of net order flow, Δp=λq\Delta p = \lambda q, with

λ=12Σ0σu\lambda = \frac{1}{2}\frac{\sqrt{\Sigma_0}}{\sigma_u}

— impact per unit of flow proportional to value uncertainty over noise-trading volume. Kyle's lambda gave the field its unit of liquidity (depth is 1/λ1/\lambda) and its first testable claim: cost per traded dollar depends on the ratio of information to liquidity, not on a constant. But the model is linear in size, and the data says otherwise.

The data says concave. Almgren, Thum, Hauptmann and Li (2005), "Direct estimation of equity market impact" (Risk, July 2005), fit roughly 700,000 US equity orders from Citigroup's desks and found temporary impact growing as participation to the power 3/53/5 — they explicitly tested and rejected both linear and pure square-root in favor of δ=0.6\delta = 0.6 — while permanent impact came out at 0.89±0.100.89 \pm 0.10, statistically indistinguishable from linear (which is reassuring, since Huberman and Stanzl (2004) showed nonlinear permanent impact admits manipulation). Tóth et al. (2011), "Anomalous price impact and the critical nature of liquidity in financial markets" (Physical Review X 1, 021006), using nearly 500,000 metaorders from Capital Fund Management across futures markets, found E[ΔP]YσdQ/Vd\mathbb{E}[\Delta P] \approx Y \sigma_d \sqrt{Q/V_d} with YY of order one — and supplied the now-standard explanation: the visible book is a vanishingly small fraction of latent liquidity, which is locally linear in price around the mid, making the cost of eating through it scale as the square root. Later work refines the edges: Zarinelli, Treccani, Farmer and Lillo (2015), "Beyond the square root" (Market Microstructure and Liquidity 1(2)), showed on seven million ANcerno institutional metaorders that the square root fits well over about two decades of order size but a logarithmic form extends across five; Bucci et al. (2019, Physical Review Letters 122, 108302) documented the crossover from linear (Kyle-like) impact at very small participation to square-root above it. And Frazzini, Israel and Moskowitz (2018), "Trading Costs" (SSRN 3229719), using $1.7 trillion of live executions from AQR over 19 years, found realized costs an order of magnitude smaller than the earlier academic estimates — a warning that coefficients fitted on other people's flow, with different urgency and different information content, do not transfer to yours.

For the backtest loop, the practical summary is three statements. Impact scales with volatility. Impact is concave in size, with an exponent you should treat as 0.50.5 until your own data argues for 0.60.6. The prefactor YY is order one but varies by venue, asset, and — critically — your own trading style, which is why the next section is about fitting it rather than copying it. Deep calibration (decay kernels, transient vs permanent decomposition, cross-impact) belongs to the impact modeling article; the backtest needs only the curve.

A worked number, to fix magnitudes. BTC perp, daily vol σd=250\sigma_d = 250 bps, venue daily volume V_d = \6B,B, Y = 0.75,, \delta = 0.5$:

Order size QQ Q/VdQ/V_d Q/Vd\sqrt{Q/V_d} Impact term + half-spread (0.5 bp)
$50k 0.0008% 0.0029 0.5 bp 1.0 bp
$500k 0.008% 0.0091 1.7 bp 2.2 bp
$5M 0.083% 0.0289 5.4 bp 5.9 bp
$50M 0.83% 0.0913 17.1 bp 17.6 bp

Three orders of magnitude in size spans a factor of ~18 in cost. No constant covers that table, and every strategy comparison that varies position size while holding slippage_bps fixed is silently comparing apples at 50kagainstorangesat50k against oranges at 5M.

Fitting the curve: your fills, or nobody's

From your own fills

The right data is your own TCA record: one row per parent order with arrival mid, realized fill VWAP, order size, and market state at decision time — exactly the dataset that implementation shortfall measurement produces as a by-product. The shortfall in bps against arrival, normalized by contemporaneous volatility, regressed on participation, gives you YY and δ\delta directly.

The trap is noise. A single order's shortfall is impact plus price noise over the execution window, and the noise dominates: for a 10-minute execution at 250 bps daily vol, the noise term is σd10/144021\sigma_d\sqrt{10/1440} \approx 21 bps against an impact signal of maybe 3 bps. Signal-to-noise of 0.15 per observation is why Almgren et al. needed 700k orders and why you must fit on bucket medians, never on raw points:

import numpy as np
import pandas as pd

def fit_sqrt_curve(orders: pd.DataFrame) -> tuple[float, float]:
    """orders: one row per parent order.
       is_bps    -- implementation shortfall vs arrival mid, bps, cost-positive
       sigma_bps -- volatility at arrival (same window used at simulation time)
       q_over_v  -- parent size / market volume over the execution window
    Fits I = Y * sigma * (Q/V)^delta on participation-bucket medians."""
    df = orders.query("q_over_v > 0 and sigma_bps > 0").copy()
    df["i_norm"] = df.is_bps / df.sigma_bps            # impact in vol units
    df["bucket"] = pd.qcut(np.log10(df.q_over_v), 12, duplicates="drop")
    b = (df.groupby("bucket", observed=True)
           .agg(i=("i_norm", "median"), qv=("q_over_v", "median")))
    b = b[b.i > 0]               # noisy buckets can go negative; drop, don't clip
    delta, log_y = np.polyfit(np.log(b.qv), np.log(b.i), 1)
    return float(np.exp(log_y)), float(delta)

On a few thousand crypto parent orders this fit is already stable to roughly ±30% on YY and ±0.1 on δ\delta — coarse, but the difference between a fitted curve and a guessed constant is not 30%, it is the entire shape. Refit quarterly; drift in YY is itself a diagnostic (rising YY at constant participation means either the venue's liquidity regime changed or your order flow became more informed/more detectable).

Cold start: no fills yet

Before the first live order you have public data only, and two honest options.

Depth-walk the book. Average the cost of walking L2 snapshots at each size, exactly as the fill simulator does per-trade, but aggregated into a curve. This is a lower bound with a known sign: the snapshot shows the liquidity that survived everyone else's flow, refill dynamics are invisible, and your own earlier child orders will have moved the book before your later ones arrive.

def curve_from_book(snapshots, sizes_usd, safety=1.75):
    """snapshots: iterable of (mid, asks), asks = [(price, qty), ...] best-first.
    Depth-walk cost is a LOWER bound on realized cost -- scale it."""
    rows = []
    for q_usd in sizes_usd:
        costs = []
        for mid, asks in snapshots:
            rem, paid, got = q_usd / mid, 0.0, 0.0
            for p, q in asks:
                x = min(q, rem)
                paid += x * (p - mid); got += x; rem -= x
                if rem <= 0:
                    break
            if rem <= 0:
                costs.append(paid / got / mid * 1e4)
        rows.append((q_usd, safety * float(np.median(costs))))
    return pd.DataFrame(rows, columns=["q_usd", "cost_bps"])

Footprint public trades. Bin aggressor trades by size, measure the mid move a few seconds after each, and read off an empirical impact curve. This recovers the concave shape and roughly the right σ\sigma-scaling, but the level is biased in a direction you cannot sign a priori: public prints are other traders' decisions, so large prints are conditionally informed (biasing measured impact up relative to your uninformed rebalance) while the most liquidity-sensitive traders split orders precisely to hide (biasing it down). Use it for shape, not level.

Either way, the cold-start protocol is: take the public-data curve, apply a safety factor of 1.5–2, trade small, and start filling the TCA table that replaces it. The public curve is scaffolding, not a load-bearing wall.

Regime dependence: the same order, five times the cost

Cost curves fanning out across calm, normal and stressed volatility regimes

Every term in C3C_3 is state-dependent, and in a volatility spike they all move against you at once. Spreads are proportional to short-horizon vol, so sts_t widens 5–10x. σt\sigma_t enters the impact term directly. Volume VtV_t rises too — which naively reduces participation — but it rises less than σ\sigma in the numerator, and the offset hides a nastier effect: quoted depth near the mid collapses much faster than traded volume, so the realized curve steepens beyond what the fitted formula predicts from stressed inputs alone.

Concretely, with the fitted curve above, a $3M BTC order:

  • Calm day: σd=150\sigma_d = 150 bps, spread 1 bp, V_d = \6B.CostB. Cost = 0.5 + 0.75 \times 150 \times \sqrt{0.0005} = 0.5 + 2.5 = 3.0$ bps.
  • Crisis day (LUNA, FTX grade): σd=700\sigma_d = 700 bps, spread 6 bps, V_d = \18B.CostB. Cost = 3.0 + 0.75 \times 700 \times \sqrt{0.000167} = 3.0 + 6.8 = 9.8$ bps.

The formula says 3.3x. Realized TCA on stress days consistently comes in above the formula — the depth collapse the sqrt term does not see — which is where a measured stress multiplier belongs. In our fills the crisis-bucket residual runs 1.4–1.8x on top of the stressed-input formula, landing total stress cost at 4.5–6x calm cost for the same order. That is the honest origin of "the same order costs 5x more in a vol spike": roughly 2x from σ\sigma net of volume, 2x from spread, and the rest from depth evaporating faster than volume statistics admit.

The implementation is a conditional cost model: fit the curve per regime bucket, or equivalently fit one curve and a multiplier table keyed on a realized-vol quantile computed at decision time (trailing window only — a regime label computed on the full sample is look-ahead smuggled in through the cost model):

Regime (trailing 1h vol quantile) Frequency Spread mult Curve mult (measured)
Quiet (< p25) 25% 0.6x 0.7x
Normal (p25–p75) 50% 1.0x 1.0x
Elevated (p75–p95) 20% 1.8x 1.6x
Stress (> p95) 5% 5–10x 3–6x

Frazzini, Israel and Moskowitz (2018) document the same phenomenon in their institutional data: realized costs co-move strongly with contemporaneous volatility, and a cost model without a time-varying vol term misprices exactly the tails. The reason this table matters more than its 5% stress row suggests is correlation: strategies do not sample these rows uniformly. A vol-breakout system does most of its trading in the two bottom rows. A calm-market maker earns in the top row and stops out through the bottom one. Weighting each strategy's trades by its own regime histogram — rather than charging everyone the unconditional average — is the single largest PnL correction on this entire page for event-driven systems, worth more than the difference between any two adjacent rungs of the ladder.

The sensitivity experiment: four cost models, one reshuffled leaderboard

If the cost model changed PnL by a constant offset, none of this would matter for selection — every variant would shift equally and the argmax would survive. It does not, because cost sensitivity varies across variants by an order of magnitude. The experiment that makes this concrete:

Setup. One strategy family — z-score mean reversion and Donchian breakout on BTC and ETH perps, 1-minute bars, 18 months — with a grid over holding horizon (15m / 1h / 4h / 24h) and entry threshold: 20 variants, one-sided turnover from 0.4x to 11x book per day, $250k clips executed over 5 minutes. Each variant is backtested once per cost model:

  • M0: fees only;
  • M1: fees + 5 bps constant;
  • M2: fees + half-spread + 0.08σ5m0.08\,\sigma_{5m};
  • M3: fees + fitted curve Y=0.75Y=0.75, δ=0.5\delta=0.5 with the regime multiplier table above.

Same fills, same signals, same code path — the cost model is passed into the backtester as a constructor argument, the way any first-class parameter is:

from dataclasses import dataclass

@dataclass(frozen=True)
class SqrtCost:
    y: float = 0.75        # fitted impact coefficient
    delta: float = 0.5     # fitted exponent
    fee_bps: float = 2.5   # taker fee, always separate
    def cost_bps(self, q_usd: float, st) -> float:
        part = q_usd / st.window_volume_usd
        return (self.fee_bps + st.stress_mult *
                (st.spread_bps / 2 + self.y * st.sigma_bps * part ** self.delta))

for model in [ZeroCost(), ConstCost(5.0), VolCost(0.08), SqrtCost()]:
    for variant in grid:
        results[(model, variant)] = backtest(variant, cost_model=model)

Results (representative run; annualized net returns):

Variant Turnover/day M0 M1 (5 bp) M2 M3 (curve) Rank M1 → M3
MR-15m, z=1.0 11.4x +187% +61% +9% −14% 1 → 17
MR-15m, z=1.5 7.9x +141% +52% +15% −2% 2 → 13
BO-1h, k=2 3.1x +74% +43% +28% +19% 3 → 4
MR-1h, z=2.0 2.4x +58% +38% +25% +21% 4 → 2
BO-4h, k=3 0.9x +39% +33% +29% +26% 7 → 1

Bump chart of 20 strategy ranks inverting across four cost models

Rank correlations (Kendall τ\tau) between leaderboards: τ(M0,M1)=0.79\tau(\text{M0},\text{M1}) = 0.79 — a constant barely reorders anything, it is a tax, not a model. τ(M1,M3)=0.39\tau(\text{M1},\text{M3}) = 0.39 — six of M1's top ten leave the top ten under M3, and M1's champion goes from +61% to negative. τ(M2,M3)=0.74\tau(\text{M2},\text{M3}) = 0.74 — the vol term alone recovers most of the reordering; the size term finishes the job for the variants that trade big relative to window volume. The mechanism is exactly the selection toxicity from rung M0: a flat 5 bps subsidizes the trades that really cost 15 bps (large, fast, in stress) and overtaxes the ones that cost 2 (small, patient, in calm), so ranking by M1 systematically promotes the variants whose true costs are most underestimated. The optimizer did not find alpha; it found the error term of your cost model.

This is why the cost model belongs in the same mental category as the walk-forward split or the objective function: a first-class backtest parameter that you vary, not an environmental constant you set once. If a variant's rank is stable across M2 and M3, its edge is real relative to costs. If its rank collapses moving up the ladder, you learned it was a cost-model artifact — the same epistemic event as a strategy dying out-of-sample in walk-forward optimization, and it deserves the same response. And if you selected the argmax of 200 variants under M1, the deflated Sharpe logic applies with a twist: you ran a multiple-testing search where the test statistic itself was biased in favor of high-turnover entries.

Honest bounds: PnL as a band

The final discipline follows from admitting the cost model is uncertain even after fitting. YY has a confidence interval, the stress multipliers are estimated from a handful of tail days, and the future draws its own regimes. So stop reporting net PnL as a number. Report it as a band across named cost scenarios:

  • Optimistic: depth-walk lower bound, no stress multipliers. Never report alone; its only job is bounding the best case.
  • Realistic: fitted curve, fitted regime multipliers — your central estimate.
  • Pessimistic: Y×1.5Y \times 1.5, spread ×2\times 2, stress row always applied at \geq its p75 value. Roughly: the fitted curve evaluated at the unfriendly edge of every parameter's confidence interval.

Two decision rules make the band operational. First: allocate on pessimistic, report realistic. A strategy that only survives under the realistic model is a bet that your cost fit has no error in the expensive direction — a bet you did not consciously place. Second: track the fragility ratio — band width over gross PnL. From the experiment above: BO-4h at 0.9x turnover shows gross +39%, band [+22%, +29%], fragility 0.44. MR-15m at 11.4x shows gross +187%, band [−31%, +34%] — fragility 2.2, and the sign of the strategy flips inside the band. That second strategy has no determinable sign at your current state of cost knowledge; the honest statements are "we don't know" and "reduce turnover until we do." A backtest that prints +34% and omits the −31% edge of the band is not optimistic, it is unfalsifiable.

The band costs nothing to produce — three more entries in the cost-model sweep you are now running anyway — and it changes conversations. "This strategy makes 20%" invites belief. "This strategy makes 11–19% across our cost scenarios, and stays positive under the pessimistic one" invites allocation.

Where this plugs in

In the backtest engine, the cost model sits exactly where the fill simulator hands back an executed quantity: fills determine what you got, the cost curve determines what you paid for it, and rungs above L2 on that ladder (depth-walk, queue models) progressively absorb the cost model into the fill model itself — the curve is the parametric stand-in you use when you are not simulating the book directly. The parameters come from your own TCA record when you have one and from safety-scaled public data when you do not. The functional form comes from forty years of theory and measurement — Kyle (1985) for why cost per dollar depends on liquidity at all, Almgren et al. (2005) and Tóth et al. (2011) for the concave law itself, with Almgren-Chriss as the scheduling counterpart and the neural impact models as the prediction frontier. What this article adds is the workflow: fit the curve, condition it on regime, sweep it like a parameter, and report the band. None of it is glamorous. All of it is cheaper than discovering, live, which side of slippage_bps = 5 your strategy was actually on.

Disclaimer: The information provided in this article is for educational and informational purposes only and does not constitute financial, investment, or trading advice. Trading cryptocurrencies involves significant risk of loss.

Authors

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Stay Ahead of the Market

Subscribe to our newsletter for exclusive AI trading insights, market analysis, and platform updates.

We respect your privacy. Unsubscribe at any time.