The $0.04 That Nearly Cost Me a Strategy

The trade looked perfect on paper.

My mean-reversion strategy had fired a buy signal for AAPL at $182.50. The backtest showed entry at $182.52 — a 2-cent slippage, negligible. What I did not see in the backtest was the 400-share block that hit the tape 200 milliseconds before my order reached the exchange, compressing available liquidity at the bid. My market order filled at $182.88. A $0.38 per-share deviation on 400 shares is $152 in unexpected cost on a single signal.

Over 500 trades, a $0.04 average slippage sounds harmless. A $0.38 average slippage ends a strategy.

This article builds a production-grade slippage monitoring system that detects when live execution diverges from signal price beyond a configurable threshold. The system is asset-class agnostic — equities, futures, crypto — because the underlying problem is universal: backtests lie about execution costs.


Why Backtests Underestimate Real Slippage

Backtest slippage is a fiction by design. Most frameworks apply a fixed assumption — 1 basis point, 5 basis points — uniformly across all symbols and market conditions. This simplification produces clean equity curves but breaks contact with reality in three systematic ways.

Liquidity regimes change. A strategy tested on 2021's high-volume environment will experience materially different fills during 2022's volatility compression. The bid-ask spread for the same symbol can widen by 300% during news events without any change in the strategy's signal logic.

Order type assumptions are wrong. Backtests typically model execution as instantaneous at the close of the signal bar. In live trading, you face the choice between market orders (immediate but uncertain price) and limit orders (price certainty but fill uncertainty). Neither matches the backtest's assumption.

Queue position is invisible. At critical liquidity levels, your place in the order queue determines fill price. A strategy that ranks signals by score and submits them in batch will have later queue positions than the backtest assumes, resulting in worse fills on crowded symbols.

Quantified Slippage Thresholds

Not all slippage is created equal. The actionable question is not "what is my slippage?" but "at what slippage does my strategy's expected edge evaporate?"

Strategy type Typical edge per trade Warning threshold Kill threshold
High-frequency market making 1–3 bps 2 bps 5 bps
Intraday mean reversion 5–15 bps 10 bps 25 bps
Momentum (1–5 day hold) 20–50 bps 40 bps 80 bps
Swing trade (5–20 day hold) 50–150 bps 100 bps 200 bps
Event-driven 100–500 bps 200 bps 400 bps

The warning threshold is where you receive an alert. The kill threshold is where you suspend the strategy for manual review. Your specific thresholds depend on your strategy's measured edge, not generic industry benchmarks.


Slippage Calculation: The Formula

The slippage calculation has two components: the price deviation and the normalization factor that makes slippage comparable across price levels.

Slippage (bps) = (|Fill Price - Signal Price| / Signal Price) × 10,000

Direction-adjusted slippage = ((Fill Price - Signal Price) / Signal Price) × 10,000
  → Positive = adverse execution (worse than signal)
  → Negative = favorable execution (better than signal)

For a long signal at $182.50 filled at $182.88:

Slippage (bps) = (|182.88 - 182.50| / 182.50) × 10,000
               = (0.38 / 182.50) × 10,000
               = 20.8 bps adverse

For monitoring, you track three metrics:

  1. Per-trade slippage: The immediate signal for individual trade quality
  2. Rolling window slippage: Average slippage over the last N trades — detects regime shifts
  3. Slippage per symbol: Identifies which symbols consistently experience adverse execution

System Architecture

The monitoring system consists of four layers:

┌─────────────────────────────────────────────────────────────────┐
│                    Signal Generation Layer                       │
│         (your strategy — outputs: symbol, side, price)          │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                   Order Execution Layer                          │
│     (broker API — submits order, receives fill confirmation)     │
│     Outputs: fill_price, fill_quantity, fill_timestamp          │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                  Slippage Monitor Layer                          │
│  ┌──────────────┐  ┌──────────────┐  ┌──────────────────────┐   │
│  │ Per-trade    │  │ Rolling      │  │ Symbol-level         │   │
│  │ slippage     │  │ window avg   │  │ slippage aggregation │   │
│  │ calculator   │  │ (N trades)   │  │                      │   │
│  └──────────────┘  └──────────────┘  └──────────────────────┘   │
└─────────────────────────────┬───────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│                     Alerting Layer                               │
│     (webhook → Slack/PagerDuty; threshold breach → suspend)     │
└─────────────────────────────────────────────────────────────────┘

