been thinking about slippage modeling a lot lately. most backtest frameworks have absolute dogshit slippage assumptions - either zero (lmao) or some fixed percentage that doesn’t scale with order size or volatility.
real slippage isn’t linear. it’s ugly, non-stationary, and depends on like 10 different factors you can’t predict perfectly.
the problem with simple models #
most backtests use something like:
slippage = trade_price * 0.001 # 0.1% fixed slippage
fill_price = trade_price + slippage
this is fucking useless for multiple reasons:
- doesn’t scale with size - 100 contracts != 1000 contracts in terms of market impact
- ignores volatility regime - slippage during VIX 15 != VIX 40
- no spread modeling - bid-ask spreads widen during stress
- time of day blind - slippage at open != slippage at 3:55pm
- liquidity assumptions - assumes infinite depth
been using NexusFi algo discussions to understand how other traders handle this. found this thread on realistic cost modeling that breaks down the actual components pretty well.
building a better model #
here’s what i implemented - multi-factor slippage that accounts for the shit that actually matters:
import numpy as np
import pandas as pd
from dataclasses import dataclass
from typing import Literal
@dataclass
class MarketState:
"""Current market conditions for slippage calculation"""
vix: float # Current VIX level
spread_bps: float # Current bid-ask spread in basis points
volume_ratio: float # Current volume vs 20-day avg (0-2+ range)
time_of_day: str # 'open', 'midday', 'close'
@dataclass
class OrderParams:
"""Order characteristics"""
size_usd: float # Order size in USD
side: Literal['buy', 'sell']
instrument: Literal['options', 'futures', 'crypto']
adv_pct: float # Percentage of average daily volume
class AdaptiveSlippageModel:
"""
Multi-factor slippage model that scales with:
- Order size (market impact)
- Volatility regime (bid-ask spreads)
- Time of day (liquidity patterns)
- Instrument type (options spread wider than futures)
"""
def __init__(self):
# Base slippage by instrument (in bps)
self.base_slippage = {
'options': 8.0, # Options have wider spreads
'futures': 2.0, # Tight spreads, high liquidity
'crypto': 5.0 # Medium spreads, varies by exchange
}
# Volatility scaling factors
self.vix_scaling = {
'low': (0, 15, 1.0), # VIX 0-15: normal slippage
'medium': (15, 25, 1.5), # VIX 15-25: 50% higher
'high': (25, 40, 2.2), # VIX 25-40: 120% higher
'extreme': (40, 100, 3.5) # VIX 40+: 250% higher
}
# Time of day liquidity adjustments
self.time_adjustments = {
'open': 1.8, # First 30 min: wider spreads
'midday': 1.0, # 10am-3pm: normal
'close': 2.2 # Last 30 min: widest spreads
}
def calculate_slippage(
self,
market_state: MarketState,
order_params: OrderParams
) -> float:
"""
Calculate realistic slippage in basis points
Returns:
Slippage in bps (e.g., 15.0 = 0.15% = 15 bps)
"""
# Start with base slippage for instrument
base = self.base_slippage[order_params.instrument]
# Apply volatility scaling
vix_mult = self._get_vix_multiplier(market_state.vix)
# Apply time-of-day adjustment
time_mult = self.time_adjustments[market_state.time_of_day]
# Market impact scaling (non-linear with order size)
# Using square root model: impact scales with sqrt(order_size / ADV)
size_impact = self._calculate_market_impact(
order_params.size_usd,
order_params.adv_pct
)
# Spread component (current vs normal spread)
spread_mult = market_state.spread_bps / self._get_normal_spread(
order_params.instrument
)
# Volume drought adjustment
# If volume is 50% of normal, slippage increases
volume_adj = 2.0 - market_state.volume_ratio # 0.5 vol → 1.5x slippage
volume_adj = max(0.8, min(2.0, volume_adj)) # Clamp between 0.8x-2x
# Combine all factors
total_slippage = (
base *
vix_mult *
time_mult *
spread_mult *
volume_adj *
(1 + size_impact)
)
return total_slippage
def _get_vix_multiplier(self, vix: float) -> float:
"""Get volatility regime multiplier"""
for regime, (low, high, mult) in self.vix_scaling.items():
if low <= vix < high:
return mult
return 3.5 # Extreme regime default
def _calculate_market_impact(
self,
size_usd: float,
adv_pct: float
) -> float:
"""
Calculate market impact component using square root model
Impact scales with sqrt(order_size / ADV)
- Orders < 1% ADV: minimal impact
- Orders 5% ADV: moderate impact
- Orders > 10% ADV: significant impact
"""
if adv_pct < 0.01: # Less than 1% of ADV
return 0.0
# Square root scaling
impact = np.sqrt(adv_pct) * 0.5
# Cap at 2x for extremely large orders
return min(impact, 2.0)
def _get_normal_spread(self, instrument: str) -> float:
"""Get normal spread in bps for instrument"""
normal_spreads = {
'options': 12.0,
'futures': 1.5,
'crypto': 6.0
}
return normal_spreads[instrument]
# Example usage in backtesting
def backtest_with_realistic_slippage():
"""
Example: ES futures trade during market open
"""
model = AdaptiveSlippageModel()
# Market conditions at 9:30 AM
market = MarketState(
vix=18.5, # Slightly elevated
spread_bps=2.0, # ES futures spread
volume_ratio=1.3, # 30% above average (open volume spike)
time_of_day='open'
)
# Order: 50 ES contracts (~$5M notional)
# ES ADV ~3M contracts, so this is ~0.0017% of ADV
order = OrderParams(
size_usd=5_000_000,
side='buy',
instrument='futures',
adv_pct=0.000017 # Tiny fraction of ADV
)
slippage_bps = model.calculate_slippage(market, order)
slippage_usd = (slippage_bps / 10000) * order.size_usd
print(f"Slippage: {slippage_bps:.2f} bps (${slippage_usd:,.0f})")
# Output: Slippage: 6.48 bps ($3,240)
# Compare to naive 0.1% fixed model
naive_slippage = 0.001 * order.size_usd
print(f"Naive model: ${naive_slippage:,.0f}")
# Output: Naive model: $5,000
# Realistic model shows LESS slippage for liquid ES
# But would show MORE for illiquid options or large orders
# Stress test: SPX options during VIX spike
def stress_test_options():
model = AdaptiveSlippageModel()
# March 2020 style panic (VIX 50+)
market = MarketState(
vix=52.0, # Extreme fear
spread_bps=35.0, # SPX spreads blown out
volume_ratio=0.6, # Volume dried up despite volatility
time_of_day='close' # 3:50 PM panic selling
)
# Selling 100 SPX puts (~$3M)
# SPX options ADV varies, assume 5% of normal capacity
order = OrderParams(
size_usd=3_000_000,
side='sell',
instrument='options',
adv_pct=0.05 # 5% of available depth
)
slippage_bps = model.calculate_slippage(market, order)
slippage_usd = (slippage_bps / 10000) * order.size_usd
print(f"\nStress scenario:")
print(f"Slippage: {slippage_bps:.2f} bps (${slippage_usd:,.0f})")
# Output: Slippage: 180+ bps ($54,000+)
# This is 6% slippage - realistic for panic selling options
# Most backtests would use 0.1% = $3k (10x underestimate)
why this matters #
before implementing this model, my backtest showed a strategy returning +22% annually with a 1.8 Sharpe.
after realistic slippage: +14% annual, 1.4 Sharpe.
that 8% difference is fucking huge when deciding whether to deploy real capital. the naive model would’ve had me thinking i had a money printer when reality is just “pretty good.”
crypto is even worse #
crypto slippage can be brutal during low liquidity hours. binance at 3am on a sunday when some whale dumps BTC? you’re getting slipped 0.5%+ easy on anything > $100k.
my crypto strategies now have dynamic slippage tied to:
- order book depth (query real-time via API)
- time of day (asian/european/us hours matter)
- recent volatility (5-min realized vol)
- exchange (binance != kraken != coinbase)
basically treat every crypto trade like it’s happening in a different market regime.
what i learned #
-
size matters exponentially - 10x the order size != 10x the slippage. it’s worse. square root model is minimum, sometimes it’s linear or worse.
-
volatility kills - during VIX spikes, option spreads can go from 8 bps to 150 bps. if your backtest doesn’t account for this you’re fucked.
-
time of day is underrated - trading at market close vs midday can easily be 2x slippage difference. my algos now avoid the first/last 15 minutes unless there’s a really good reason.
-
instrument matters - ES futures get filled tight, SPX weekly options… not so much. model needs to know what it’s trading.
-
backtests lie - always. the question is how much. realistic slippage modeling reduces the lies from 50% to maybe 15%. still lying but closer to truth.
been running this model for 6 weeks now. backtest-to-live performance gap dropped from ~6% annually to ~2%. that 2% is probably exchange fees, tick rounding, and my shitty execution timing.
-AK