Here is a robust Python implementation demonstrating how to model dynamic, volume-based slippage inside a custom Pandas backtester.

Instead of applying a flat, unrealistic penalty to every trade, this snippet uses a nonlinear market impact model (often called a square-root law variant). The model dynamically scales execution friction based on two critical variables:

  1. Order Size relative to Market Liquidity: Larger orders that eat deeper into the order book suffer worse slippage.
  2. Historical Volatility: Slippage increases during periods of high market turbulence because the bid-ask spread naturally widens.
dynamic slippage models in Python

Python

import numpy as np
import pandas as pd


def apply_dynamic_slippage(
    df: pd.DataFrame,
    order_size_col: str,
    market_vol_col: str,
    market_return_col: str,
    participation_rate: float = 0.10,
    impact_exponent: float = 0.5,
) -> pd.DataFrame:
    """Simulates a nonlinear, volume-and-volatility-driven market impact model

    to accurately penalize backtest executions.

    Parameters:
    -----------
    df : pd.DataFrame
        The backtest time-series dataframe.
    order_size_col : str
        The column name representing the number of shares/units your strategy
        intends to trade on that bar. (Positive for Buy, Negative for Sell).
    market_vol_col : str
        The column name for historical market volume on that bar.
    market_return_col : str
        The column name representing asset volatility (e.g., rolling standard
        deviation of log returns or rolling ATR percentage).
    participation_rate : float, default 0.10
        Scaling factor determining how aggressively order volume impacts the
        order book spread.
    impact_exponent : float, default 0.5
        The square-root power factor. Empirical microstructure data suggests
        market impact scales roughly with the square root of order size (0.5).

    Returns:
    --------
    pd.DataFrame
        Dataframe with added 'slippage_pct' and 'execution_price' columns.
    """
    # Create a deep copy to prevent mutating the original dataframe
    backtest_df = df.copy()

    # Calculate Volume Participation Ratio: (Your Order Size / Total Bar Market Volume)
    # Using absolute values since both buys and sells consume liquidity
    backtest_df["volume_participation"] = backtest_df[order_size_col].abs() / (
        backtest_df[market_vol_col] + 1e-8
    )

    # Nonlinear Market Impact Formula:
    # Slippage % = Participation Rate * (Vol Participation)^Impact Exponent * Market Volatility
    backtest_df["slippage_pct"] = (
        participation_rate
        * (backtest_df["volume_participation"] ** impact_exponent)
        * backtest_df[market_return_col]
    )

    # Fill NaNs with 0 (for bars where the strategy didn't issue any orders)
    backtest_df["slippage_pct"] = backtest_df["slippage_pct"].fillna(0.0)

    # Apply execution direction constraints:
    # Buys (positive order) get filled HIGHER than the printed price.
    # Sells (negative order) get filled LOWER than the printed price.
    backtest_df["execution_price"] = np.where(
        backtest_df[order_size_col] > 0,
        backtest_df["close"] * (1 + backtest_df["slippage_pct"]),  # Buy Penalty
        np.where(
            backtest_df[order_size_col] < 0,
            backtest_df["close"] * (1 - backtest_df["slippage_pct"]),  # Sell Penalty
            backtest_df["close"],  # No trade issued
        ),
    )

    return backtest_df


# ==========================================
# EXAMPLE USAGE & SAMPLE DATA SIMULATION
# ==========================================
if __name__ == "__main__":
    # Generate mock 15-minute bar market data
    np.random.seed(42)
    dates = pd.date_range(start="2026-07-10 09:30", periods=5, freq="15min")

    data = {
        "close": [150.00, 150.50, 152.00, 151.20, 151.80],
        "market_volume": [
            50000,
            12000,
            85000,
            22000,
            45000,
        ],  # Notice low liquidity on bar 2
        "rolling_volatility": [
            0.012,
            0.015,
            0.025,
            0.018,
            0.011,
        ],  # High volatility on bar 3
        "strategy_order": [
            2500,
            2500,
            -5000,
            0,
            100,
        ],  # Positive = Buy, Negative = Sell
    }

    raw_backtest = pd.DataFrame(data, index=dates)

    # Process through our defensive slippage engine
    realistic_backtest = apply_dynamic_slippage(
        df=raw_backtest,
        order_size_col="strategy_order",
        market_vol_col="market_volume",
        market_return_col="rolling_volatility",
    )

    # Display results focused on execution deviations
    display_cols = [
        "close",
        "strategy_order",
        "volume_participation",
        "slippage_pct",
        "execution_price",
    ]
    print(realistic_backtest[display_cols].to_string())

Breakdown of the Code Output Logic

  • Bar 1 (Standard Buy): Buying 2,500 shares into 50,000 market volume yields a $5\%$ participation rate. The resulting fill price slips up slightly from the base price of $150.00 to $150.40.
  • Bar 2 (Low Liquidity Trap): Buying that exact same 2,500 share size into a dry market volume of only 12,000 shares causes volume participation to spike to nearly $21\%$. Even though the market price only moved up $0.50$, your aggressive order profile pushes your execution price up significantly further due to lack of available liquidity.
  • Bar 3 (High Volatility Liquidity Sweep): Selling a massive block of 5,000 shares during a high-volatility window ($0.025$) compounds the microstructural degradation, forcing a heavily penalized fill price way below the historical baseline close of $152.00.

Contact us if you are looking for Professional Custom Trading Software Development Services

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.