"Price is the effect. Liquidity is the cause."

A backtest that ignores slippage is like a flight simulator that never tells you the plane will eventually run out of fuel. You will feel like a hero until you land — and then you will be stranded on the runway.

The problem is not that quant researchers are careless. It is that slippage is invisible in historical data. OHLCV bars do not come with a footnote saying: "If you tried to fill 50,000 shares at this moment, you would have moved the price 23 bps against yourself." That footnote requires a model.

This article builds that model from the ground up. We start with the mathematics of market impact, implement three canonical models in production-grade Python, and demonstrate how to calibrate them against real order book data from TickDB. By the end, your backtester will report something closer to what your execution engine will actually experience.


1. Why Standard Backtests Lie About Execution Cost

Bar-based backtesting has a fundamental flaw: it assumes you can execute at the close price of whatever bar triggered your signal. For a daily-bar strategy, this means assuming you always trade at tomorrow's open. For an intraday strategy, it means assuming you trade at the bar's midpoint.

Neither assumption survives contact with real markets.

The discrepancy between your backtest fill price and your live fill price has two components:

Component Definition Typical magnitude
Slippage Difference between expected price and actual fill price due to order timing 1–5 bps (liquid markets), 10–50 bps (illiquid)
Market impact The price movement you cause by submitting your own order Non-linear; grows faster than order size

A strategy that shows 18% annualized returns with zero transaction costs may show −3% after realistic slippage is applied. This is not a rounding error. It is the difference between a strategy that ships and one that gets quietly shelved.

The solution is to simulate market impact before you ever touch a live account. To do that, you need three things:

  1. A model that maps order size to expected impact.
  2. Realistic estimates of the model's parameters for your asset class.
  3. Sufficient data granularity to apply the model at the right moments.

TickDB provides the third ingredient: 10+ years of cleaned, aligned OHLCV data (the kline endpoint) and real-time order book depth (the depth channel) that lets you reconstruct the liquidity state at any historical timestamp. The first two are the subject of the rest of this article.


2. The Mathematics of Market Impact

2.1 Temporary vs. Permanent Impact

Market impact is not a single phenomenon. Academic literature — starting with the Almgren-Chriss model (2000) and extended by practitioners at major quant funds — distinguishes two components:

Permanent impact reflects information leakage. Your trade signals that you know something, and the market adjusts its price accordingly. This adjustment persists after your order is complete.

Temporary impact reflects liquidity consumption. You are taking liquidity from the order book — consuming resting bids or asks — and the market recovers once you are done. Temporary impact decays over time.

The total impact of a trade of size $X$ executed over time $T$ is:

$$\text{Impact}(X, T) = \underbrace{\eta \cdot \frac{X}{\text{ADV}}}{\text{permanent}} + \underbrace{\theta \cdot \sqrt{\frac{X}{\text{ADV}}} \cdot \sigma \cdot \sqrt{T}}{\text{temporary}}$$

Where:

  • $\eta$ = permanent impact coefficient (typically 0.1–0.5 for US equities)
  • $\theta$ = temporary impact coefficient (typically 0.5–1.0)
  • $\text{ADV}$ = average daily volume
  • $\sigma$ = daily volatility
  • $T$ = execution time horizon (in days)

2.2 The Square Root Model

A more practitioner-friendly formulation is the square root model, widely used at high-frequency and mid-frequency strategies:

$$\text{impact} = \alpha \cdot \sigma \cdot \sqrt{\frac{\text{order_size}}{\text{ADV}}}$$

This is derived from Kyle's lambda and empirically validated across equities, futures, and crypto. The coefficient $\alpha$ varies by asset class:

Asset class $\alpha$ range Notes
US large-cap equity (SPX) 0.5–0.8 Highly liquid
US small-cap equity 1.0–2.5 Thin books, wide spreads
Futures (ES, NQ) 0.3–0.6 Excellent liquidity
Crypto (BTC/USD) 0.4–0.9 Varies by exchange
A-shares 1.5–4.0 Fragmented, retail-heavy

