Strasmore Research
Deep Dives · Matt ConnorBy Matt Connor ·

How Much Slippage to Assume in a Backtest

How much slippage does a backtest need to assume? Half the quoted spread is a floor. Here is why that floor is optimistic and how to measure the real number.

How much slippage to assume in a backtest has one defensible starting point: half the quoted bid ask spread at the instant the strategy made its decision. Treat that as a floor rather than an estimate. It understates the true cost for orders of real size and for options, and the number worth trusting comes from measuring your own fills, then feeding the measured distribution back into the test in place of a constant.

What slippage measures, exactly

Slippage is the gap between the price a backtest assumed and the price a live order would have received. Two reference points make the definition precise. The first is the decision price: the midpoint between the best bid and the best offer at the moment the signal fired. The second is the fill price. Realized slippage is fill minus decision mid, signed with the worse price counted as a cost, and expressed in basis points of the decision mid. One basis point is one hundredth of one percent, so a single penny on a $100 stock is one basis point.

Commission sits outside this. It is known in advance and trivial to model. Slippage is the part the market decides. A market order to buy crosses the bid ask spread and prints at or near the offer, half a spread above the mid. That mechanic is the whole basis of the half spread floor. A resting limit order can collect the spread instead of paying it, at the cost of queue risk and signals that never fill, a separate problem covered in estimating queue position.

How much slippage to assume in a backtest: start at half the spread

The floor is measurable for any name over any window. The panel below takes six household tickers across one midday hour on a pinned September 2026 date and averages the quoted spread in cents next to half of it in basis points of the mid.

QueryQuoted spread and the half spread floor, six household names, one midday hour
tickeravg_spread_centshalf_spread_bps
JNJ17.923.36
MSFT12.821.3
KO1.220.69
AAPL4.540.68
NVDA1.570.36
SPY1.710.11
The exact SQL behind every number
SELECT
    ticker,
    round(avg(toFloat64(ask_price - bid_price)) * 100, 2)                                      AS avg_spread_cents,
    round(avg(toFloat64(ask_price - bid_price) / toFloat64(ask_price + bid_price)) * 10000, 2) AS half_spread_bps
FROM global_markets.cache_stocks_quotes
WHERE ticker IN ('SPY', 'AAPL', 'MSFT', 'NVDA', 'KO', 'JNJ')
  AND sip_timestamp >= toDateTime('2026-09-16 14:00:00', 'UTC')
  AND sip_timestamp <  toDateTime('2026-09-16 15:00:00', 'UTC')
  AND bid_price > 0
  AND ask_price > bid_price
GROUP BY ticker
ORDER BY half_spread_bps DESC
Run this yourself

Sorted widest first, the half spread ran 3.36 basis points on JNJ and 0.11 basis points on SPY. In cents, those same two names quoted 17.92 and 1.71 cents of spread on average. The distance between the two ends of that chart is the first reason a blanket percentage misprices a multi name backtest. A flat 5 basis point assumption sits well above the measured floor for the tightest of these names.

Why order size changes the number

Half a spread assumes the order was small enough to fill at the touch. Size retires that assumption. A larger order either walks up the book through worse price levels or gets worked over minutes while the quote moves around it. The next panel matches every AAPL print in the same midday hour to the quote in force at the time of that print, then groups the prints by size.

QueryDistance from the prevailing mid by trade size, AAPL, one midday hour
size_bucketavg_distance_bpspct_outside_touch
1 to 99 shares0.55214.31
100 to 4990.40114.21
500 to 9990.6316.91
1,000 to 4,9990.52315.38
5,000 or more4.63836.36
The exact SQL behind every number
WITH
    quotes AS
    (
        SELECT
            ticker,
            sip_timestamp,
            toFloat64(bid_price + ask_price) / 2 AS mid,
            toFloat64(ask_price - bid_price) / 2 AS half_spread
        FROM global_markets.cache_stocks_quotes
        WHERE ticker = 'AAPL'
          AND sip_timestamp >= toDateTime('2026-09-16 14:00:00', 'UTC')
          AND sip_timestamp <  toDateTime('2026-09-16 15:00:00', 'UTC')
          AND bid_price > 0
          AND ask_price > bid_price
    ),
    fills AS
    (
        SELECT
            ticker,
            sip_timestamp,
            toFloat64(price) AS fill_price,
            size
        FROM global_markets.stocks_trades
        WHERE ticker = 'AAPL'
          AND sip_timestamp >= toDateTime('2026-09-16 14:00:00', 'UTC')
          AND sip_timestamp <  toDateTime('2026-09-16 15:00:00', 'UTC')
          AND price > 0
          AND size > 0
    )
