Lesson 4.5.3 · 70 min

Paper trading, shadow trading and slippage monitoring

Paper trading promises what the backtest promises: fills at the mid price, no bid-ask spread and almost no latency. This lesson compares, fill by fill, the backtest, the paper account and the live account of the same system, separates delay from slippage, recovers the slippage parameters planted by order type and time of day, and builds the monitoring that would have raised the alarm in the first week.

Fig. — Extra slippage of market orders (over the half spread) measured in paper and live trading, with ±2 standard errors, against the value planted in the data
Level 4 lessons
  1. 4.1 · Portfolio theory and optimisation

    1. 4.1.1 Markowitz: efficient frontier and real constraints
    2. 4.1.2 Covariance estimation: shrinkage, factor models and denoising
    3. 4.1.3 Black-Litterman: from market equilibrium to views
    4. 4.1.4 Risk parity, minimum variance, HRP and HERC
    5. 4.1.5 Optimization with CVaR, drawdown, turnover and costs
    6. 4.1.6 Factor models: CAPM, Fama-French, PCA and attribution
    7. 4.1.7 Rebalancing and strategy portfolios
    8. 4.1.8 Portfolio tools: Riskfolio-Lib, PyPortfolioOpt, skfolio and bt
  2. 4.2 · Risk management and position sizing

    1. 4.2.1 Risk per trade, stops and risk of ruin
    2. 4.2.2 Kelly criterion and volatility targeting: how much to risk
    3. 4.2.3 VaR, Expected Shortfall and VaR backtesting
    4. 4.2.4 Stress testing and correlation, liquidity, gap and counterparty risk
    5. 4.2.5 Drawdown control and kill switch
    6. 4.2.6 Derivatives risk: portfolio Greeks, margin and liquidations
  3. 4.3 · Microstructure and execution

    1. 4.3.1 Implementation shortfall, VWAP, TWAP and Almgren-Chriss
    2. 4.3.2 Order flow imbalance, microstructure signals and latency
  4. 4.4 · Brokers and APIs

    1. 4.4.1 Choosing a broker from Spain or the EU: regulation, assets, costs and API
    2. 4.4.2 Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders
    3. 4.4.3 Alpaca, Tradier, MetaTrader 5 with Python and OANDA v20
    4. 4.4.4 Crypto exchange APIs: ccxt, Binance, Bybit, Kraken and Coinbase
    5. 4.4.5 Secure credentials: environment variables, secret managers and least privilege
  5. 4.5 · Live trading system

    1. 4.5.1 Live system architecture: from data to reconciliation
    2. 4.5.2 Order management: states, partial fills, cancellations and reconnections
    3. 4.5.3 Paper trading, shadow trading and slippage monitoring
    4. 4.5.4 Task scheduling and alerts: cron, APScheduler, Prefect, Telegram, Slack and email
    5. 4.5.5 Fault tolerance: idempotency, persistent state and reconciliation at startup
    6. 4.5.6 NautilusTrader or LEAN live: from backtest to production with the same code

Three prices for the same order

The checklist of lesson 2.5.8 ended with paper trading: the strategy runs in real time with simulated orders for a period fixed in advance, to check that what the backtest assumes actually happens. This lesson does it with numbers. The system in the level data (lessons 4.5.1 and 4.5.2) traded on a paper account from September 2 to October 10, 2025 and on a live account from October 13 to December 31, with the same rules and 1,000,000 USD in each. For each signal there are three prices:

  • Backtest (vivo4_backtest_fills.csv): what the backtest assumes. The mid at the time of the signal (15 minutes before the close) for market and limit orders, the official close for closing orders and the next session's open for the Monday orders that are executed at the open. No costs.
  • Mid at submission (precio_medio_envio in vivo4_ordenes.csv): the market mid price when the OMS sends the order. It is the reference for slippage.
  • Fill: the price of each fill the broker returned.