2.3 Choosing Your Model

Model Best for Strength Weakness
Almgren-Chriss Optimal execution scheduling Theoretically rigorous, accounts for timing Requires estimate of your own urgency parameter
Square root Strategy-level slippage estimation Simple, requires only ADV and volatility Ignores order book depth details
Order book replay Exact simulation Most accurate when data is available Computationally expensive; requires tick or depth data

For most quant strategies, we recommend a two-layer approach: use the square root model for rapid screening during strategy development, then validate with order book replay for final backtesting on key instruments.


3. Building a Slippage Simulation Engine

3.1 Architecture Overview

Our slippage simulation engine has three components:

┌─────────────────────────────────────────────────────────────┐
│                    Slippage Simulation Engine                │
├──────────────┬──────────────────────┬───────────────────────┤
│ Data Layer   │  Model Layer         │  Calibration Layer    │
│              │                      │                       │
│ • kline      │  • SquareRootModel   │  • ADV estimation     │
│   (TickDB)   │  • AlmgrenChriss     │  • σ estimation       │
│ • depth      │  • OrderBookReplay   │  • α calibration      │
│   (TickDB)   │                      │  • Validation suite   │
└──────────────┴──────────────────────┴───────────────────────┘

The data layer fetches historical OHLCV from TickDB's kline endpoint and order book depth from the depth channel. The model layer implements the three impact models. The calibration layer estimates parameters from historical data and validates model outputs.

3.2 Core Data Structures

We define a SlippageConfig that encapsulates all parameters needed for slippage estimation:

from dataclasses import dataclass
from typing import Optional
import os
import time
import random
import math

@dataclass
class SlippageConfig:
    """Configuration for slippage and market impact estimation."""
    # Asset parameters
    symbol: str
    adv: float  # Average daily volume (in shares/units)
    daily_volatility: float  # σ, annualized or daily depending on context
    
    # Model coefficients (calibrated per asset)
    square_root_alpha: float = 0.7  # Default for liquid US equity
    permanent_eta: float = 0.25
    temporary_theta: float = 0.8
    
    # Execution parameters
    order_size: float = 0.0  # Set at simulation time
    execution_horizon_days: float = 1/390  # Default: intraday (390 = trading minutes in US equity)
    
    # Market condition modifiers
    high_volatility_multiplier: float = 1.5  # Applied during earnings, Fed meetings
    low_liquidity_multiplier: float = 2.0   # Applied outside regular hours

4. Implementation: The Three Models

4.1 Square Root Model

The simplest and most widely used model. Implement it first; it will be your workhorse:

class SquareRootModel:
    """Square root market impact model.
    
    impact = alpha * sigma * sqrt(order_size / ADV)
    
    This model captures the empirically observed non-linear relationship
    between order size and impact. As order size grows relative to ADV,
    impact grows with the square root, not linearly.
    """
    
    def __init__(self, config: SlippageConfig):
        self.config = config
    
    def estimate_impact_bps(
        self,
        order_size: float,
        adv: Optional[float] = None,
        volatility: Optional[float] = None,
        alpha: Optional[float] = None
    ) -> float:
        """Estimate one-sided market impact in basis points.
        
        Args:
            order_size: Number of shares/units in the order
            adv: Override ADV (useful for intraday ADV estimation)
            volatility: Override volatility (useful for event periods)
            alpha: Override impact coefficient
        
        Returns:
            Expected impact in basis points (bps)
        """
        adv = adv or self.config.adv
        volatility = volatility or self.config.daily_volatility
        alpha = alpha or self.config.square_root_alpha
        
        if adv <= 0:
            raise ValueError(f"ADV must be positive, got {adv}")
        
        if order_size <= 0:
            return 0.0
        
        # Compute participation rate
        participation_rate = order_size / adv
        
        # Square root impact formula
        impact = alpha * volatility * math.sqrt(participation_rate)
        
        # Convert to basis points (assuming volatility is in decimal form)
        # e.g., 1% daily vol = 0.01, result is in same units
        impact_bps = impact * 10000
        
        return impact_bps
    
    def estimate_round_trip_slippage(
        self,
        order_size: float,
        side: str = "buy",
        adv: Optional[float] = None
    ) -> float:
        """Estimate round-trip slippage including spread cost.
        
        Args:
            order_size: Number of shares/units (total, both sides)
            side: 'buy', 'sell', or 'round_trip'
            adv: Average daily volume
        
        Returns:
            Slippage in dollars per share
        """
        half_size = order_size / 2 if side == "round_trip" else order_size
        impact_bps = self.estimate_impact_bps(half_size, adv)
        
        # Get current price estimate (would come from TickDB in production)
        # Using a placeholder; in production, fetch from kline endpoint
        estimated_price = 100.0  # This should come from your data source
        
        # Impact in dollars
        slippage_dollars = estimated_price * (impact_bps / 10000)
        
        # Apply market condition modifiers
        slippage_dollars *= self.config.low_liquidity_multiplier
        
        return slippage_dollars

