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:
- Order Size relative to Market Liquidity: Larger orders that eat deeper into the order book suffer worse slippage.
- Historical Volatility: Slippage increases during periods of high market turbulence because the bid-ask spread naturally widens.

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