Backtest priceDelayMid at submissionSlippageFill
Mermaid source
flowchart TB
  B[Backtest price] --> R[Delay]
  R --> M[Mid at submission]
  M --> D[Slippage]
  D --> E[Fill]

The difference between backtest and fill splits in two. The delay is how much the market moves between the time the backtest assumes and the time of submission: if the signal has no short-term information, its mean is zero, but its noise is large. Slippage is what it costs to execute against the mid at the time of sending: half the spread, impact, latency. It is what a cost model (lessons 2.4.7 and 4.3.1) has to estimate, and what paper does not have.

IN [01]python
import numpy as np
import pandas as pd
import statsmodels.formula.api as smf

ordn = pd.read_csv("vivo4_ordenes.csv",
                   parse_dates=["ts_envio", "fecha"])
ev = pd.read_csv("vivo4_eventos_broker.csv")
bt = pd.read_csv("vivo4_backtest_fills.csv")
ej = (ev[ev["evento"].isin(["parcial", "ejecutada"])]
      .drop_duplicates("id_ejecucion"))          # lesson 4.5.2
x = (ej[["id_orden_cliente", "id_ejecucion", "cantidad_ultima",
         "precio_ultimo", "comision", "ts_broker"]]
     .merge(ordn, on="id_orden_cliente")
     .merge(bt[["id_senal", "precio_backtest", "supuesto"]],
            on="id_senal"))
x["s"] = np.where(x["lado"] == "compra", 1, -1)
x["efectivo"] = x["cantidad_ultima"] * x["precio_ultimo"]
pb = lambda a, b: x["s"] * (a / b - 1) * 1e4    # + = worse
x["total"] = pb(x["precio_ultimo"], x["precio_backtest"])
x["retraso"] = pb(x["precio_medio_envio"], x["precio_backtest"])
x["desliz"] = pb(x["precio_ultimo"], x["precio_medio_envio"])
print(len(x), "fills;", x.groupby("modo").size().to_dict())

def resumen(g):
    w = g["efectivo"]
    return pd.Series({c: np.average(g[c], weights=w)
                      for c in ("total", "retraso", "desliz")}
                     | {"efectivo_musd": w.sum() / 1e6,
                        "comisiones": g["comision"].sum()})

x.groupby("modo").apply(resumen, include_groups=False).round(2)
OUT [01]Fills with their three prices
531 fills; {'paper': 183, 'real': 348}
       total  retraso  desliz  efectivo_musd  comisiones
modo
paper  -0.04     0.27   -0.31           5.21      303.30
real    0.97    -0.77    1.74          11.70      593.71

Weighted by traded value, paper fills come out 0.04 bp better than the backtest and live ones 0.97 bp worse. But looking only at that column is misleading: in the live account the delay contributes −0.77 bp (the market moved in our favor between the signal and the submission, by chance) and execution slippage is 1.74 bp. In paper, slippage is −0.31 bp: the paper broker charges nothing for executing. Closing orders have a meaningless slippage against the mid at submission (they are executed ten minutes later, in the auction), so from here on slippage is measured only with market orders.

What a paper broker simulates

The paper simulator in the data fills market orders at the mid price of the instant of execution, with no spread and no extra slippage; the live account pays half the spread plus an extra that depends on the time of day. It is a simplification, but not a caricature: brokers document it. Alpaca says that its paper trading does not account for order market impact, information leakage, latency slippage, queue position for non-marketable limit orders, price improvement, regulatory fees or dividends, and that the order quantity is not checked against the quantity at the best price1Note 1 · Alpaca, “Paper Trading” (accessed on October 22, 2026; link in the resources).. Interactive Brokers warns that fills in its paper account may or may not fall within the market at the time and are not indicative of what would happen in live trading2Note 2 · Interactive Brokers, “I Received an Execution in my Paper Trader Account that Did Not Correspond to the Market at the Time, What Happened?”, updated November 11, 2024 (accessed on October 22, 2026; link in the resources)..