The critical design decision is that the slippage monitor runs as an independent service, not inside your strategy process. Decoupling means the monitor survives strategy crashes, does not introduce latency into signal generation, and provides an objective audit trail.


Production-Grade Implementation

The following code implements the slippage monitoring service. It receives execution reports from your broker, computes slippage against stored signal prices, and fires alerts via webhook when thresholds are breached.

"""
SlippageMonitor: Real-time execution quality monitoring service.

Monitors live fills against signal prices, computes slippage metrics,
and triggers alerts when execution deviates beyond configured thresholds.

Architecture:
    - Receives fill events from broker WebSocket or polling loop
    - Maintains in-memory signal store with TTL
    - Computes per-trade, rolling window, and symbol-level slippage
    - Fires webhook alerts on threshold breach

Engineering notes:
    - Run as independent process; do NOT embed in strategy runtime
    - Use a persistent signal store (Redis) for multi-process deployments
    - Ensure time synchronization between signal and fill sources (< 1 ms drift)
"""

import os
import json
import time
import logging
import threading
import statistics
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Optional
from collections import deque

import requests

# ⚠️ For production HFT workloads, consider aiohttp/asyncio for non-blocking I/O
# This synchronous implementation is suitable for strategies with > 100 ms signal latency

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s | %(levelname)s | %(name)s | %(message)s"
)
logger = logging.getLogger("SlippageMonitor")


# ============================================================================
# Configuration
# ============================================================================

@dataclass
class MonitorConfig:
    """Configuration for slippage monitoring thresholds and behavior."""

    # Per-trade thresholds (in basis points)
    warning_slippage_bps: float = 10.0
    kill_slippage_bps: float = 25.0

    # Rolling window configuration
    rolling_window_size: int = 50  # Number of trades for rolling average

    # Symbol-level thresholds
    symbol_warning_bps: float = 15.0  # Rolling avg per symbol

    # Alert configuration
    webhook_url: str = os.environ.get("SLIPPAGE_WEBHOOK_URL", "")
    alert_cooldown_seconds: int = 300  # Prevent alert spam

    # Signal store TTL (seconds)
    signal_ttl_seconds: int = 300  # Auto-expire stale signals


config = MonitorConfig()


# ============================================================================
# Data Models
# ============================================================================

@dataclass
class Signal:
    """Represents a trading signal generated by the strategy."""

    symbol: str
    side: str  # "BUY" or "SELL"
    signal_price: float
    signal_time: datetime
    strategy_id: str = "default"
    order_id: Optional[str] = None


@dataclass
class Fill:
    """Represents a confirmed trade execution from the broker."""

    symbol: str
    side: str
    fill_price: float
    fill_quantity: float
    fill_time: datetime
    order_id: Optional[str] = None
    commission: float = 0.0


@dataclass
class SlippageResult:
    """Computed slippage metrics for a single fill."""

    symbol: str
    signal_price: float
    fill_price: float
    slippage_bps: float
    direction: str  # "adverse" or "favorable"
    is_warning: bool
    is_kill: bool
    timestamp: datetime

    def to_dict(self) -> dict:
        return {
            "symbol": self.symbol,
            "signal_price": self.signal_price,
            "fill_price": self.fill_price,
            "slippage_bps": round(self.slippage_bps, 2),
            "direction": self.direction,
            "is_warning": self.is_warning,
            "is_kill": self.is_kill,
            "timestamp": self.timestamp.isoformat(),
        }


# ============================================================================
# Slippage Calculator
# ============================================================================

class SlippageCalculator:
    """Computes slippage metrics from signal and fill pairs."""

    @staticmethod
    def compute(signal: Signal, fill: Fill) -> SlippageResult:
        """
        Compute slippage in basis points.

        For a BUY signal: adverse = fill_price > signal_price
        For a SELL signal: adverse = fill_price < signal_price
        """
        price_diff = fill.fill_price - signal.signal_price

        # Normalize by signal price to get percentage
        slippage_pct = abs(price_diff) / signal.signal_price
        slippage_bps = slippage_pct * 10_000

        # Direction: adverse means we got worse execution than signal
        if signal.side == "BUY":
            adverse = fill.fill_price > signal.signal_price
        else:  # SELL
            adverse = fill.fill_price < signal.signal_price

        direction = "adverse" if adverse else "favorable"

        # Threshold checks
        is_warning = slippage_bps >= config.warning_slippage_bps
        is_kill = slippage_bps >= config.kill_slippage_bps

        return SlippageResult(
            symbol=fill.symbol,
            signal_price=signal.signal_price,
            fill_price=fill.fill_price,
            slippage_bps=slippage_bps,
            direction=direction,
            is_warning=is_warning,
            is_kill=is_kill,
            timestamp=fill.fill_time,
        )


