Skip to main content

modeling slippage the right way

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

Related

first algo went live today
three years of paper trading, finally real money # started learning algo trading april 2020 during COVID lockdown. i was 16, bored af at home, discovered r/algotrading and fell into the rabbit hole
first week done. slippage is brutal
one week live trading = reality check # algo’s been running 7 days. march 15-17. opened 3 positions total