IN [02]python
mk = x[x["tipo"] == "mercado"].copy()
mk["momento"] = np.where(
    mk["motivo"] == "resto_de_limite", "resto_limite",
    np.where(mk["ts_envio"].dt.hour < 17, "apertura", "cierre"))

def media_et(g):
    return pd.Series({"n": len(g), "media": g.mean(),
                      "desv": g.std(),
                      "et": g.std() / np.sqrt(len(g))})

med = (mk.groupby(["modo", "momento"])["desliz"]
       .apply(media_et).unstack())
med.round(2)
OUT [02]Slippage of market orders, by mode and time of day
                        n  media  desv    et
modo  momento
paper cierre        108.0   0.02  0.56  0.05
      resto_limite   21.0   0.14  0.43  0.09
real  apertura       31.0   5.22  3.73  0.67
      cierre        141.0   1.49  1.55  0.13
      resto_limite   48.0   2.32  2.40  0.35

In paper, mean slippage against the mid at submission is practically zero (0.02 bp at the close, 0.14 bp on the limit order remainders): all that is left is how much the mid moves in the milliseconds between submission and fill. In live there are three times of day: the closing orders (cierre, 10 minutes before), the remainders of the limit orders that did not fill (resto_limite, three minutes before the close) and the Monday orders that are executed at Tuesday's open (apertura). Opening costs 5.2 bp, more than three times as much as closing (1.5 bp). And each mean comes with its standard error: with 31 opening fills, ±0.67 bp; with 141 closing fills, ±0.13 bp.

Latency is the other half of paper's promise. From the time the OMS sends until it receives the acknowledgment, and from the acknowledgment to the first fill at the broker:

IN [03]python
ac = ev[ev["evento"] == "aceptada"].merge(
    ordn[["id_orden_cliente", "ts_envio", "tipo"]])
ac["ms_acuse"] = (pd.to_datetime(ac["ts_recibido"])
                  - ac["ts_envio"]).dt.total_seconds() * 1e3
prim = (ej.groupby("id_orden_cliente")["ts_broker"].min()
        .rename("primera"))
ac = ac.join(prim, on="id_orden_cliente")
ac["ms_ejec"] = (pd.to_datetime(ac["primera"])
                 - pd.to_datetime(ac["ts_broker"])
                 ).dt.total_seconds() * 1e3
q = lambda s: s.quantile([0.1, 0.5, 0.9]).round(1).tolist()
corte = ac["ms_acuse"] > 10_000      # ack received after a drop
print("acks after a drop:", ac[corte].groupby("modo").size()
      .to_dict())
for modo, g in ac[~corte].groupby("modo"):
    print(f"{modo:5s} ack p10/p50/p90 ms:", q(g["ms_acuse"]),
          "| 1st market order fill:",
          q(g.loc[g["tipo"] == "mercado", "ms_ejec"]))
OUT [03]Paper and live latencies
acks after a drop: {'real': 42}
paper ack p10/p50/p90 ms: [24.0, 31.3, 40.7] | 1st market order fill: [2.9, 5.0, 10.2]
real  ack p10/p50/p90 ms: [69.8, 87.3, 110.0] | 1st market order fill: [19.4, 61.9, 167.9]
Fig. — Live system latencies in paper and live trading: from sending to the received ack and from acceptance to the first fill of a market order (median and 10th to 90th percentiles, log scale)

First, an exclusion: 42 acknowledgments from the live account arrived more than 10 seconds after submission. That is not latency but the connection drops of lesson 4.5.2: orders sent just before a drop receive their acknowledgment on reconnecting, minutes later. Without them, the median acknowledgment is 31 ms in paper and 87 ms in live; the first fill of a market order arrives in 5 ms in paper and in 62 ms in live, with a 90th percentile of 168 ms. For a system that trades at the close, a few tens of milliseconds change nothing; for one that reacts to every quote, paper misrepresents the most important part.