# ============================================================================
# Signal Store
# ============================================================================

class SignalStore:
    """
    In-memory store for pending signals.
    
    For multi-process deployments, replace with Redis:
        - Key: f"signal:{symbol}:{order_id}"
        - Value: JSON serialized Signal
        - TTL: config.signal_ttl_seconds
    
    ⚠️ In-memory store is lost on process restart — use Redis for production
    """

    def __init__(self, ttl_seconds: int = 300):
        self.ttl_seconds = ttl_seconds
        self._store: dict[str, tuple[Signal, datetime]] = {}
        self._lock = threading.Lock()

    def put(self, signal: Signal) -> None:
        """Store a signal with timestamp for TTL tracking."""
        key = self._make_key(signal.symbol, signal.order_id)
        with self._lock:
            self._store[key] = (signal, datetime.now())
            logger.debug(f"Stored signal: {key}")

    def get(self, symbol: str, order_id: Optional[str] = None) -> Optional[Signal]:
        """Retrieve a signal if it exists and has not expired."""
        key = self._make_key(symbol, order_id)
        with self._lock:
            if key not in self._store:
                return None
            signal, stored_at = self._store[key]
            age = (datetime.now() - stored_at).total_seconds()
            if age > self.ttl_seconds:
                del self._store[key]
                logger.warning(f"Signal expired: {key} (age: {age:.1f}s)")
                return None
            return signal

    def remove(self, symbol: str, order_id: Optional[str] = None) -> None:
        """Remove a signal after fill is processed."""
        key = self._make_key(symbol, order_id)
        with self._lock:
            self._store.pop(key, None)

    def _make_key(self, symbol: str, order_id: Optional[str]) -> str:
        return f"{symbol}:{order_id or 'pending'}"

    def cleanup_expired(self) -> int:
        """Remove all expired signals. Returns count of removed signals."""
        now = datetime.now()
        removed = 0
        with self._lock:
            expired_keys = [
                k for k, (_, stored_at) in self._store.items()
                if (now - stored_at).total_seconds() > self.ttl_seconds
            ]
            for key in expired_keys:
                del self._store[key]
                removed += 1
        return removed


# ============================================================================
# Rolling Window Metrics
# ============================================================================

class RollingMetrics:
    """Maintains rolling window of slippage results for trend detection."""

    def __init__(self, window_size: int = 50):
        self.window_size = window_size
        self._all_trades: deque[SlippageResult] = deque(maxlen=window_size)
        self._by_symbol: dict[str, deque[SlippageResult]] = {}

    def add(self, result: SlippageResult) -> None:
        """Add a slippage result to rolling windows."""
        self._all_trades.append(result)
        if result.symbol not in self._by_symbol:
            self._by_symbol[result.symbol] = deque(maxlen=self.window_size)
        self._by_symbol[result.symbol].append(result)

    def average_slippage(self) -> Optional[float]:
        """Average slippage across all trades in window."""
        if not self._all_trades:
            return None
        return statistics.mean(r.slippage_bps for r in self._all_trades)

    def average_slippage_by_symbol(self, symbol: str) -> Optional[float]:
        """Average slippage for a specific symbol."""
        if symbol not in self._by_symbol or not self._by_symbol[symbol]:
            return None
        return statistics.mean(r.slippage_bps for r in self._by_symbol[symbol])

    def adverse_rate(self) -> float:
        """Percentage of trades with adverse execution."""
        if not self._all_trades:
            return 0.0
        adverse_count = sum(1 for r in self._all_trades if r.direction == "adverse")
        return adverse_count / len(self._all_trades) * 100

    def summary(self) -> dict:
        """Generate metrics summary for logging and alerting."""
        avg = self.average_slippage()
        return {
            "trades_in_window": len(self._all_trades),
            "average_slippage_bps": round(avg, 2) if avg else None,
            "adverse_rate_pct": round(self.adverse_rate(), 1),
            "symbol_count": len(self._by_symbol),
        }


# ============================================================================
# Alerting
# ============================================================================