SELECT
    multiIf(f.size < 100,  '1 to 99 shares',
            f.size < 500,  '100 to 499',
            f.size < 1000, '500 to 999',
            f.size < 5000, '1,000 to 4,999',
                           '5,000 or more')                                      AS size_bucket,
    round(avg(abs(f.fill_price - q.mid) / q.mid) * 10000, 3)                      AS avg_distance_bps,
    round(100 * countIf(abs(f.fill_price - q.mid) > q.half_spread) / count(), 2)  AS pct_outside_touch
FROM fills AS f
ASOF JOIN quotes AS q ON f.ticker = q.ticker AND f.sip_timestamp >= q.sip_timestamp
GROUP BY size_bucket
ORDER BY min(f.size)
Run this yourself

Average distance from the prevailing mid came to 0.552 basis points in the 1 to 99 shares bucket and 4.638 basis points in the 5,000 or more bucket. The share of prints landing outside the touch, further from the mid than half the quoted spread, moved from 14.31% in the smallest bucket to 36.36% in the largest. These are prints from every participant in that hour rather than one strategy's fills, and they show the dispersion a single scalar flattens.

A usable assumption is a small lookup table, not a number. Two inputs carry most of the variation, and both are known at decision time:

  • Spread width in basis points, read from the quote the signal actually saw rather than from a daily average.
  • Order size as a share of the name's average daily volume over a trailing window, in bands such as under 0.1%, 0.1% to 1%, 1% to 5%, and above 5%.

Give each cell of that table its own measured cost, and where you hold no fills, carry the cost of the worse neighbouring cell. One constant hurts worst in thin names, the failure mode backtesting in illiquid markets takes apart.

Why options need a bigger assumption than stocks

On options the same idea lands in a far less forgiving denominator. Premiums are small, quoted increments are coarse against those premiums, and a concession of a few cents per contract is a large percentage of a cheap contract. The panel below takes SPY contracts within 2% of the money on the same pinned date, groups them by days to expiry, and prices a five cent per contract concession against the average premium in each bucket.

QueryA five cent concession as a share of premium, near-the-money SPY contracts by expiry
dte_bucketavg_premium_usdnickel_pct_of_premium
0 to 2 days5.178.21
3 to 7 days6.153.43
8 to 21 days7.891.23
22 to 45 days13.290.45
46 days or more38.40.17
The exact SQL behind every number
SELECT
    multiIf(days_to_expiry <= 2,  '0 to 2 days',
            days_to_expiry <= 7,  '3 to 7 days',
            days_to_expiry <= 21, '8 to 21 days',
            days_to_expiry <= 45, '22 to 45 days',
                                  '46 days or more')    AS dte_bucket,
    round(avg(toFloat64(option_close)), 2)              AS avg_premium_usd,
    round(avg(0.05 / toFloat64(option_close)) * 100, 2) AS nickel_pct_of_premium
FROM global_markets.options_greeks
WHERE underlying_symbol = 'SPY'
  AND date = '2026-09-16'
  AND iv_converged = 1
  AND volume > 0
  AND option_close > 0.05
  AND abs(toFloat64(strike_price) / toFloat64(underlying_close) - 1) < 0.02
GROUP BY dte_bucket
ORDER BY min(days_to_expiry)
Run this yourself

Average premium ran $5.17 in the 0 to 2 days bucket and $38.4 in the 46 days or more bucket. The same five cents is 8.21% of premium at the short end of that curve and 0.17% at the long end. A stock backtest assuming 5 basis points per side and an options backtest assuming the same figure are different exercises wearing the same label. What breaks an options backtest covers the adjacent traps around expiry and quote quality.

The published hand wave, and what one tick costs

Published defaults are close to useless as guidance. Tutorials and off the shelf engines ship slippage settings anywhere from 0.01% to 0.50% per trade, a fiftyfold range, with no stated method and no dependence on either spread width or order size. The stakes of picking from that range are easy to show. The panel below runs a deliberately naive yardstick over ten household names in the first half of 2026, buying the official open and selling the official close, then nets the average per trade result against a ladder of per side slippage assumptions. The rule is a measuring stick, never a strategy.