Recovering the planted parameters

The data have the known answer: the price of each live market order fill is the mid times 1±(h+ε)/1041 \pm (h + \varepsilon)/10^4, with hh the ETF's half spread and ε\varepsilon a normal extra whose mean and standard deviation depend on the time of day (vivo4_deslizamiento_verdad.csv). What was measured above is h+εh + \varepsilon plus the movement of the mid during the latency. To get to ε\varepsilon you need hh, and the data do not give it directly. It can be estimated with the system's rule for limit orders, which are placed a quarter of the spread from the mid (half spread = 2 × the distance from the limit to the mid):

IN [04]python
lim = ordn[ordn["tipo"] == "limite"]
s_l = np.where(lim["lado"] == "compra", 1, -1)
dist = s_l * (lim["precio_medio_envio"] - lim["precio_limite"]) \
    / lim["precio_medio_envio"] * 1e4
h = (2 * dist).groupby(lim["instrumento"]).agg(["mean", "count"])
print(h.round(2).T.to_string())
mk["h"] = np.where(mk["modo"] == "real",
                   mk["instrumento"].map(h["mean"]), 0.0)
OUT [04]The spread, estimated with the limit orders
instrumento   ORAU  ORCM  OREM   OREU   ORIG  ORRE   ORSC  ORTL   ORTN   ORUS
mean          0.67  1.59   1.6   0.88   0.36  0.93   0.49  0.72   0.33   0.19
count        13.00  6.00   9.0  12.00  13.00  6.00  17.00  9.00  14.00  12.00

The estimated half spreads range from 0.2 bp (ORUS, the US equity ETF) to 1.6 bp (OREM and ORCM, emerging markets and commodities). They are noisy estimates: the limit price is rounded to the cent, and in a 24 USD ETF one cent is 4 bp, ten times the distance being measured; with six orders, the error for ORCM is large. With a real broker, the spread is measured with the quotes (lesson 1.1.5); here the limit order rule is enough.

IN [05]python
verdad = pd.read_csv("vivo4_deslizamiento_verdad.csv")
mk["extra"] = mk["desliz"] - mk["h"]
est = (mk.groupby(["modo", "momento"])["extra"]
       .apply(media_et).unstack()[["n", "media", "et"]])
comp = est.join(verdad.set_index(["modo", "momento"]))
comp["z"] = (comp["media"] - comp["media_pb"]) / comp["et"]
comp.round(2)
OUT [05]The estimated extra against the ground truth
                        n  media    et  media_pb  desviacion_pb     z
modo  momento
paper cierre        108.0   0.02  0.05       0.0            0.0  0.31
      resto_limite   21.0   0.14  0.09       0.0            0.0  1.49
real  apertura       31.0   4.44  0.69       3.0            4.0  2.08
      cierre        141.0   0.85  0.13       1.0            1.5 -1.20
      resto_limite   48.0   1.61  0.34       1.5            2.0  0.32

The header figure summarizes it. At the close, the measured extra is 0.85 ± 0.13 bp against the true 1.0; on the limit order remainders, 1.61 ± 0.34 against 1.5. Both are within 1.2 standard errors of the truth. At the open, 4.44 ± 0.69 against 3.0: slightly more than two standard errors away, and with 31 fills from a handful of Mondays; it is the kind of difference where, with real data, you would not know whether it is chance or a regime change. Paper gives exactly 0: that is its rule. And the paper column makes clear what cannot be done: calibrate the cost model with paper.

The differences between times of day can be measured without knowing the spread. With a fixed effect per ETF, the spread of each one is absorbed and the regression gives how much more the open or the remainder costs than the close. Variances differ by time of day (almost 4 bp of standard deviation at the open, 1.5 at the close), so the errors have to be robust:

IN [06]python
real = mk[mk["modo"] == "real"]
fit = smf.ols("desliz ~ C(instrumento) + "
              "C(momento, Treatment('cierre'))", data=real
              ).fit(cov_type="HC1")
