---
title: "4.5.3 Paper trading, shadow trading and slippage"
description: "Backtest, paper and live of the same system compared fill by fill: paper fills at the mid, live pays a measurable bid-ask spread and slippage."
url: "https://pythonparatrading.com/en/python-trading-course/level-4/paper-trading-shadow-trading-and-slippage/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-4/paper-trading-shadow-trading-and-slippage.png"
---

# 4.5.3 · 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.

- Course: [Level 4: Portfolios, risk, execution and live trading](https://pythonparatrading.com/en/python-trading-course/level-4/)
- Module: 4.5 · Live trading system
- Duration: 70 min
- Prerequisites: [4.5.1 · Live system architecture: from data to reconciliation](https://pythonparatrading.com/en/python-trading-course/level-4/live-trading-system-architecture/), [4.5.2 · Order management: states, partial fills, cancellations and reconnections](https://pythonparatrading.com/en/python-trading-course/level-4/order-management-states-fills-and-reconnections/), [4.3.1 · Implementation shortfall, VWAP, TWAP and Almgren-Chriss](https://pythonparatrading.com/en/python-trading-course/level-4/implementation-shortfall-vwap-twap-almgren-chriss/), [2.4.7 · Cost modeling: commissions, bid-ask spread, impact, financing and securities borrowing](https://pythonparatrading.com/en/python-trading-course/level-2/backtest-transaction-cost-modeling/)

## By the end of this lesson

- Explain what a paper trading broker simulates and what it does not, and why its fills are optimistic.
- Decompose the difference between the backtest price and the live fill price into delay (the market moves before sending) and slippage (what it costs to execute).
- Measure slippage by order type and time of day, with its standard error, and check it against the known ground truth in the data.
- Build a shadow book (shadow trading) with the paper rules applied to the live orders and measure in dollars what paper hides.
- Monitor slippage live with a CUSUM against the cost model and know when a model is badly calibrated.

## 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.

```mermaid
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.

```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)
```
```output
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 price[^1]. 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 trading[^2].

```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)
```
```output
                        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:

```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"]))
```
```output
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).* Four rows on a logarithmic millisecond scale. Paper ack: median 31 ms (24 to 41). Live ack: median 87 ms (70 to 110), highlighted. Paper first fill: 5 ms (2.9 to 10). Live first fill: 62 ms (19 to 168).

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 \pm (h + \varepsilon)/10^4$, with $h$ 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 + \varepsilon$ plus the movement of the mid during the latency. To get to $\varepsilon$ you need $h$, 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):

```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)
```
```output
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.

```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)
```
```output
                        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:

```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)
```
```output
                   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.

```mermaid
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.

```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")
```
```output
{'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.* Three lines in dollars. In paper, from 2 September to 10 October, the cost moves between −189 and 327 USD and ends at 283. In the live account, from 13 October, the live line (highlighted) rises to 1,732 USD on 31 December; the shadow moves between −831 and 339 and ends at 27. The gap between live and shadow grows almost without interruption.

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.

$$
S_t = \max\!\left(0,\; S_{t-1} + \frac{d_t - \widehat{d}_t}{\sigma} - k\right),
\qquad \text{alarm if } S_t > h
$$

with $d_t$ the slippage of fill $t$, $\widehat{d}_t$ the model's, $\sigma$ the expected standard deviation (1.5 bp at
the close), $k = 0.5$ (detects shifts of one sigma) and $h = 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.

```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])
```
```output
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.

```mermaid
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.**

```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))
```
```output
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.

2. Does live market order slippage depend on the ETF? Compare the mean slippage per ETF with its estimated half
   spread.

**Solution.**

```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))
```
```output
              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.

3. 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.

[^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).

## 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?

   - 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
   - B. Because in paper the system traded better
   - C. Because the live account had more capital
   - D. By chance: with more data they would be equal

   *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?

   - A. Because it is in basis points
   - B. Because the backtest has no commissions
   - C. It is enough
   - 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

   *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?

   - A. The spreads
   - B. The differences in slippage between times of day (open versus close), not its level
   - C. The total slippage
   - D. Nothing

   *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?

   - A. Never
   - B. At the end of December
   - C. On the eighth closing fill of the live account, on October 21, a week after starting
   - D. Before starting

   *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?

   - A. Nothing
   - B. Higher returns
   - C. It avoids paying commissions
   - 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

   *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

### Research

- [Quantitative Models and Algorithmic Execution](https://pythonparatrading.com/en/quantitative-models-algorithmic-execution/) · Research-0009 · 11 min

### Documentation

- [Alpaca: paper trading (what it simulates and what it does not)](https://docs.alpaca.markets/docs/paper-trading) · docs.alpaca.markets
- [Interactive Brokers: paper account fills that do not correspond to the market](https://ibkb.interactivebrokers.com/article/252) · ibkb.interactivebrokers.com
- [statsmodels: OLS with robust errors (cov_type)](https://www.statsmodels.org/stable/generated/statsmodels.regression.linear_model.OLS.fit.html) · statsmodels.org

### Data

- [Level 4 data: what the backtest assumes for each signal (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/vivo4_backtest_fills.csv) · Download · vivo4_backtest_fills.csv
- [Level 4 data: true slippage by mode and time of day (CSV)](https://pythonparatrading.com/curso/datos/nivel-4/vivo4_deslizamiento_verdad.csv) · Download · vivo4_deslizamiento_verdad.csv

Previous lesson: [4.5.2 · Order management: states, partial fills, cancellations and reconnections](https://pythonparatrading.com/en/python-trading-course/level-4/order-management-states-fills-and-reconnections/)

Next lesson: [4.5.4 · Task scheduling and alerts: cron, APScheduler, Prefect, Telegram, Slack and email](https://pythonparatrading.com/en/python-trading-course/level-4/task-scheduling-and-trading-alerts/)