class AlertManager:
    """Manages alert dispatch with cooldown to prevent spam."""

    def __init__(self, webhook_url: str, cooldown_seconds: int = 300):
        self.webhook_url = webhook_url
        self.cooldown_seconds = cooldown_seconds
        self._last_alert_time: dict[str, datetime] = {}

    def send_alert(
        self,
        level: str,
        message: str,
        data: dict,
        symbol: Optional[str] = None,
    ) -> bool:
        """
        Send alert via webhook if cooldown has elapsed.
        
        Args:
            level: "warning" or "kill"
            message: Human-readable alert message
            data: Metrics payload
            symbol: Optional symbol for cooldown tracking
        
        Returns:
            True if alert was sent, False if suppressed by cooldown
        """
        if not self.webhook_url:
            logger.warning("No webhook URL configured — alert not sent")
            return False

        # Cooldown key: combination of level and symbol
        cooldown_key = f"{level}:{symbol or 'global'}"
        last_time = self._last_alert_time.get(cooldown_key)
        if last_time:
            elapsed = (datetime.now() - last_time).total_seconds()
            if elapsed < self.cooldown_seconds:
                logger.debug(
                    f"Alert suppressed (cooldown): {cooldown_key} "
                    f"({elapsed:.0f}s elapsed, {self.cooldown_seconds}s cooldown)"
                )
                return False

        payload = {
            "alert_level": level,
            "message": message,
            "timestamp": datetime.now().isoformat(),
            "data": data,
        }

        try:
            # ⚠️ Timeout is mandatory — never leave a webhook call hanging
            response = requests.post(
                self.webhook_url,
                json=payload,
                headers={"Content-Type": "application/json"},
                timeout=(3.05, 10),
            )
            response.raise_for_status()
            self._last_alert_time[cooldown_key] = datetime.now()
            logger.info(f"Alert sent: {level} - {message}")
            return True
        except requests.exceptions.Timeout:
            logger.error(f"Webhook timeout: {self.webhook_url}")
            return False
        except requests.exceptions.RequestException as e:
            logger.error(f"Webhook error: {e}")
            return False


# ============================================================================
# Monitor Service
# ============================================================================

class SlippageMonitor:
    """
    Main slippage monitoring service.
    
    Integrates signal store, calculator, rolling metrics, and alerting.
    Processes fill events and computes slippage against stored signals.
    """

    def __init__(self, config: MonitorConfig):
        self.config = config
        self.signal_store = SignalStore(ttl_seconds=config.signal_ttl_seconds)
        self.calculator = SlippageCalculator()
        self.metrics = RollingMetrics(window_size=config.rolling_window_size)
        self.alert_manager = AlertManager(
            webhook_url=config.webhook_url,
            cooldown_seconds=config.alert_cooldown_seconds,
        )
        self._suspended_symbols: set[str] = set()

    def record_signal(self, signal: Signal) -> None:
        """Record a signal from the strategy for later slippage calculation."""
        self.signal_store.put(signal)
        logger.info(
            f"Signal recorded: {signal.symbol} {signal.side} @ {signal.signal_price}"
        )

    def process_fill(self, fill: Fill) -> Optional[SlippageResult]:
        """
        Process a fill event and compute slippage against stored signal.
        
        This is the main entry point for fill data from your broker API.
        """
        # Check if symbol is suspended
        if fill.symbol in self._suspended_symbols:
            logger.warning(
                f"Fill received for suspended symbol {fill.symbol} — skipping"
            )
            return None

        # Look up matching signal
        signal = self.signal_store.get(fill.symbol, fill.order_id)
        if signal is None:
            # Try without order_id for market orders
            signal = self.signal_store.get(fill.symbol)
            if signal is None:
                logger.warning(
                    f"No signal found for fill: {fill.symbol} @ {fill.fill_price}"
                )
                return None

        # Compute slippage
        result = self.calculator.compute(signal, fill)

        # Update rolling metrics
        self.metrics.add(result)

        # Log result
        direction_indicator = "⚠️" if result.is_warning else "✓"
        logger.info(
            f"{direction_indicator} Slippage: {result.symbol} | "
            f"Signal: {result.signal_price} | Fill: {result.fill_price} | "
            f"Slip: {result.slippage_bps:.1f} bps ({result.direction})"
        )

        # Handle alerts
        self._handle_alerts(result)

        # Remove processed signal
        self.signal_store.remove(fill.symbol, fill.order_id)

        return result

    def _handle_alerts(self, result: SlippageResult) -> None:
        """Evaluate alert conditions and dispatch notifications."""
        metrics_summary = self.metrics.summary()

        if result.is_kill:
            # Kill alert: suspend strategy for this symbol
            self._suspended_symbols.add(result.symbol)
            self.alert_manager.send_alert(
                level="kill",
                message=(
                    f"KILL THRESHOLD BREACH: {result.symbol} slippage "
                    f"{result.slippage_bps:.1f} bps exceeds kill threshold "
                    f"{self.config.kill_slippage_bps} bps. Strategy suspended."
                ),
                data={
                    "slippage_result": result.to_dict(),
                    "rolling_metrics": metrics_summary,
                    "suspended_symbols": list(self._suspended_symbols),
                },
                symbol=result.symbol,
            )
            logger.critical(
                f"Strategy KILLED for {result.symbol} — "
                f"manual review required"
            )

        elif result.is_warning:
            # Warning alert
            symbol_avg = self.metrics.average_slippage_by_symbol(result.symbol)
            self.alert_manager.send_alert(
                level="warning",
                message=(
                    f"WARNING: {result.symbol} slippage {result.slippage_bps:.1f} bps "
                    f"exceeds warning threshold {self.config.warning_slippage_bps} bps. "
                    f"Symbol rolling avg: {symbol_avg:.1f} bps."
                ),
                data={
                    "slippage_result": result.to_dict(),
                    "rolling_metrics": metrics_summary,
                    "symbol_rolling_avg_bps": round(symbol_avg, 2) if symbol_avg else None,
                },
                symbol=result.symbol,
            )

    def resume_symbol(self, symbol: str) -> None:
        """Manually resume trading for a suspended symbol."""
        if symbol in self._suspended_symbols:
            self._suspended_symbols.remove(symbol)
            logger.info(f"Symbol {symbol} resumed after manual review")

    def get_status(self) -> dict:
        """Return current monitor status for health checks."""
        return {
            "suspended_symbols": list(self._suspended_symbols),
            "pending_signals": len(self.signal_store._store),
            "rolling_metrics": self.metrics.summary(),
            "config": {
                "warning_bps": self.config.warning_slippage_bps,
                "kill_bps": self.config.kill_slippage_bps,
                "window_size": self.config.rolling_window_size,
            },
        }