fila = fit.params.filter(like="momento")
tabla = pd.DataFrame({"coef": fila,
                      "et": fit.bse.filter(like="momento")})
tabla.index = ["open - close", "remainder - close"]
tabla["verdad"] = [3.0 - 1.0, 1.5 - 1.0]
tabla.round(2)
OUT [06]Differences between times of day with fixed effects
                   coef    et  verdad
open - close       3.64  0.70     2.0
remainder - close  0.87  0.35     0.5

According to the regression, the open costs about 3.6 bp more than the close, against 2.0 in truth: the same excess of the opens in the previous table, now with a robust standard error of about 0.7 bp. The remainder costs 0.9 bp more than the close, against 0.5. With fixed effects there is no level: the regression cannot say how much the close costs, only how much more everything else costs. For the level you need the spread.

A shadow book

Comparing September's paper with November's live trading mixes two things: the execution rule and each period's market. Shadow trading separates them: the same system keeps, alongside the live book, a book that applies a different execution rule to the same orders, on the same days. Here the shadow is the paper rule applied to the live orders: each market order fill at the mid at submission, limit orders at their limit and closing orders at the close. It is what the paper account would have said if it had kept running in parallel.

SignalOMSLive brokerShadow bookBacktestCompare
Mermaid source
flowchart TB
  S[Signal] --> O[OMS]
  O --> BR[Live broker]
  O --> SB[Shadow book]
  BT[Backtest] --> C[Compare]
  BR --> C
  SB --> C

A common variant is the opposite: running the new strategy in shadow mode, without sending orders, alongside the one already trading, with the same data and the same clock, and comparing its signals and its assumed fills before giving it money. The architecture of lesson 4.5.1 allows it without changes: another signal and another OMS subscribed to the same bus, with a simulated broker.

IN [07]python
r = x[x["modo"] == "real"].copy()
r["px_sombra"] = np.where(r["tipo"] == "mercado",
                          r["precio_medio_envio"], r["precio_ultimo"])
usd = lambda px: r["s"] * r["cantidad_ultima"] * (
    px - r["precio_backtest"])
r["coste_real"] = usd(r["precio_ultimo"]) + r["comision"]
r["coste_sombra"] = usd(r["px_sombra"]) + r["comision"]
tot = r[["coste_real", "coste_sombra"]].sum()
print(tot.round(2).to_dict())
oculto = tot["coste_real"] - tot["coste_sombra"]
efec_mk = r.loc[r["tipo"] == "mercado", "efectivo"].sum()
print(f"What paper hides: {oculto:,.2f} USD, "
      f"{oculto / efec_mk * 1e4:.2f} bp of the market order value")
import exchange_calendars as xcals
dias = len(xcals.get_calendar("XNYS").sessions_in_range(
    "2025-10-13", "2025-12-31"))
print(f"{dias} sessions; per year, {oculto / dias * 252:,.0f} USD, "
      f"{oculto / dias * 252 / 1e6:.2%} of capital")
OUT [07]Backtest, shadow and live in the live account
{'coste_real': 1732.02, 'coste_sombra': 27.02}
What paper hides: 1,705.00 USD, 2.49 bp of the market order value
56 sessions; per year, 7,672 USD, 0.77% of capital
Fig. — Cumulative cost versus the backtest (price and commissions) of the live system: paper account until 10 October and live account afterwards, with the shadow book that applies the paper rule to the live orders

Over the 56 sessions of the live account, the live book costs 1,732 USD against the backtest (slippage and commissions) and the shadow, 27 USD: the commissions almost offset by the delay, which played in our favor those days. The difference, 1,705 USD, is what paper hides: the slippage of market orders, 2.49 bp of the value executed with market orders. Over a year, about 7,700 USD, 0.77% of capital, for a strategy that expects a few percentage points a year. The figure shows the cumulative cost against the backtest for the two periods. In paper, the curve goes up and down with the delay (hundreds of dollars in a day) and ends at 283 USD, almost only commissions. In live, the live curve and the shadow go up and down together, because the delay is the same for both, but the distance between them grows almost without interruption: it is the slippage, paid on every order. No month of paper would have anticipated that distance.