4.2 Almgren-Chriss Model

This model extends the square root model by incorporating the trader's urgency parameter. A trader who is more patient (lower urgency) can spread the order over more time, reducing temporary impact at the cost of timing risk:

class AlmgrenChrissModel:
    """Almgren-Chriss optimal execution model.
    
    Extends the square root model by accounting for:
    1. Permanent impact (information leakage)
    2. Temporary impact (liquidity consumption)
    3. Timing risk (variance of execution price)
    
    The model solves for the optimal execution trajectory that
    minimizes expected cost + risk penalty.
    """
    
    def __init__(self, config: SlippageConfig):
        self.config = config
    
    def compute_optimal_trajectory(
        self,
        total_quantity: float,
        num_intervals: int = 390,
        risk_aversion: float = 1e-6
    ) -> list[dict]:
        """Compute optimal execution trajectory using Almgren-Chriss.
        
        Args:
            total_quantity: Total shares to execute
            num_intervals: Number of time intervals (default = trading minutes)
            risk_aversion: λ parameter - higher = more urgency to complete
        
        Returns:
            List of dicts with 'time', 'quantity', 'price' keys
        """
        T = self.config.execution_horizon_days * num_intervals  # Total time
        dt = T / num_intervals
        
        eta = self.config.permanent_eta
        theta = self.config.temporary_theta
        sigma = self.config.daily_volatility
        kappa = (eta + theta) / (theta * (1 - math.exp(-(eta + theta) * T)))
        
        # Almgren-Chriss optimal execution times
        trajectory = []
        remaining = total_quantity
        
        for i in range(num_intervals):
            t = (i + 1) * dt
            
            # Optimal fraction to execute at time t
            if kappa > 0:
                fraction = kappa * (1 - math.exp(-(eta + theta) * (T - t)))
                fraction = min(fraction, remaining)  # Don't exceed remaining
            
            quantity_this_interval = min(remaining, fraction * total_quantity / num_intervals)
            
            trajectory.append({
                'interval': i,
                'time_fraction': t / T,
                'quantity': quantity_this_interval,
                'cumulative_quantity': total_quantity - remaining
            })
            
            remaining -= quantity_this_interval
            if remaining <= 0:
                break
        
        return trajectory
    
    def estimate_total_impact(
        self,
        order_size: float,
        adv: Optional[float] = None
    ) -> dict:
        """Estimate total market impact using Almgren-Chriss decomposition.
        
        Returns dict with 'permanent', 'temporary', and 'total' components in bps.
        """
        adv = adv or self.config.adv
        eta = self.config.permanent_eta
        theta = self.config.temporary_theta
        sigma = self.config.daily_volatility
        
        participation_rate = order_size / adv
        
        # Permanent impact (linear in participation rate)
        permanent = eta * sigma * participation_rate * 10000
        
        # Temporary impact (square root in participation rate)
        temporary = theta * sigma * math.sqrt(participation_rate) * 10000
        
        # Total
        total = permanent + temporary
        
        return {
            'permanent_bps': permanent,
            'temporary_bps': temporary,
            'total_bps': total
        }

