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.
- Module
- 4.5 · Live trading system
- Duration
- 70 min
- Prerequisites
- 4.5.1 · Live system architecture: from data to reconciliation, 4.5.2 · Order management: states, partial fills, cancellations and reconnections, 4.3.1 · Implementation shortfall, VWAP, TWAP and Almgren-Chriss, 2.4.7 · Cost modeling: commissions, bid-ask spread, impact, financing and securities borrowing
- Author
- Jesús Cuesta
- Updated
Level 4 lessons
4.1 · Portfolio theory and optimisation
- 4.1.1 Markowitz: efficient frontier and real constraints
- 4.1.2 Covariance estimation: shrinkage, factor models and denoising
- 4.1.3 Black-Litterman: from market equilibrium to views
- 4.1.4 Risk parity, minimum variance, HRP and HERC
- 4.1.5 Optimization with CVaR, drawdown, turnover and costs
- 4.1.6 Factor models: CAPM, Fama-French, PCA and attribution
- 4.1.7 Rebalancing and strategy portfolios
- 4.1.8 Portfolio tools: Riskfolio-Lib, PyPortfolioOpt, skfolio and bt
4.2 · Risk management and position sizing
- 4.2.1 Risk per trade, stops and risk of ruin
- 4.2.2 Kelly criterion and volatility targeting: how much to risk
- 4.2.3 VaR, Expected Shortfall and VaR backtesting
- 4.2.4 Stress testing and correlation, liquidity, gap and counterparty risk
- 4.2.5 Drawdown control and kill switch
- 4.2.6 Derivatives risk: portfolio Greeks, margin and liquidations
4.3 · Microstructure and execution
4.4 · Brokers and APIs
- 4.4.1 Choosing a broker from Spain or the EU: regulation, assets, costs and API
- 4.4.2 Interactive Brokers: TWS and IB Gateway, ib_async, contracts, data and orders
- 4.4.3 Alpaca, Tradier, MetaTrader 5 with Python and OANDA v20
- 4.4.4 Crypto exchange APIs: ccxt, Binance, Bybit, Kraken and Coinbase
- 4.4.5 Secure credentials: environment variables, secret managers and least privilege
4.5 · Live trading system
- 4.5.1 Live system architecture: from data to reconciliation
- 4.5.2 Order management: states, partial fills, cancellations and reconnections
- 4.5.3 Paper trading, shadow trading and slippage monitoring
- 4.5.4 Task scheduling and alerts: cron, APScheduler, Prefect, Telegram, Slack and email
- 4.5.5 Fault tolerance: idempotency, persistent state and reconciliation at startup
- 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_envioinvivo4_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.
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.
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)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.71Weighted 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)..
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) 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.35In 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:
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"]))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]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
, with the ETF's half spread and a normal extra whose mean and
standard deviation depend on the time of day (vivo4_deslizamiento_verdad.csv). What was measured above is
plus the movement of the mid during the latency. To get to you need , 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):
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)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.00The 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.
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) 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.32The 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:
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) coef et verdad
open - close 3.64 0.70 2.0
remainder - close 0.87 0.35 0.5According 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.
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.
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"){'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 capitalOver 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.
with the slippage of fill , the model's, the expected standard deviation (1.5 bp at the close), (detects shifts of one sigma) and (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.
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])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-14With 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.
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
- 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
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))paper slippage 0.03 | half spread 0.0 | delay 1.67
real slippage 1.4 | half spread 0.55 | delay -0.43At 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.
- Does live market order slippage depend on the ETF? Compare the mean slippage per ETF with its estimated half spread.
Solution
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)) 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.53The 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.
- 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
-
Alpaca, “Paper Trading” (accessed on October 22, 2026; link in the resources). ↩
-
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
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.
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.
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.
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.
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.