# ============================================================================
# Broker API Integration Example
# ============================================================================

def simulate_broker_fill_stream(monitor: SlippageMonitor):
    """
    Simulates a broker WebSocket fill stream for demonstration.
    
    In production, replace with your broker's SDK:
    - Interactive Brokers: ib_insync
    - Alpaca: alpaca-trade-api
    - TD Ameritrade: td-ameritrade-python-api
    - Binance: python-binance
    
    ⚠️ Ensure clock synchronization between your strategy and broker
    """

    # Example: simulating a sequence of signals and fills
    test_signals = [
        Signal(
            symbol="AAPL",
            side="BUY",
            signal_price=182.50,
            signal_time=datetime.now(),
            order_id="ord-001",
        ),
        Signal(
            symbol="AAPL",
            side="BUY",
            signal_price=182.55,
            signal_time=datetime.now(),
            order_id="ord-002",
        ),
    ]

    # Record signals
    for sig in test_signals:
        monitor.record_signal(sig)

    # Simulate fills with varying slippage
    test_fills = [
        Fill(
            symbol="AAPL",
            side="BUY",
            fill_price=182.52,  # Good: 1.1 bps
            fill_quantity=100,
            fill_time=datetime.now(),
            order_id="ord-001",
        ),
        Fill(
            symbol="AAPL",
            side="BUY",
            fill_price=182.88,  # Bad: 18.1 bps — WARNING
            fill_quantity=100,
            fill_time=datetime.now(),
            order_id="ord-002",
        ),
    ]

    # Process fills
    for fill in test_fills:
        monitor.process_fill(fill)
        time.sleep(0.1)

    # Print final status
    import json as json_lib
    print("\n" + "=" * 60)
    print("MONITOR STATUS")
    print("=" * 60)
    print(json_lib.dumps(monitor.get_status(), indent=2, default=str))


# ============================================================================
# Main Entry Point
# ============================================================================

if __name__ == "__main__":
    # Initialize monitor with environment-based configuration
    monitor = SlippageMonitor(config)

    # Run simulation
    print("Running slippage monitor simulation...")
    simulate_broker_fill_stream(monitor)

Integration with Your Broker API

The monitor expects fill events in a structured format. You need to adapt the process_fill method to receive data from your specific broker. The integration pattern is the same regardless of provider.