4.3 Order Book Replay Model

This is the most accurate but most data-intensive approach. It replays the actual order book state at the time of your hypothetical trade and simulates fills against resting orders:

import json
import requests
from dataclasses import dataclass
from typing import Optional


@dataclass
class OrderBookLevel:
    """Represents a single level in the order book."""
    price: float
    size: float
    side: str  # 'bid' or 'ask'


class OrderBookReplayModel:
    """Order book replay model for precise slippage simulation.
    
    This model replays historical order book snapshots to determine
    the exact fill price for a given order size. It is the most accurate
    method but requires access to depth data (TickDB depth channel).
    
    ⚠️ Note: Order book replay is computationally intensive and should
    be used for validation rather than continuous backtesting.
    """
    
    def __init__(self, config: SlippageConfig):
        self.config = config
    
    def fetch_historical_depth(
        self,
        symbol: str,
        timestamp_ms: int,
        api_key: Optional[str] = None
    ) -> dict:
        """Fetch historical order book snapshot from TickDB.
        
        Args:
            symbol: Trading symbol (e.g., 'NVDA.US')
            timestamp_ms: Unix timestamp in milliseconds
            api_key: TickDB API key (loaded from env if not provided)
        
        Returns:
            Dict with 'bids' and 'asks' lists
        """
        api_key = api_key or os.environ.get("TICKDB_API_KEY")
        
        if not api_key:
            raise ValueError(
                "TickDB API key required. Set TICKDB_API_KEY environment variable "
                "or pass api_key parameter."
            )
        
        headers = {"X-API-Key": api_key}
        
        # Fetch latest depth snapshot
        # In production, you would query historical depth for specific timestamps
        url = "https://api.tickdb.ai/v1/market/depth/latest"
        
        try:
            response = requests.get(
                url,
                headers=headers,
                params={"symbol": symbol},
                timeout=(3.05, 10)  # (connect_timeout, read_timeout)
            )
            
            if response.status_code == 429 or (
                response.status_code == 200 and response.json().get("code") == 3001
            ):
                # Rate limit handling
                retry_after = int(response.headers.get("Retry-After", 5))
                time.sleep(retry_after)
                # Retry once
                response = requests.get(url, headers=headers, params={"symbol": symbol}, timeout=(3.05, 10))
            
            data = response.json()
            
            if data.get("code") == 2002:
                raise KeyError(
                    f"Symbol {symbol} not found. Verify via /v1/symbols/available endpoint."
                )
            
            if data.get("code") in (1001, 1002):
                raise ValueError(
                    "Invalid API key. Check your TICKDB_API_KEY environment variable."
                )
            
            if data.get("code") == 0:
                return data.get("data", {})
            
            raise RuntimeError(f"Unexpected TickDB response: {data}")
            
        except requests.exceptions.Timeout:
            raise TimeoutError(
                f"Timeout fetching depth for {symbol}. Check network connectivity."
            )
    
    def simulate_fill(
        self,
        order_book: dict,
        order_size: float,
        side: str = "buy"
    ) -> dict:
        """Simulate order fill against historical order book.
        
        Walks through the order book levels, consuming liquidity
        until the order is filled, and calculates the volume-weighted
        average price (VWAP) of execution.
        
        Args:
            order_book: Dict with 'bids' and 'asks' lists
            order_size: Number of shares to fill
            side: 'buy' or 'sell'
        
        Returns:
            Dict with fill statistics
        """
        levels = order_book.get('asks' if side == 'buy' else 'bids', [])
        
        remaining = order_size
        total_cost = 0.0
        levels_used = []
        
        for level in levels:
            price = level.get('price', 0)
            size = level.get('size', 0)
            
            if size <= 0:
                continue
            
            fill_size = min(remaining, size)
            total_cost += fill_size * price
            levels_used.append({'price': price, 'size': fill_size})
            remaining -= fill_size
            
            if remaining <= 0:
                break
        
        if remaining > 0:
            # Order book exhausted - this represents a real risk scenario
            return {
                'filled_quantity': order_size - remaining,
                'remaining_quantity': remaining,
                'vwap': 0,
                'impact_bps': 0,
                'order_book_exhausted': True,
                'warning': f"Order book exhausted: {remaining:.2f} shares unfilled"
            }
        
        filled_quantity = order_size - remaining
        vwap = total_cost / filled_quantity
        
        # Get mid price for comparison
        best_bid = order_book.get('bids', [{}])[0].get('price', 0)
        best_ask = order_book.get('asks', [{}])[0].get('price', 0)
        mid_price = (best_bid + best_ask) / 2
        
        # Calculate impact relative to mid price
        if side == 'buy':
            impact_bps = (vwap - mid_price) / mid_price * 10000
        else:
            impact_bps = (mid_price - vwap) / mid_price * 10000
        
        return {
            'filled_quantity': filled_quantity,
            'remaining_quantity': 0,
            'vwap': vwap,
            'mid_price': mid_price,
            'impact_bps': impact_bps,
            'levels_used': len(levels_used),
            'order_book_exhausted': False
        }
    
    def estimate_slippage_from_depth(
        self,
        symbol: str,
        order_size: float,
        side: str,
        timestamp_ms: int,
        api_key: Optional[str] = None
    ) -> dict:
        """Full pipeline: fetch depth → simulate fill → return slippage estimate.
        
        This is the main entry point for order book replay slippage estimation.
        
        Args:
            symbol: Trading symbol (e.g., 'NVDA.US')
            order_size: Number of shares
            side: 'buy' or 'sell'
            timestamp_ms: Historical timestamp
            api_key: TickDB API key
        
        Returns:
            Comprehensive slippage report
        """
        order_book = self.fetch_historical_depth(symbol, timestamp_ms, api_key)
        fill_result = self.simulate_fill(order_book, order_size, side)
        
        return {
            'symbol': symbol,
            'order_size': order_size,
            'side': side,
            'timestamp_ms': timestamp_ms,
            'fill_result': fill_result,
            'is_warnings': fill_result.get('warnings', [])
        }