Live slippage monitoring

Measuring slippage at the end of the quarter is not enough: you need to know early whether the cost model is wrong. The classic tool is Page's CUSUM: it accumulates the deviations of the observed slippage from what the model says, minus an allowance, and raises an alarm when the sum crosses a threshold.

St=max⁡ ⁣(0,  St−1+dt−d^tσ−k),alarm if St>hS_t = \max\!\left(0,\; S_{t-1} + \frac{d_t - \widehat{d}_t}{\sigma} - k\right), \qquad \text{alarm if } S_t > h

with dtd_t the slippage of fill tt, d^t\widehat{d}_t the model's, σ\sigma the expected standard deviation (1.5 bp at the close), k=0.5k = 0.5 (detects shifts of one sigma) and h=5h = 5 (few false alarms). It is applied to the closing fills of the live account, in order, with three cost models: the paper one (0), half spread only, and half spread plus 1 bp.

IN [08]python
def cusum(d, modelo, sigma=1.5, k=0.5, h=5.0):
    s, camino = 0.0, []
    for v in (d - modelo) / sigma:
        s = max(0.0, s + v - k)
        camino.append(s)
    camino = np.array(camino)
    salta = np.argmax(camino > h) if (camino > h).any() else None
    return camino, salta

cie = mk[(mk["modo"] == "real") & (mk["momento"] == "cierre")] \
    .sort_values("ts_broker")
modelos = {"paper (0)": 0.0, "half spread": cie["h"],
           "spread + 1 bp": cie["h"] + 1.0}
for nombre, mod in modelos.items():
    camino, i = cusum(cie["desliz"], mod)
    cuando = ("never" if i is None else
              f"fill {i + 1}, {cie['ts_broker'].iloc[i][:10]}")
    print(f"{nombre:17s} alarm: {cuando}")

ape = mk[(mk["modo"] == "real") & (mk["momento"] == "apertura")] \
    .sort_values("ts_broker")
_, i = cusum(ape["desliz"], ape["h"] + 1.0)
print("opens with the closing model: alarm on fill",
      i + 1, ape["ts_broker"].iloc[i][:10])
OUT [08]CUSUM of slippage at the close
paper (0)         alarm: fill 8, 2025-10-21
half spread       alarm: fill 9, 2025-10-21
spread + 1 bp     alarm: never
opens with the closing model: alarm on fill 2 2025-10-14

With the paper model, the alarm fires on the eighth closing fill, on October 21: six sessions after going live. With half spread only, one fill later: the 1 bp extra is small but constant. With spread plus 1 bp it never fires in eleven weeks: it is the right model. If the closing model were applied to the opens, it would fire on the second fill: cost models are calibrated and monitored by time of day and order type, not as a block.

noyesFillSlippageCUSUM by time of dayCrosses thresholdAlertRecalibrate model
Mermaid source
flowchart TB
  E[Fill] --> D[Slippage]
  D --> C[CUSUM by time of day]
  C --> A{Crosses threshold}
  A -- no --> E
  A -- yes --> AL[Alert]
  AL --> R[Recalibrate model]

The alert corrects nothing by itself: it says that the cost model with which the strategy was approved no longer describes reality. The next step is to recalibrate it with the live fills and rerun the backtest with it (lesson 2.4.7); if the strategy no longer covers its costs, it is scaled down or stopped (lesson 4.2.5). Sending the alert is lesson 4.5.4.

What paper does check

