the slippage problem is worse than i thought #
after 2 weeks live trading (10 total trades), my average slippage is $6.40 per spread
that’s $640 per 100 trades. at 240 trades per year (20/month avg), that’s $1,536 annual drag
on a $400k account that’s 0.384% annual slippage cost
sounds small. but when your gross return is 18% and slippage takes 0.4%, you’re down to 17.6% net
sharpe ratio drops from 1.9 to 1.78
more importantly: my backtest didn’t model slippage correctly
what i was doing wrong #
old backtest assumed fixed $0.05 slippage per option leg
so on a $10 wide credit spread:
- sell short leg at $2.20
- buy long leg at $1.35
- net credit: $0.85
- backtest slippage: $0.05
- backtest fill: $0.80
problem: real slippage varies based on bid-ask spread width and market conditions
on march 21 (yesterday) the bid-ask on my short leg was $0.12 wide. i got filled at bid + $0.03. that’s $3 slippage on one leg
old model would’ve assumed $5 slippage total. reality was $8
the new model #
built a lively slippage estimator based on actual fills
here’s the code (integrated into my backtrader strategy):
import numpy as np
import pandas as pd
from datetime import datetime
class DynamicSlippageModel:
"""
Models realistic slippage for SPX credit spreads based on:
- Bid-ask spread width
- Time of day
- VIX level (volatility)
- Days to expiration
"""
def __init__(self):
# historical fill data from live trading
self.fill_history = []
self.bid_ask_history = []
def estimate_slippage(self, option_price, dte, vix_level, time_of_day):
"""
Estimate slippage for a single option leg
Args:
option_price: Mid price of option
dte: Days to expiration
vix_level: Current VIX reading
time_of_day: Hour (0-23)
Returns:
Estimated slippage in dollars
"""
# base slippage is function of option price
# cheaper options have wider relative spreads
if option_price < 0.50:
base_slippage = 0.03
elif option_price < 1.00:
base_slippage = 0.04
elif option_price < 2.00:
base_slippage = 0.05
else:
base_slippage = 0.06
# adjust for DTE (closer to expiration = wider spreads)
if dte <= 2:
dte_multiplier = 1.5
elif dte <= 5:
dte_multiplier = 1.2
else:
dte_multiplier = 1.0
# adjust for VIX (high vol = wider spreads)
if vix_level > 30:
vix_multiplier = 1.4
elif vix_level > 20:
vix_multiplier = 1.2
else:
vix_multiplier = 1.0
# adjust for time of day
# 9:30-10:30 AM EST has tightest spreads (high volume)
# after 3pm spreads widen
if 9 <= time_of_day <= 10: # 9:30-10:30 EST
time_multiplier = 1.0
elif 15 <= time_of_day <= 16: # 3pm-4pm EST
time_multiplier = 1.3
else:
time_multiplier = 1.15
# calculate total slippage
slippage = base_slippage * dte_multiplier * vix_multiplier * time_multiplier
return slippage
def record_fill(self, expected_price, actual_price, bid_ask_width,
dte, vix, time_of_day):
"""
Record actual fill for model improvement
"""
fill_data = {
'timestamp': datetime.now(),
'expected': expected_price,
'actual': actual_price,
'slippage': expected_price - actual_price,
'bid_ask_width': bid_ask_width,
'dte': dte,
'vix': vix,
'time': time_of_day
}
self.fill_history.append(fill_data)
def get_average_slippage(self, lookback_trades=20):
"""
Calculate average slippage from recent fills
"""
if len(self.fill_history) < lookback_trades:
return 0.05 # default assumption
recent = self.fill_history[-lookback_trades:]
slippages = [f['slippage'] for f in recent]
return np.mean(slippages)
def backtest_with_slippage(self, trades_df):
"""
Apply slippage model to backtest trades
Args:
trades_df: DataFrame with columns [entry_price, exit_price,
dte_entry, dte_exit, vix_entry, vix_exit, time_entry, time_exit]
Returns:
DataFrame with slippage-adjusted returns
"""
results = []
for idx, trade in trades_df.iterrows():
# entry slippage (we're selling, so we get worse price)
entry_slip = self.estimate_slippage(
trade['entry_price'],
trade['dte_entry'],
trade['vix_entry'],
trade['time_entry']
)
# exit slippage (we're buying back, so we pay more)
exit_slip = self.estimate_slippage(
trade['exit_price'],
trade['dte_exit'],
trade['vix_exit'],
trade['time_exit']
)
# calculate adjusted P&L
# for credit spreads: profit = entry_price - exit_price
theoretical_pnl = trade['entry_price'] - trade['exit_price']
actual_pnl = (trade['entry_price'] - entry_slip) - (trade['exit_price'] + exit_slip)
results.append({
'trade_id': idx,
'theoretical_pnl': theoretical_pnl,
'actual_pnl': actual_pnl,
'slippage_cost': theoretical_pnl - actual_pnl,
'entry_slip': entry_slip,
'exit_slip': exit_slip
})
return pd.DataFrame(results)
# integration with backtrader strategy
class SPXCreditSpreadWithSlippage(bt.Strategy):
"""
Original strategy with realistic slippage modeling
"""
def __init__(self):
super().__init__()
self.slippage_model = DynamicSlippageModel()
def next(self):
# ... strategy logic ...
# when placing order, estimate slippage
current_vix = self.get_vix()
current_hour = self.datas[0].datetime.datetime(0).hour
dte = self.get_dte_for_target()
theoretical_credit = self.calculate_spread_price(short_strike, long_strike)
# estimate slippage for both legs
short_slippage = self.slippage_model.estimate_slippage(
short_price, dte, current_vix, current_hour
)
long_slippage = self.slippage_model.estimate_slippage(
long_price, dte, current_vix, current_hour
)
# adjust expected fill
expected_credit = theoretical_credit - short_slippage - long_slippage
# place order with adjusted expectations
self.sell_spread(short_strike, long_strike, expected_credit)
def notify_trade(self, trade):
"""
Record actual fills for model improvement
"""
if trade.isclosed:
# extract actual fill prices and record
self.slippage_model.record_fill(
expected_price=trade.price,
actual_price=trade.executed.price,
bid_ask_width=self.get_bid_ask_width(),
dte=self.get_current_dte(),
vix=self.get_vix(),
time_of_day=self.get_hour()
)
backtest results with new model #
re-ran 2021-2022 backtest with lively slippage:
old model (fixed $0.05 slippage):
- annual return: 18.2%
- sharpe ratio: 1.9
- max drawdown: 18%
- total slippage cost: $892/year
new model (fluid slippage):
- annual return: 14.3%
- sharpe ratio: 1.64
- max drawdown: 18.5%
- total slippage cost: $1,547/year
that’s a 3.9% annual return difference just from realistic slippage modeling
fucking brutal but at least now i know what to expect
live performance validation #
comparing march 15-21 live trading to new backtest model:
- actual average slippage: $6.40 per spread
- new model predicted: $6.10 per spread
- error: 4.9%
way better than old model which predicted $5.00 (28% error)
what this means going forward #
adjusted my profit targets:
- old target: 18% annual return
- new realistic target: 14-15% annual return
- still good for a sharpe 1.6+ strategy
also means i need ~715 subscribers at $19/month to hit $10k MRR if i ever productize this (14% on $400k = $56k annual, need $120k total to make $10k/mo after living expenses)
math works but margin is tighter than i thought
next steps #
- run model for another 20 trades to validate
- track prediction error weekly
- adjust multipliers if needed
- maybe switch to limit orders on high-slippage scenarios
for now the model is live in my backtest framework and i’m using it to set realistic expectations
-AK