QueryA naive open-to-close yardstick, netted against a ladder of slippage assumptions
assumed_slippage_bpsnames_testednames_still_positiveavg_net_edge_bps
01072.04
0.51061.04
11060.04
2105-1.96
3103-3.96
5102-7.96
The exact SQL behind every number
WITH
    naive_rule AS
    (
        SELECT
            ticker,
            round(avg((toFloat64(close) / toFloat64(open) - 1) * 10000), 3) AS gross_edge_bps
        FROM global_markets.stocks_daily_aggs
        WHERE ticker IN ('SPY', 'AAPL', 'MSFT', 'NVDA', 'KO', 'JNJ', 'XOM', 'PG', 'WMT', 'JPM')
          AND date >= '2026-01-02'
          AND date <  '2026-07-01'
          AND open > 0
          AND volume > 0
        GROUP BY ticker
    ),
    ladder AS
    (
        SELECT arrayJoin([0., 0.5, 1., 2., 3., 5.]) AS slippage_bps
    )
SELECT
    l.slippage_bps                                        AS assumed_slippage_bps,
    count()                                               AS names_tested,
    countIf(r.gross_edge_bps - 2 * l.slippage_bps > 0)    AS names_still_positive,
    round(avg(r.gross_edge_bps - 2 * l.slippage_bps), 2)  AS avg_net_edge_bps
FROM naive_rule AS r
CROSS JOIN ladder AS l
GROUP BY l.slippage_bps
ORDER BY assumed_slippage_bps
Run this yourself

With no cost assumed, 7 of the 10 names carried a positive average. One penny on a $100 stock is one basis point per side, and at that single tick the count stands at 6, with the basket average per trade moving from 2.04 to 0.04 basis points. At the top rung, 5 basis points per side, 2 sit above zero. An edge that survives at only one rung of that ladder is an artifact of the parameter, and showing the whole ladder is the honest way to report it. The same discipline belongs on the rest of a test's assumptions, as how to backtest a trading strategy lays out.

Measure realized slippage, then feed back the distribution

A constant is a placeholder for a measurement nobody has taken yet. Once an order log exists, replace it:

  • Store the decision mid with every order, at signal time, before the order leaves.
  • Compute fill minus decision mid in basis points for every fill, signed by side.
  • Compare the result against the markout curve for the same fills at one second, ten seconds, one minute, and five minutes after the print.
  • Swap the scalar for percentiles from the matching bucket, and run the test twice, at the median and at the 90th percentile.

The markout comparison separates two costs that one slippage number blends. The distance from decision mid to fill is the price of demanding immediacy. The drift of the mid in the seconds after the fill is the part belonging to information and impact, and it keeps accruing after the order is done. A strategy whose markout curve keeps sliding against it after every fill carries a cost no entry price adjustment can represent.

Where the measured distribution is thin, keep the assumption deliberately pessimistic and record which bucket the number came from. Documented pessimism survives review. An undocumented 0.05% does not.

FAQ

What is a reasonable slippage assumption for a stock backtest?

Half the quoted spread at decision time is the floor for a small market order, measured above at 0.11 basis points for the tightest of six household names. Anything beyond a small order needs a size dependent number taken from real fills, bucketed by spread width and by share of average daily volume.

Is half the bid ask spread enough slippage?

It is enough only for an order that would have filled at the touch in full. For orders large against a name's average daily volume, and for options, measured costs run well above the half spread floor.

How do I measure realized slippage from my own fills?

Log the decision mid with every order, then compute fill price minus that mid in basis points, signed with the worse price positive. Group the results by spread width and order size, and keep the full distribution rather than the average.

Should slippage be a percentage or a fixed number of ticks?

A percentage of price carries across names and across years better than a fixed cent amount, since a penny is one basis point on a $100 stock and ten on a $10 stock. The exception is a market where the tick itself binds: there the modelled cost cannot drop below one tick, whatever the percentage says.


Every panel above ships with the SQL that produced it. Open one, change the ticker or the window, and measure the floor for the names you test on the Strasmore terminal.

Full data notes
  • The quote and trade panels use one pinned midday hour, 14:00 to 15:00 UTC on 2026-09-16, which is 10:00 to 11:00 in New York. A pinned window keeps the figures stable across regenerations.
  • Distance from the mid is matched print by print against the last quote at or before each trade. It covers every print in that hour from every participant, a population measurement rather than one account's fills.
  • The option panel keeps contracts within 2% of the money with converged implied volatility and non zero volume, then averages the daily close premium per expiry bucket. The five cent figure is an illustrative concession, not a quoted spread.
  • The ladder's yardstick buys the official open and sells the official close, one trade per session per name, from 2026-01-02 through 2026-06-30. Auction prices carry mechanics of their own, and the window's prices are as currently adjusted, a caveat split adjusted price history explains.
#backtesting#slippage#execution costs#bid-ask spread#markouts