None of the above makes paper trading useless. The six weeks of paper in the data served to check that the live signals match those of the backtest (lesson 4.5.1), that the state machine withstands the broker's real messages and a disconnection (lesson 4.5.2), that rejections are handled correctly and that the system starts and stops every day without intervention. What they did not serve for is measuring costs. The practical way to combine it all: paper for operations, a small live account for costs (with the shadow in parallel), and full size only when the CUSUM has gone weeks without firing with the model the backtest was run with.

Exercises

  1. Repeat the decomposition of the first section with the closing market orders only. How much of the 1.74 bp of live slippage does the estimated half spread explain?
Solution
IN [09]python
c = mk[mk["momento"] == "cierre"]
for modo, g in c.groupby("modo"):
    w = g["efectivo"]
    print(modo, "slippage", round(np.average(g["desliz"],
                                             weights=w), 2),
          "| half spread", round(np.average(g["h"], weights=w), 2),
          "| delay", round(np.average(g["retraso"], weights=w), 2))
OUT [09]Closing market orders only
paper slippage 0.03 | half spread 0.0 | delay 1.67
real slippage 1.4 | half spread 0.55 | delay -0.43

At the close, the live account pays 1.4 bp weighted by traded value, of which the estimated half spread explains 0.55: the rest is the extra. In paper, 0.03. The delay, on the other hand, changes sign between periods (+1.67 bp in paper, −0.43 in live): it is market noise over five minutes, not a cost, and four months are not enough for its mean to get close to zero.

  1. Does live market order slippage depend on the ETF? Compare the mean slippage per ETF with its estimated half spread.
Solution
IN [10]python
por = (mk[mk["modo"] == "real"].groupby("instrumento")
       .agg(n=("desliz", "size"), desliz=("desliz", "mean"),
            h=("h", "first")))
por["desliz - h"] = por["desliz"] - por["h"]
print(por.round(2).sort_values("h").to_string())
print("desliz-h correlation:", round(por["desliz"].corr(por["h"]), 2))
OUT [10]Slippage and spread per ETF
              n  desliz     h  desliz - h
instrumento
ORUS         20    1.12  0.19        0.92
ORTN         25    2.85  0.33        2.53
ORIG         34    1.79  0.36        1.43
ORSC         28    2.10  0.49        1.61
ORAU         22    2.22  0.67        1.55
ORTL         27    1.36  0.72        0.64
OREU         19    3.58  0.88        2.70
ORRE         18    2.26  0.93        1.32
ORCM          9    3.04  1.59        1.45
OREM         18    2.68  1.60        1.08
desliz-h correlation: 0.53

The ETFs with wider spreads slip somewhat more (a correlation of 0.53 across the ten means), but what is left after subtracting the half spread ranges from 0.6 to 2.7 bp. With about 20 fills per ETF, mixing times of day (each ETF has a different proportion of opens, which cost three times as much) and spreads estimated with few orders, each mean has an error of half a point or more: the data do not allow distinguishing a per-ETF extra, and in the planted truth there is none, it only depends on the time of day. In real data, the extra also depends on the size of the order relative to liquidity (impact, lesson 4.3.1); here the orders are small relative to the ETFs' volume and that effect is absent.

  1. A strategy expects a gross profit of 3 bp per trade and trades at the close with market orders. With what was measured in the live account, how much is left? And if half of the trades were done at the open?
Solution

At the close, mean live slippage is 1.5 bp per fill and commissions add a little more (0.0035 USD per share: 0.35 bp on a 100 USD share): about 1.1 bp is left, little more than a third of what was expected. If half is executed at the open (5.2 bp), mean slippage rises to about 3.4 bp and the strategy loses money before commissions. Paper would have said a clean 3 bp in both cases.

Footnotes

  1. Alpaca, “Paper Trading” (accessed on October 22, 2026; link in the resources). ↩

  2. Interactive Brokers, “I Received an Execution in my Paper Trader Account that Did Not Correspond to the Market at the Time, What Happened?”, updated November 11, 2024 (accessed on October 22, 2026; link in the resources). ↩