Interactive Brokers (IBKR) Integration

from ib_insync import IB, Fill

def on_fill(ib: IB, fill: Fill, report):
    """IBKR fill event handler — translate to SlippageMonitor."""
    
    # Extract fill details from IBKR contract
    contract = fill.contract
    execution = fill.execution
    
    fill_event = Fill(
        symbol=contract.symbol,
        side="BUY" if execution.side == "BOT" else "SELL",
        fill_price=execution.price,
        fill_quantity=execution.shares,
        fill_time=datetime.fromtimestamp(execution.time),
        order_id=str(execution.orderId),
    )
    
    monitor.process_fill(fill_event)

Alpaca Integration

import alpaca_trade_api as alpaca

def on_alpaca_fill(event):
    """Alpaca WebSocket fill event handler."""
    
    fill_event = Fill(
        symbol=event.symbol,
        side=event.side.upper(),
        fill_price=float(event.price),
        fill_quantity=float(event.qty),
        fill_time=datetime.fromisoformat(event.timestamp.rstrip("Z")),
        order_id=event.order_id,
    )
    
    monitor.process_fill(fill_event)

# Attach to Alpaca streaming
stream = alpaca.Stream()
stream.subscribe_trades(on_alpaca_fill, "*")
stream.run()

The critical integration requirement is timestamp alignment. Your signal timestamp and fill timestamp must be synchronized to a common time source (NTP-synced clock). A 100 ms clock drift between your strategy and broker can produce phantom slippage readings that trigger false alerts.


Deployment Configuration by Scale

Deployment scenario Configuration Signal store Alert channel
Single strategy, retail Warning: 10 bps, Kill: 25 bps In-memory Email / Slack
Multiple strategies, individual Per-strategy thresholds Redis Slack / webhook
Prop desk, multiple traders Symbol-level thresholds + per-trader attribution Redis + PostgreSQL PagerDuty + Slack
Fund, full audit trail All metrics → TimescaleDB Redis + PostgreSQL PagerDuty + email + ticket system

For retail users, the in-memory store is sufficient. For anyone managing capital beyond hobby trading, Redis provides persistence across restarts and enables multi-process signal coordination.


Reading the Metrics: A Decision Framework

Once your monitor is running, the data becomes actionable through this decision tree:

Step 1: Check per-trade slippage. A single trade exceeding the kill threshold triggers immediate suspension. Do not wait for a pattern.

Step 2: Examine rolling average. If the rolling average over 50 trades exceeds 50% of your kill threshold, investigate. A slow drift in execution quality often precedes a regime change.

Step 3: Analyze by symbol. If one symbol consistently shows 2x the slippage of others, the problem is likely in that symbol's liquidity profile — consider widening your limit order prices or reducing position size.

Step 4: Check adverse rate. A 70%+ adverse rate means your strategy is systematically receiving worse execution than signaled. This typically indicates a signal timing issue, not a broker issue.


What Slippage Is Telling You

Slippage is a diagnostic signal, not just a cost. A strategy that consistently experiences 15 bps of adverse slippage on a 20 bps expected edge is barely profitable. A strategy that experiences 5 bps on the same edge is generating genuine alpha. The monitor is not just tracking costs — it is measuring whether your strategy's assumptions match reality.

When the monitor fires a kill alert, the question is not "how do I reduce slippage?" The question is "does this strategy have enough edge to survive execution costs?" If the answer is no, the correct response is to either adjust the strategy (wider signals, lower frequency, limit orders) or retire the strategy entirely.


Next Steps

If you want to implement this monitoring system today:

  1. Clone the SlippageMonitor class from this article
  2. Set the SLIPPAGE_WEBHOOK_URL environment variable to your Slack incoming webhook
  3. Integrate monitor.record_signal() into your strategy's signal generation
  4. Route your broker's fill events through monitor.process_fill()
  5. Start with warning=10 bps and kill=25 bps; calibrate from observed data

If you need historical execution data to establish baseline thresholds:
Access 10+ years of cleaned US equity OHLCV data via the TickDB /kline endpoint to backtest your strategy's signal prices against historical bars and establish realistic slippage expectations before going live.

If you are debugging an existing execution problem:
Run the monitor in shadow mode — record signals and fills but suppress alerts — for 2–3 trading days. The rolling metrics will reveal whether your execution issues are systemic or isolated events.


This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results. Slippage monitoring systems require calibration to your specific strategy characteristics and risk tolerance.