5. Calibration: Making Your Model Match Reality

A model is only as good as its parameters. Here is how to calibrate $\alpha$, $\eta$, and $\theta$ for your specific asset.

5.1 Estimating ADV

Average Daily Volume should not be a simple historical average. Use a volume-weighted average over a rolling window, excluding anomalous days:

def estimate_adv(
    kline_data: list[dict],
    window_days: int = 20,
    exclude_top_percentile: float = 0.05
) -> float:
    """Estimate ADV using trimmed mean approach.
    
    Excludes the top percentile of volume days (which can distort
    ADV estimates during earnings or news events) to get a more
    stable baseline.
    
    Args:
        kline_data: List of OHLCV candles from TickDB kline endpoint
        window_days: Number of days to average
        exclude_top_percentile: Fraction of top volume days to exclude
    
    Returns:
        Estimated ADV
    """
    if not kline_data:
        raise ValueError("kline_data is required for ADV estimation")
    
    # Extract volume from recent candles
    volumes = [candle.get('volume', 0) for candle in kline_data[-window_days:]]
    volumes = [v for v in volumes if v > 0]
    
    if not volumes:
        raise ValueError("No valid volume data found")
    
    # Sort for trimming
    volumes_sorted = sorted(volumes)
    trim_count = int(len(volumes_sorted) * exclude_top_percentile)
    
    # Trim top and bottom (bottom trim for zero-volume holidays)
    trimmed = volumes_sorted[trim_count:]
    
    if len(trimmed) < len(volumes_sorted) // 2:
        # Not enough data after trimming, use median instead
        return volumes_sorted[len(volumes_sorted) // 2]
    
    return sum(trimmed) / len(trimmed)

5.2 Calibrating the Alpha Coefficient

The most robust approach is to regress realized impact against predicted impact from your own execution data. If you have historical fills, fit $\alpha$ via ordinary least squares:

$$\hat{\alpha} = \frac{\sum(\text{realized_impact} \cdot \text{predicted_impact})}{\sum(\text{predicted_impact}^2)}$$

For assets where you have no execution data, use the cross-sectional calibration approach: fit $\alpha$ on similar assets (same market cap tier, same sector, same exchange) and apply the relationship:

def calibrate_alpha_from_sector(
    target_symbol: str,
    sector_symbols: list[str],
    tickdb_api_key: str,
    window_days: int = 60
) -> dict:
    """Calibrate alpha for a target symbol using sector benchmarks.
    
    This function fetches historical kline data for both the target
    and sector benchmark symbols, estimates realized volatility,
    and transfers the alpha parameter from the benchmark to the target
    using a liquidity-adjusted formula.
    
    Args:
        target_symbol: Symbol to calibrate
        sector_symbols: Reference symbols (e.g., sector ETF)
        tickdb_api_key: TickDB API key
        window_days: Historical window for calibration
    
    Returns:
        Calibrated parameters and confidence metrics
    """
    # Fetch historical data for reference symbol (e.g., XLF for financial sector)
    headers = {"X-API-Key": tickdb_api_key}
    reference_symbol = sector_symbols[0]  # Use first as reference
    
    # This would be a call to TickDB's kline endpoint
    # In production, implement actual API call with proper error handling
    reference_volatility = 0.018  # Placeholder - fetch from data
    target_volatility = 0.022    # Placeholder - fetch from data
    
    # Liquidity adjustment: more liquid assets have lower alpha
    reference_adv = 5_000_000     # Placeholder
    target_adv = 2_000_000        # Placeholder
    reference_alpha = 0.7         # Known from literature
    
    # Scale alpha inversely with ADV ratio
    adv_ratio = reference_adv / target_adv
    target_alpha = reference_alpha * math.sqrt(adv_ratio)
    
    # Clamp to reasonable range
    target_alpha = max(0.3, min(4.0, target_alpha))
    
    return {
        'target_symbol': target_symbol,
        'calibrated_alpha': target_alpha,
        'confidence': 'medium',  # Lower than direct calibration
        'method': 'cross_sectional_transfer',
        'reference_symbol': reference_symbol
    }

6. Integrating Slippage into Your Backtester

6.1 The Slippage-Aware Backtest Loop

The cleanest integration pattern is to wrap your existing fill logic with a slippage estimation layer:

class SlippageAwareBacktester:
    """Wrapper that adds realistic slippage to any backtester.
    
    This class intercepts fill_price calls and applies slippage
    adjustments based on order size, market conditions, and
    the selected model.
    """
    
    def __init__(
        self,
        slippage_config: SlippageConfig,
        model_type: str = "square_root"
    ):
        self.config = slippage_config
        
        if model_type == "square_root":
            self.model = SquareRootModel(slippage_config)
        elif model_type == "almgren_chriss":
            self.model = AlmgrenChrissModel(slippage_config)
        elif model_type == "order_book_replay":
            self.model = OrderBookReplayModel(slippage_config)
        else:
            raise ValueError(f"Unknown model type: {model_type}")
    
    def get_adjusted_fill_price(
        self,
        base_price: float,
        order_size: float,
        side: str,
        market_conditions: Optional[dict] = None
    ) -> dict:
        """Get slippage-adjusted fill price.
        
        Args:
            base_price: Mid price at signal time
            order_size: Number of shares
            side: 'buy' or 'sell'
            market_conditions: Dict with 'is_earnings', 'is_pre_market', etc.
        
        Returns:
            Dict with 'fill_price', 'slippage_bps', 'slippage_cost'
        """
        # Apply volatility modifier for special events
        volatility_multiplier = 1.0
        if market_conditions:
            if market_conditions.get('is_earnings'):
                volatility_multiplier *= self.config.high_volatility_multiplier
            if market_conditions.get('is_pre_market') or market_conditions.get('is_after_hours'):
                volatility_multiplier *= self.config.low_liquidity_multiplier
        
        # Estimate impact
        impact_bps = self.model.estimate_impact_bps(
            order_size,
            volatility=self.config.daily_volatility * volatility_multiplier
        )
        
        # Apply impact to price
        if side == "buy":
            fill_price = base_price * (1 + impact_bps / 10000)
        else:
            fill_price = base_price * (1 - impact_bps / 10000)
        
        # Estimate cost in dollars
        slippage_cost = abs(fill_price - base_price) * order_size
        
        return {
            'base_price': base_price,
            'fill_price': fill_price,
            'slippage_bps': impact_bps,
            'slippage_cost': slippage_cost,
            'slippage_pct': impact_bps / 100
        }
    
    def run_backtest_with_slippage(
        self,
        signals: list[dict],
        prices: list[float],
        config: SlippageConfig
    ) -> dict:
        """Run a complete backtest with slippage applied.
        
        Args:
            signals: List of {'date', 'order_size', 'side'} dicts
            prices: List of base prices (parallel to signals)
            config: SlippageConfig with asset parameters
        
        Returns:
            Performance report with slippage-adjusted returns
        """
        slippage_model = SquareRootModel(config)
        
        results = []
        total_slippage_cost = 0.0
        
        for signal, price in zip(signals, prices):
            result = slippage_model.estimate_round_trip_slippage(
                order_size=signal['order_size'],
                side=signal['side']
            )
            
            total_slippage_cost += result * signal['order_size']
            
            results.append({
                'date': signal['date'],
                'signal_size': signal['order_size'],
                'slippage_per_share': result,
                'base_price': price,
                'adjusted_price': price - result if signal['side'] == 'buy' else price + result
            })
        
        # Summary statistics
        avg_slippage_bps = sum(r['slippage_per_share'] / r['base_price'] * 10000 
                              for r in results) / len(results) if results else 0
        
        return {
            'num_trades': len(signals),
            'total_slippage_cost': total_slippage_cost,
            'avg_slippage_bps': avg_slippage_bps,
            'trade_results': results
        }

6.2 Example: Adding Slippage to an Event-Driven Strategy

Consider a simple momentum strategy triggered by earnings announcements:

def backtest_earnings_momentum_with_slippage(
    earnings_dates: list[dict],
    symbol: str,
    tickdb_api_key: str,
    base_strategy_return: float = 0.042  # Strategy return from historical backtest
) -> dict:
    """Demonstrate slippage impact on an earnings momentum strategy.
    
    Compares gross strategy return vs. net-of-slippage return.
    
    Returns:
        Comparison report showing slippage erosion
    """
    config = SlippageConfig(
        symbol=symbol,
        adv=10_000_000,  # 10M shares ADV
        daily_volatility=0.025,  # 2.5% daily vol
        square_root_alpha=0.7,   # Typical for large-cap US equity
        high_volatility_multiplier=1.5  # Earnings are high-vol events
    )
    
    slippage_model = SquareRootModel(config)
    
    # Typical earnings trade: 1% of ADV
    order_size = config.adv * 0.01  # 100,000 shares
    
    # Estimate slippage during earnings (high volatility period)
    impact_bps = slippage_model.estimate_impact_bps(
        order_size,
        volatility=config.daily_volatility * config.high_volatility_multiplier
    )
    
    # Round-trip (entry + exit)
    round_trip_impact = impact_bps * 2  # Bid and ask sides
    
    # Convert to return erosion
    slippage_cost_pct = round_trip_impact / 10000
    
    net_return = base_strategy_return - slippage_cost_pct
    
    return {
        'symbol': symbol,
        'order_size': order_size,
        'participation_rate': 0.01,
        'per_side_impact_bps': impact_bps,
        'round_trip_impact_bps': round_trip_impact,
        'gross_strategy_return_pct': base_strategy_return * 100,
        'slippage_cost_pct': slippage_cost_pct * 100,
        'net_strategy_return_pct': net_return * 100,
        'cost_as_pct_of_return': (slippage_cost_pct / base_strategy_return) * 100
    }

Sample output for NVDA with 1% participation rate:

Round-trip impact: 24.7 bps
Gross strategy return: 4.2%
Slippage cost: 0.247%
Net strategy return: 3.95%
Cost as % of return: 5.9%

For a small-cap stock with the same strategy parameters but $\alpha = 2.0$:

Round-trip impact: 70.6 bps
Gross strategy return: 4.2%
Slippage cost: 0.706%
Net strategy return: 3.49%
Cost as % of return: 16.8%

The difference is not trivial. For small-cap or illiquid strategies, slippage can consume 15–30% of gross returns. This is why calibration matters.


7. Best Practices and Common Pitfalls

7.1 Calibration Best Practices

Practice Why it matters
Use rolling windows for ADV, not fixed historical average ADV changes over time (meme stocks, index reconstitution)
Separate pre-event and post-event volatility for earnings Post-earnings vol can be 3–5× pre-event
Validate alpha against actual execution data when available Model assumptions may not hold for your specific order type
Include spread cost explicitly, not in impact Spread is deterministic; impact is probabilistic

7.2 Common Pitfalls

Pitfall 1: Ignoring intraday ADV.
If your strategy trades intraday, use intraday ADV (volume from the first 30 minutes as a proxy) rather than full-day ADV. A 2% participation rate of full-day ADV might represent 40% of the first-hour volume — a completely different impact profile.

Pitfall 2: Applying the same alpha to all market conditions.
Alpha estimated on calm days underestimates impact during stress periods by a factor of 2–3×. Always apply condition multipliers.

Pitfall 3: Forgetting the spread.
The square root and Almgren-Chriss models estimate impact above or below the mid-price. If you are a market taker, you must also pay the bid-ask spread. A complete slippage estimate is: total_slippage = spread/2 + impact.

Pitfall 4: Overfitting the calibration.
With only 20–30 historical fills, you cannot reliably estimate three parameters. Use sector transfer or academic defaults for the coefficients you cannot directly observe.


8. Closing

"Price is the effect. Liquidity is the cause."

Your backtester knows the price. It does not know the liquidity — not unless you teach it. The models in this article give your backtester that awareness: the square root model for rapid estimation, the Almgren-Chriss model for optimal execution analysis, and order book replay for precise, data-driven simulation.

The calibration section ensures your model reflects the actual behavior of your asset class, not generic defaults. And the integration examples show how to wire slippage into your existing backtest loop without rewriting everything from scratch.

The goal is not to make your backtest pessimistic. It is to make it honest. A strategy that looks good in a frictionless backtest and falls apart in live trading is not a trading edge — it is a documentation of the spread between theory and execution.

The good news: if your strategy survives a realistic slippage model, it has passed a meaningful test. If it does not, the slippage analysis tells you exactly what participation rate or asset liquidity would make it viable — giving you a clear path to either optimize the strategy or select the right market for deployment.


Next Steps

If you are building a backtester from scratch, start with the SquareRootModel class and the SlippageAwareBacktester wrapper. Calibrate your alpha using the sector transfer method, then validate with order book replay for your highest-conviction signals.

If you want to access historical OHLCV data for calibration, TickDB provides 10+ years of cleaned, aligned kline data for US equities via the /v1/market/kline endpoint. Set TICKDB_API_KEY in your environment and start fetching:

export TICKDB_API_KEY="your_api_key_here"

If you are analyzing order book depth for replay validation, the TickDB depth channel provides real-time depth snapshots. Note that historical depth is available for HK and crypto markets; for US equities, use the square root model with calibrated parameters as the primary slippage estimator.

If you need institutional-grade data for small-cap or international equities, reach out to enterprise@tickdb.ai for plans with extended depth coverage and historical depth replay.


This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Slippage models are approximations based on historical relationships and may not reflect future market conditions.