Guides

How much slippage should a crypto backtest assume? Measure it for your order size

Take the slippage figure from the tape, by order size, instead of guessing one number. Look at the bad fills, not just the median.

Why it happens

The usual answer is a guess: 1 bp, 5 bps, "the spread". The cost of a taker order depends on its size compared with the trades around it, and it changes from minute to minute. The quickest honest answer is to measure it from the trades themselves: place the same order at a few hundred random moments in a day and replay each one against the prints that followed.

Read the 90th percentile as well as the median. Over a few seconds, price drifts in your favour about as often as against you, so the median can sit close to zero. The cost a strategy feels is in the tail, and the tail widens as the order grows. Measure on several days, including a volatile one, before trusting the number.

Fix it with plain pandas

import numpy as np

rng = np.random.default_rng(0)
buys = ticks[ticks["side"] == 1]
px, q = buys["price"].to_numpy(), buys["qty"].to_numpy()
starts = rng.integers(1, len(buys) - 5000, size=300)     # 300 random moments in the day

for size in (0.1, 1, 5, 25):                              # order size in BTC
    costs = []
    for i in starts:
        p, s = px[i:i + 5000], q[i:i + 5000]
        before = np.concatenate([[0.0], np.cumsum(s)[:-1]])
        used = np.where(before < size, s, 0.0)             # whole prints until the size is covered
        costs.append(((p * used).sum() / used.sum() / px[i - 1] - 1) * 1e4)
    print(f"{size:>5} BTC buy: median {np.median(costs):.1f} bps, 90th pct {np.percentile(costs, 90):.1f} bps")

Or with Deptho

Deptho replays a whole grid of order sizes at once. Feed the measured figure into the backtest's cost attribution, and the tearsheet shows slippage next to fees and funding:

import numpy as np
import pandas as pd
import deptho as dp

t = ticks.index[np.random.default_rng(0).integers(100, len(ticks) - 100, size=300)]
grid = pd.concat([pd.DataFrame({"side": 1, "qty": size}, index=t) for size in (0.1, 1, 5, 25)])
cost = dp.slippage_bps(dp.simulate_taker_fills(ticks, grid))
print(cost.groupby("qty")["slippage_bps"].describe(percentiles=[0.5, 0.9]))

# Then backtest with the number you measured for your size, not a guess. Use the mean:
# a flat per-trade cost has to carry the tail, and the median can sit near zero.
pnl = dp.pnl_from_positions(prices, positions, funding=funding, fee_bps=5,
                            slippage_bps=cost.loc[cost["qty"] == 1, "slippage_bps"].mean())

Working with files bigger than RAM? Deptho Pro memory-maps them.

Related