Paper 09 Execution Backtest Market Impact Latency

Slippage and Latency Modeling in Backtesting

Backtests are usually too optimistic for one simple reason: they assume the market waited for you. This paper decomposes latency, models fill prices with market impact, and shows why PnL arises from signal after implementation.

Abstract

Backtest engines routinely overstate strategy performance by ignoring the mechanics of order execution. This paper presents a latency decomposition framework and fill-price model incorporating spread crossing, market impact via square-root law, and stochastic drift during the decision-to-fill interval. Realistic slippage modeling is shown to be inseparable from strategy definition itself.

Key Takeaways

Introduction

Backtests are usually too optimistic for one simple reason: they assume the market waited for you. Between the instant a signal is computed and the instant an order is filled, several things happen. Threads wake up, messages are serialized, risk checks run, gateways forward packets, the venue processes the order, and other participants move the book. By the time the fill occurs, the price you thought you traded may no longer exist.

Latency Decomposition

Latency Decomposition
$$\Delta t = \Delta t_{\text{decision}} + \Delta t_{\text{queue}} + \Delta t_{\text{network}} + \Delta t_{\text{venue}}$$

A practical fill model for a buy order (where \(M_t\) is the reference midprice at decision time):

Fill Price Model
$$P_{\text{fill}} = M_{t+\Delta t} + \frac{1}{2}S_{t+\Delta t} + I(q) + \epsilon$$

Market Impact

A commonly used specification for market impact uses a square-root law:

Square-Root Impact Model
$$I(q) = \eta \sigma \sqrt{\frac{q}{V}}$$

where \(\sigma\) is volatility, \(V\) is available volume, and \(\eta\) is a calibrated coefficient.

Python Fill Simulator

fill_model.py Python
import numpy as np

def simulate_fill(mid, spread, sigma, q, V, latency_ms, side, eta=0.1):
    # side: +1 for buy, -1 for sell
    impact = eta * sigma * np.sqrt(max(q, 1) / max(V, 1))
    noise  = np.random.normal(0, spread * 0.05)

    # latency drift: price can move during delay
    drift      = np.random.normal(0, sigma * np.sqrt(latency_ms / 1000.0))
    future_mid = mid + drift

    fill = future_mid + side * (0.5 * spread + impact) + noise
    return fill

# Example
for side in [1, -1]:
    f = simulate_fill(
        mid=100.0, spread=0.02, sigma=0.01,
        q=10_000, V=1_000_000, latency_ms=8, side=side
    )
    print(ff"Fill ({'buy' if side > 0 else 'sell'}): {f:.4f}")

Implications

A strategy with a 1.5 Sharpe ratio in a frictionless backtest may collapse below 0.5 after realistic fill modeling. Mean reversion strategies are especially vulnerable because edge decays quickly and costs are frequent. The broader lesson: PnL does not arise from signal alone — it arises from signal after implementation. Slippage and latency are not "cost assumptions." They are part of the strategy definition.