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:
- Per-trade slippage: The immediate signal for individual trade quality
- Rolling window slippage: Average slippage over the last N trades — detects regime shifts
- 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:
- Clone the SlippageMonitor class from this article
- Set the
SLIPPAGE_WEBHOOK_URLenvironment variable to your Slack incoming webhook - Integrate
monitor.record_signal()into your strategy's signal generation - Route your broker's fill events through
monitor.process_fill() - 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.