Each answer is folded under its question

Self-assessment

  1. Why do the market order fills of the paper account in the data come out 0.02 bp from the mid and those of the live account 1.49 bp?

    • ABecause the paper broker fills at the mid price, with no bid-ask spread or slippage; the live account pays half the spread plus an extra
    • BBecause in paper the system traded better
    • CBecause the live account had more capital
    • DBy chance: with more data they would be equal
    Show answer

    Answer A. Because the paper broker fills at the mid price, with no bid-ask spread or slippage; the live account pays half the spread plus an extra

    It is the rule of the paper simulator in the data, and that of many brokers to some extent: Alpaca, for example, documents that its paper trading does not account for market impact, latency slippage or queue position. The 0.02 left in paper is the movement of the mid during the milliseconds of latency.

  2. The live account executes 0.97 bp worse than the backtest. Why is that figure not enough to calibrate the cost model?

    • ABecause it is in basis points
    • BBecause the backtest has no commissions
    • CIt is enough
    • DBecause it mixes the delay (the mid moves for 5 minutes between the signal and the submission, with zero mean but a lot of noise) with execution slippage
    Show answer

    Answer D. Because it mixes the delay (the mid moves for 5 minutes between the signal and the submission, with zero mean but a lot of noise) with execution slippage

    Against the mid at submission, the live account pays 1.74 bp; against the backtest, 0.97 bp, because the delay contributes −0.77 bp of pure noise over these four months. Execution cost is measured against the mid at the time of sending; the delay, separately.

  3. What does the regression of slippage on ETF and time-of-day fixed effects recover, without knowing the spreads?

    • AThe spreads
    • BThe differences in slippage between times of day (open versus close), not its level
    • CThe total slippage
    • DNothing
    Show answer

    Answer B. The differences in slippage between times of day (open versus close), not its level

    With a fixed effect per ETF, the spread of each ETF and the common level are confounded: only the differences are identified. For the level you need the spread, which in the lesson is estimated with the limit orders.

  4. A CUSUM monitors slippage at the close against a model calibrated on paper (expected cost 0). When does it fire?

    • ANever
    • BAt the end of December
    • COn the eighth closing fill of the live account, on October 21, a week after starting
    • DBefore starting
    Show answer

    Answer C. On the eighth closing fill of the live account, on October 21, a week after starting

    The live cost at the close is 1.5 bp per fill and the model says 0: the cumulative sum crosses the threshold in eight fills. With a model of half spread plus 1 bp it never fires: it is well calibrated.

  5. What does a shadow book add compared with comparing September's paper with November's live trading?

    • ANothing
    • BHigher returns
    • CIt avoids paying commissions
    • DIt compares the same orders, the same days and the same prices with two execution rules: the difference is only the rule, not the market
    Show answer

    Answer D. It compares the same orders, the same days and the same prices with two execution rules: the difference is only the rule, not the market

    Paper and live in different periods mix the execution rule with each period's market. The shadow applies the paper rule to the live orders: in the lesson, the difference is 1,705 USD in eleven weeks, 2.49 bp of the value executed with market orders.

Key points

  • Paper trading checks that the system works (signals, orders, states, reconnections); it does not measure costs. Its broker usually fills at the mid, with no queue or impact.
  • The difference between backtest and live splits into delay (zero-mean noise if the signal has no short-term information) and execution slippage (the cost that can be modeled).
  • Slippage depends on the time of day: in the data, opening with a market order on Monday costs about three times as much as executing at the close. A single model gets both wrong.
  • Every mean comes with its standard error: with 31 fills, opening slippage is measured to ±0.7 bp; with 141, closing slippage to ±0.13.
  • A shadow book applies a different execution rule to the same orders and isolates what the rule costs.
  • Live monitoring is a sequential test (CUSUM) of slippage against the model, by time of day: it fires within days if the model is the paper one.

Resources

Data