---
title: "2.2.3 Trading frequency and strategy capacity"
description: "Intraday, swing and position trading: turnover, cost sensitivity, net Sharpe and capacity with the square-root law of market impact, in Python."
url: "https://pythonparatrading.com/en/python-trading-course/level-2/trading-frequency-and-strategy-capacity/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-2/trading-frequency-and-strategy-capacity.png"
---

# 2.2.3 · Frequencies: intraday, swing and position trading, and the capacity of a strategy

> The shorter a strategy's horizon, the more trades it makes, the sooner it can be checked whether it works and the more costs and impact hurt it. This lesson measures all three with 5-minute bars and with daily data, and estimates how much capital each horizon takes before impact eats the edge.

- Course: [Level 2: Systematic strategies and rigorous backtesting](https://pythonparatrading.com/en/python-trading-course/level-2/)
- Module: 2.2 · Anatomy of a trading system
- Duration: 65 min
- Prerequisites: [2.2.2 · Universe, signal, filter, entry, exit, sizing and risk management](https://pythonparatrading.com/en/python-trading-course/level-2/universe-signal-entry-exit-position-sizing/), [1.1.5 · Costs, settlement, leverage and margin](https://pythonparatrading.com/en/python-trading-course/level-1/trading-costs-settlement-leverage-and-margin/), [1.6.3 · Seasonality: day of the week, month, turn of the month and intraday sessions](https://pythonparatrading.com/en/python-trading-course/level-1/seasonality-and-calendar-effects/)

## By the end of this lesson

- Tell the intraday, swing and position horizons apart by their turnover, their costs, their estimation error and their competitors.
- Measure a strategy's annual turnover and its net Sharpe at several cost levels, and check the approximation net SR ≈ gross SR − annual cost / volatility.
- Estimate market impact with the square-root law, with its parameters stated as assumptions.
- Compute a strategy's capacity curve (net return against capital) and the capital at which its edge goes to zero.

## Three horizons

A strategy's **horizon** is the typical time it holds a position. Three groups are usually distinguished, without
exact boundaries:

| Horizon | Typical holding period | Data it needs | Who it competes against | Its main problem |
|---|---|---|---|---|
| Intraday | Minutes to hours; no overnight position | Minute bars, ticks, order book | Market makers, HFT, execution algorithms | Costs and impact: every basis point counts |
| Swing | Days to weeks | Daily bars, sometimes hourly | Quant funds, discretionary traders | Noise: signals over days are weak |
| Position | Weeks to months | Daily and weekly, fundamentals | CTAs, asset managers | Few bets: many years are needed to check it |

The horizon is not a matter of style: it decides almost everything else. A chain of consequences that this lesson
measures link by link:

```mermaid
flowchart TB
  H[Short horizon] --> N[More trades]
  N --> T[More turnover]
  N --> A[More bets]
  T --> C[Higher annual cost]
  T --> K[Less capacity]
  A --> E[Fewer years of testing]
```

Two links work against it and one in favor. More turnover means more costs and less admissible capital; more bets
means that chance averages out sooner and the edge can be told apart with fewer years of data. All three depend on the
same number: how many times a year the strategy trades its capital.

## An intraday effect: KAPA's opening gap

The data in this section are 5-minute bars of **KAPA**, a fictitious ETF on an index, over 1,003 NYSE sessions from
2022 to 2025. They carry a small planted effect (level 2 README, section e): in the first half hour the price
recovers, on average, 10% of the opening **gap** (the difference between the open and the previous close). Outside the
first half hour, returns have no autocorrelation: that is the control.

```python
import numpy as np
import pandas as pd

b = pd.read_parquet("indice2_5min.parquet")
ny = b["ts"].dt.tz_convert("America/New_York")
b["dia"] = ny.dt.date
b["k"] = b.groupby("dia").cumcount()        # bar of the day, from 0
g = b.groupby("dia")
d = pd.DataFrame({
    "apertura": g["apertura"].first(),
    "cierre": g["cierre"].last(),
    "volumen": g["volumen"].sum(),
    "vol_30m": b[b["k"] < 6].groupby("dia")["volumen"].sum(),
    "c_10h": b[b["k"] == 5].set_index("dia")["cierre"],
    "c_12h": b[b["k"] == 29].set_index("dia")["cierre"],
    "c_1230": b[b["k"] == 35].set_index("dia")["cierre"],
    "c_1130": b[b["k"] == 23].set_index("dia")["cierre"],
})
d["hueco"] = np.log(d["apertura"] / d["cierre"].shift())
d["r_30m"] = np.log(d["c_10h"] / d["apertura"])
d = d.dropna()
print(len(d), "sessions with a gap")
d[["hueco", "r_30m"]].describe().loc[["mean", "std"]].mul(1e4).round(1)
```
```output
1002 sessions with a gap
      hueco  r_30m
mean    3.8   -0.3
std    60.8   47.9
```

The figures are in basis points (bp, hundredths of a percentage point). The average gap is almost zero, but its
standard deviation is about 60 bp: KAPA opens every day far from the previous close. Half sessions (bars 0 to 41) have
no 12:30 bar, which is why they drop out of the control below.

```python
import statsmodels.api as sm

def pendiente(y, x):
    m = sm.OLS(y, sm.add_constant(x)).fit(cov_type="HC1")
    return round(float(m.params.iloc[1]), 3), round(
        float(m.tvalues.iloc[1]), 2)

print("first half hour on the gap:",
      pendiente(d["r_30m"], d["hueco"]))
ctrl = d.dropna(subset=["c_1230"])
r_antes = np.log(ctrl["c_12h"] / ctrl["c_1130"])
r_despues = np.log(ctrl["c_1230"] / ctrl["c_12h"])
print("12:00-12:30 on 11:30-12:00 (control):",
      pendiente(r_despues, r_antes))

# strategy: against the gap from the open until 10:00
contra = -np.sign(d["hueco"]) * d["r_30m"]
sr_bruto = contra.mean() / contra.std() * np.sqrt(252)
print(f"per trade: {1e4 * contra.mean():.2f} bp "
      f"(std. dev. {1e4 * contra.std():.1f} bp)")
print(f"annual gross Sharpe: {sr_bruto:.2f}")
```
```output
first half hour on the gap: (-0.132, -3.52)
12:00-12:30 on 11:30-12:00 (control): (-0.01, -0.31)
per trade: 4.69 bp (std. dev. 47.6 bp)
annual gross Sharpe: 1.56
```

The slope of the regression of the first half hour's return on the gap is −0.132 with a t of −3.52 (with
heteroskedasticity-robust errors, lesson 2.1.1): a standard error of 0.04, so the planted −0.10 is less than one away.
That of the control, with the same data and another time of day, is −0.01 with a t of −0.31: it cannot be told apart
from zero. The simplest strategy that exploits the effect (sell at the open if the gap is up, buy if it is down, close
at 10:00) earns about 4.7 bp per trade on average with a standard deviation ten times larger, and an annual gross
Sharpe of 1.56: the figure of an excellent strategy. It still has to pay.

*Fig. — KAPA, 2022–2025: mean first half-hour return by decile of the opening gap (left) and, as a control, the 12:00–12:30 return by decile of the 11:30–12:00 one (right), with their regression slope.* Two decile point panels. Left, the opening gap goes from −83 to +113 bp and the first half-hour return falls from +3 to −12 bp: the regression line, highlighted, has slope −0.13 (planted effect −0.10). Right, the midday control: points move between −2.5 and +3 bp with no direction and the slope is −0.01.

## What it costs to trade every day

The costs of a trade are half the bid-ask spread (buy at the ask and sell at the bid) plus the commission. KAPA trades
around USD 370; with a one-cent spread (an assumption: the data do not include quotes) and the tiered fee of the
fictitious broker Alcor in `costes2_brokers.csv`:

```python
brokers = pd.read_csv("costes2_brokers.csv")
comision = brokers.query("plan == 'escalonado'")["tarifa"].iloc[1]
precio = d["apertura"].median()
coste_lado = (0.01 / 2 + comision) / precio      # as a decimal

def neto(r_op, coste, lados=2):
    x = r_op - lados * coste
    return x.mean() / x.std() * np.sqrt(252)

print(f"commission {comision} USD/share; median price {precio:.0f}")
print(f"cost per side: {1e4 * coste_lado:.2f} bp")
for mult in (1, 5, 10):
    print(f"cost x{mult:<2}: net Sharpe "
          f"{neto(contra, mult * coste_lado):.2f}")
print(f"cost per side that cancels the edge: "
      f"{1e4 * contra.mean() / 2:.2f} bp")
```
```output
commission 0.002 USD/share; median price 370
cost per side: 0.19 bp
cost x1 : net Sharpe 1.44
cost x5 : net Sharpe 0.93
cost x10: net Sharpe 0.30
cost per side that cancels the edge: 2.34 bp
```

With the assumed costs, barely 0.2 bp per side, the net Sharpe stays high. But the margin is narrow: at 2.3 bp per
side the edge disappears. A spread of two or three cents, one tick of slippage at the open (when the opening spread is
wider than during the rest of the session, lesson 1.1.3) or a retail commission are enough to get there. The strategy
lives in the last decimal of its costs.

## The same idea at different speeds

To see the effect of the horizon on a single family of strategies, take the trend of lesson 2.2.2 in its simplest
version: the sign of the L-session return, with the same risk per market, in the 24 futures. As L goes down, the
strategy changes its mind more often. Each market's cost is the one in its specifications file (typical slippage plus
commission per contract) divided by the contract's notional:

```python
fut = pd.read_parquet("futuros2_continuos.parquet")
spec = pd.read_csv("futuros2_especificaciones.csv",
                   index_col="mercado")
aj = fut.pivot(index="fecha", columns="mercado",
               values="cierre_ajustado")
ci = fut.pivot(index="fecha", columns="mercado", values="cierre")
spec = spec.loc[aj.columns]
r = aj.pct_change()
c_contrato = (spec["coste_tipico_ticks"] * spec["valor_tick"]
              + spec["comision_usd"])
c_frac = (c_contrato / (ci * spec["multiplicador"])).shift(1)
vol = r.ewm(span=60, min_periods=60).std() * np.sqrt(252)

def tendencia(L, mult_coste=1.0):
    s = np.sign(np.log(aj / aj.shift(L)))
    w = (s * 0.10 / np.sqrt(aj.shape[1]) / vol).shift(1)
    bruto = (w * r).sum(axis=1).loc["2001":]
    dw = w.diff().abs()
    coste = (dw * c_frac).sum(axis=1).loc["2001":] * mult_coste
    return bruto, coste, dw.sum(axis=1).loc["2001":]

def fila(bruto, coste, giro):
    sr = lambda x: x.mean() / x.std() * np.sqrt(252)
    v = bruto.std() * np.sqrt(252)
    return {"rotacion": 252 * giro.mean(), "vol": v,
            "sr_bruto": sr(bruto), "coste_anual": 252 * coste.mean(),
            "sr_neto": sr(bruto - coste),
            "aprox": sr(bruto) - 252 * coste.mean() / v}

ventanas = [2, 5, 10, 21, 63, 126, 252]
tabla = pd.DataFrame({L: fila(*tendencia(L)) for L in ventanas}).T
tabla.index.name = "L"
tabla.round(3)
```
```output
     rotacion    vol  sr_bruto  coste_anual  sr_neto  aprox
L
2     751.828  0.149     0.362        0.195   -0.952 -0.952
5     475.550  0.150     0.139        0.122   -0.673 -0.673
10    337.859  0.149     0.119        0.087   -0.466 -0.466
21    224.479  0.149     0.402        0.059    0.010  0.010
63    138.463  0.149     0.640        0.035    0.402  0.402
126   100.789  0.146     0.705        0.025    0.535  0.535
252    66.946  0.147     0.999        0.017    0.884  0.884
```

Turnover is the annual sum of the weight changes, in multiples of the capital. It falls steadily with the window: from
752 times a year with L = 2 to 67 with L = 252 (it includes the daily rebalancing to volatility, which also trades).
The `aprox` column is the quick rule:

$$
SR_{net} \approx SR_{gross} - \frac{T \cdot c}{\sigma}
$$

with $T$ the annual turnover, $c$ the cost per unit traded and $\sigma$ the strategy's annual volatility: the annual
cost in return, divided by the volatility, is subtracted from the Sharpe. It matches the computed net Sharpe to the
hundredth. It is the calculation worth doing **before** the backtest: with the expected turnover and a realistic cost,
one knows what gross Sharpe is needed for something to be left.

Short windows lose on both counts. Their gross signal is worse: the gross Sharpe rises from 0.40 with L = 21 to 1.00
with L = 252, because the planted term uses the sign of 252 sessions and a shorter window picks it up worse; below 21
sessions it stays between 0.12 and 0.36, a weak and noisy signal. And they cost more: with L = 2 the annual cost is
19.5% of the capital and the net Sharpe, −0.95. The figure at the top puts the seven windows and the gap strategy on a
plane of Sharpe against turnover: the net line moves away from the gross one the more the strategy turns over, and the
gap strategy, with 504 turns a year, only survives because its costs per trade are tiny.

## Cost sensitivity

The reference costs are an estimate. The useful question is how much worse they can be before the strategy stops being
worth it, and how many years are needed to tell its edge apart from chance:

```python
sens = {}
for L in ventanas:
    bruto, coste, _ = tendencia(L)
    sr = lambda x: x.mean() / x.std() * np.sqrt(252)
    sens[f"L={L}"] = {f"x{m}": sr(bruto - m * coste)
                      for m in (0, 1, 2, 3)}
sens["KAPA gaps"] = {f"x{m}": neto(contra, m * coste_lado)
                     for m in (0, 1, 2, 3)}
sens = pd.DataFrame(sens).T
sens["anos_t2"] = (2 / sens["x0"].clip(lower=0.05)) ** 2
sens.round(2)
```
```output
             x0    x1    x2    x3  anos_t2
L=2        0.36 -0.95 -2.26 -3.56    30.60
L=5        0.14 -0.67 -1.48 -2.29   206.57
L=10       0.12 -0.47 -1.05 -1.63   284.39
L=21       0.40  0.01 -0.38 -0.77    24.71
L=63       0.64  0.40  0.16 -0.07     9.77
L=126      0.71  0.53  0.36  0.19     8.04
L=252      1.00  0.88  0.77  0.65     4.01
KAPA gaps  1.56  1.44  1.31  1.18     1.64
```

*Fig. — Net Sharpe with reference costs multiplied by 0, 1, 2 and 3: trend on futures with 21, 63 and 252-session windows and the KAPA gap strategy.* Four lines falling with cost. KAPA gaps: from 1.56 to 1.18. 252-session trend, highlighted: from 1.00 to 0.65. 63: from 0.64 to −0.07, negative at triple cost. 21: from 0.40 to −0.78, negative already at double cost.

Two readings of the table:

- **Long windows withstand cost errors; short ones do not.** The 252-session one still has a Sharpe of 0.65 at triple
  the cost. The 21-session one is already at 0.01 with the reference costs and turns negative at double. A strategy
  whose result changes sign when the cost doubles depends above all on one number, the cost, that the backtest cannot
  measure well.
- **Short windows are checked sooner.** The `anos_t2` column is $(2 / SR)^2$: the years of data needed for an edge
  with that gross Sharpe to give a t of 2. With the gap strategy's Sharpe, 1.6 years are enough; with the 252-session
  trend, 4; with the 5- and 10-session ones, which have almost no gross signal, more than 200. That is why
  high-frequency desks can validate and discard ideas in weeks, while trend managers need decades of history (and
  suffer more from overfitting risk, lesson 2.5.4).

## Capacity: market impact

The costs in the previous section are proportional: the same percentage for USD 1,000 as for 100 million. **Market
impact** is not: a large order moves the price against whoever executes it while it is being executed, and the larger
it is, the more it moves it. The most stable empirical regularity is the **square-root law**[^1]: the average impact
of an order (as a fraction of the price) is

$$
I(Q) = Y \cdot \sigma_D \cdot \sqrt{\frac{Q}{V_D}}
$$

with $Q$ the size of the order, $V_D$ the asset's daily volume (in the same units), $\sigma_D$ its daily volatility and
$Y$ an empirical factor. In this lesson $Y = 1$ is taken as an **assumption**: the real value depends on the asset, the
period and how the order is executed, and it has to be estimated with one's own executions (lesson 2.4.7). The total
cost of the order is $Q \cdot I(Q)$, which grows as $Q^{1.5}$.

With impact, the net return is no longer constant and falls with the capital $A$. For a strategy with an annual gross
edge $\alpha$ that trades its capital $T$ times a year, split into daily orders of size $Q = A\,T/252$:

$$
\alpha_{net}(A) = \alpha - T\,c - T \cdot Y \sigma_D \sqrt{\frac{A\,T}{252\,V_D}}
$$

The capital that cancels the edge, **capacity** in the strict sense, comes from setting it to zero:

$$
A_0 = \frac{252\,V_D}{T} \left(\frac{\alpha - T\,c}{T\,Y\,\sigma_D}\right)^2
$$

Turnover appears cubed in the denominator: doubling turnover with the same annual edge divides capacity by eight. And
the dollar profit, $A \cdot \alpha_{net}(A)$, is at its maximum at $A^* = \frac{4}{9} A_0$: beyond that capital, each
dollar added reduces the total profit.

For the gap strategy, the quantities come from the KAPA data. Each day there are two orders the size of the capital
(entry and exit), so $T = 504$ and $Q = A$:

```python
V_usd = (d["volumen"] * d["cierre"]).median()
sigma_d = np.log(d["cierre"] / d["cierre"].shift()).std()
Y = 1.0                                     # assumption
alfa_op = contra.mean()                     # per trade

def neto_op(A):
    """Net return per trade (round trip)."""
    impacto = Y * sigma_d * np.sqrt(A / V_usd)
    return alfa_op - 2 * coste_lado - 2 * impacto

A0 = V_usd * ((alfa_op - 2 * coste_lado) / (2 * Y * sigma_d)) ** 2
print(f"median daily volume: {V_usd / 1e9:.1f} bn USD")
print(f"daily sigma: {sigma_d:.4f}")
print(f"capacity A0: {A0 / 1e6:.1f} M USD; optimum: "
      f"{4 / 9 * A0 / 1e6:.1f} M USD")
part = A0 / (d["vol_30m"] * d["cierre"]).median()
print(f"participation in the first half hour with A0: {part:.2%}")
for A in (1e6, 3e6, 1e7):
    print(f"A = {A / 1e6:>4.0f} M: {1e4 * neto_op(A):+.2f} bp "
          f"per trade, {252 * A * neto_op(A) / 1e3:,.0f} thousand "
          "USD a year")
```
```output
median daily volume: 20.8 bn USD
daily sigma: 0.0109
capacity A0: 8.2 M USD; optimum: 3.6 M USD
participation in the first half hour with A0: 0.27%
A =    1 M: +2.80 bp per trade, 71 thousand USD a year
A =    3 M: +1.70 bp per trade, 129 thousand USD a year
A =   10 M: -0.45 bp per trade, -114 thousand USD a year
```

That is the lesson of the section: a strategy with a gross Sharpe of 1.56 on the most liquid ETF in the data, with USD
20,800 million traded in a median session, stops making money with 8.2 million of capital. Not for lack of volume
(with $A_0$ it trades 0.27% of the first half hour's volume), but because its edge per trade is 4.7 bp and
square-root impact eats it first. The dollar profit peaks at 3.6 million: USD 129,000 a year with 3 million, and losses
with 10.

## Capacity curves by horizon

To compare horizons with the same yardstick, the last cell applies the impact formula to the 21- and 252-session trend
strategies, market by market: with each one's measured turnover, its daily volatility and its median daily dollar
volume, which comes from the `volumen` column of the futures file (contracts traded in the contract held) times the
price and the multiplier:

```python
# daily volume in USD and daily volatility of each future
fut["usd"] = (fut["volumen"] * fut["cierre"]
              * fut["mercado"].map(spec["multiplicador"]))
V_m = fut.groupby("mercado")["usd"].median()
sigma_m = r.std()

def curva_tendencia(L, capitales):
    bruto, coste, _ = tendencia(L)
    s = np.sign(np.log(aj / aj.shift(L)))
    w = (s * 0.10 / np.sqrt(aj.shape[1]) / vol).shift(1)
    T_m = 252 * w.diff().abs().loc["2001":].mean()   # per market
    alfa = 252 * (bruto - coste).mean()
    out = []
    for A in capitales:
        q = A * T_m / 252                   # daily order per market
        imp = (T_m * Y * sigma_m * np.sqrt(q / V_m)).sum()
        out.append(alfa - imp)
    return np.array(out)

print("median daily volume (bn USD):",
      (V_m / 1e9).round(1).agg(["min", "median", "max"]).to_dict())

capitales = np.logspace(6, 11, 11)          # from 1 M to 100,000 M USD
curvas = pd.DataFrame({
    "gaps": [252 * neto_op(A) for A in capitales],
    "L=21": curva_tendencia(21, capitales),
    "L=252": curva_tendencia(252, capitales),
}, index=[f"{A:.0e}" for A in capitales])
(100 * curvas).round(2)
```
```output
median daily volume (bn USD): {'min': 0.3, 'median': 6.3, 'max': 72.8}
          gaps    L=21  L=252
1e+06     7.06   -0.25  12.95
3e+06     4.11   -0.56  12.90
1e+07    -1.14   -1.11  12.82
3e+07   -10.48   -2.08  12.68
1e+08   -27.08   -3.81  12.42
3e+08   -56.61   -6.88  11.97
1e+09  -109.13  -12.34  11.17
3e+09  -202.51  -22.06   9.75
1e+10  -368.57  -39.34   7.21
3e+10  -663.87  -70.08   2.71
1e+11 -1188.99 -124.72  -5.30
```

*Fig. — Capacity curves with the square-root law (Y = 1, assumed): net annual return against capital for the KAPA gap strategy and the 21 and 252-session trend, with each future's own volume and volatility.* Logarithmic horizontal axis from 1 million to 100 billion USD. The gap strategy starts at 7.1% a year and crosses zero, highlighted, at 8.2 million; the 21-session trend is negative from the first million (−0.25%) and falls ever faster; the 252-session one starts at 12.95%, falls slowly and crosses zero between 30 and 100 billion.

Futures volume ranges from USD 0.3 to 72.8 billion a day depending on the market (median of 6.3): the smallest ones
are the first to feel the impact. The three curves fall with capital and cross zero at very different points. The gap
curve returns 7.1% a year with one million (with 7.6% volatility) and turns negative before 10 million. The
21-session trend curve is negative from the first million (−0.25%): its proportional costs already eat the whole edge
(a net Sharpe of 0.01) and impact only subtracts. The 252-session one, with the lowest turnover, returns 12.95% with
one million (with 15% volatility), still 7.21% with 10,000 million and 2.71% with 30,000, and crosses zero before
100,000 million under these assumptions. That is why the largest funds in the world are position traders and why
high-frequency strategies, with very high Sharpe ratios, take hardly any outside capital.

<aside class="callout" data-kind="aviso">

The absolute numbers in this section depend on stated assumptions: $Y = 1$, KAPA's one-cent spread and the 2000-2025
median volume as the available volume (futures volume grows 4.5% a year in the data, so at the end of the sample there
is more than the median). Changing them moves the curves (exercise 2 does it with $Y$). The ranking between horizons,
on the other hand, comes from turnover and is robust: capacity falls as $1/T^3$.

</aside>

## Exercises

1. The gap strategy enters at the open. If it entered at the close of the first bar (09:35), how much of the edge
   would be left? Measure it with the data.

**Solution.**

```python
c_0935 = b[b["k"] == 0].set_index("dia")["cierre"].reindex(d.index)
tarde = -np.sign(d["hueco"]) * np.log(d["c_10h"] / c_0935)
print(f"from the open: {1e4 * contra.mean():.2f} bp; "
      f"from 09:35: {1e4 * tarde.mean():.2f} bp")
print(f"net Sharpe from 09:35: {neto(tarde, coste_lado):.2f}")
```
```output
from the open: 4.69 bp; from 09:35: 3.99 bp
net Sharpe from 09:35: 1.44
```

Entering five minutes late leaves 3.99 bp of the 4.69: the first bar already captures part of the gap's reversal. With
these costs the net Sharpe barely changes (1.44 in both cases, because the return of the shorter window is also less
volatile), but the margin over the cost that cancels the edge narrows. Entering at the open requires an order in the
opening auction (lesson 1.1.2), with its own spread and its own execution risk.

2. Repeat the capacity calculation of the gap strategy with $Y = 0.5$ and with $Y = 2$. How does $A_0$ scale with
   $Y$? Check the answer with the formula.

**Solution.**

```python
for y in (0.5, 1.0, 2.0):
    a0 = V_usd * ((alfa_op - 2 * coste_lado)
                  / (2 * y * sigma_d)) ** 2
    print(f"Y = {y}: A0 = {a0 / 1e6:6.1f} M USD")
```
```output
Y = 0.5: A0 =   32.8 M USD
Y = 1.0: A0 =    8.2 M USD
Y = 2.0: A0 =    2.0 M USD
```

Capacity scales as $1/Y^2$: with half the impact, four times the capital; with double, a quarter. Since $Y$ is
uncertain, the estimated capacity is much more so: an error of a factor of 2 in $Y$ is an error of a factor of 4 in the
admissible capital. That is why managers estimate their own impact with their real executions before growing.

3. A swing strategy earns 6% a year gross with a turnover of 40 times the capital. Another one, a position strategy,
   earns 4% with a turnover of 4. With the formula for $A_0$ and the same asset, how many times more capacity does the
   second one have? (Ignore proportional costs.)

**Solution.**

With $c = 0$, $A_0 \propto \frac{1}{T}\left(\frac{\alpha}{T}\right)^2 = \frac{\alpha^2}{T^3}$. For the swing one,
$0.06^2 / 40^3 = 5.6 \times 10^{-8}$; for the position one, $0.04^2 / 4^3 = 2.5 \times 10^{-5}$. The ratio is about
444 times: with a smaller annual edge, the position strategy takes more than four hundred times as much capital,
because its turnover is ten times lower.

[^1]: Tóth et al. (2011) propose an explanation of the square-root law of impact based on the latent liquidity of the
order book and provide empirical evidence; Bouchaud, Bonart, Donier and Gould (2018) cover impact and optimal execution
with Nasdaq data. Both are in the resources.

## Self-assessment

1. A strategy has a gross Sharpe of 0.8 and a volatility of 10%, and turns over its capital 50 times a year at a cost of 2 bp per unit traded. What approximate net Sharpe is left?

   - A. 0.8
   - B. 0.7: the annual cost is 50 × 0.0002 = 1%, and divided by the volatility it takes 0.1 off the Sharpe
   - C. 0.4
   - D. Negative

   *Answer B: 0.7: the annual cost is 50 × 0.0002 = 1%, and divided by the volatility it takes 0.1 off the Sharpe.* The annual cost in return is turnover × cost per unit: 50 × 2 bp = 100 bp. Divided by the 10% volatility, that is 0.1 points of Sharpe. The approximation holds in the lesson's table with an error of hundredths.

2. Why can an intraday strategy be checked with fewer years of data than a position one?

   - A. Because intraday data are more reliable
   - B. Because it has no costs
   - C. Because it makes many more independent bets: with the same small Sharpe per trade, its annual Sharpe is higher and the t grows sooner with the square root of the years
   - D. Because it is not affected by survivorship bias

   *Answer C: Because it makes many more independent bets: with the same small Sharpe per trade, its annual Sharpe is higher and the t grows sooner with the square root of the years.* The years needed for a t of 2 are (2 / annual Sharpe)². With the gap strategy's gross Sharpe of 1.56, 1.6 years are enough; with the 1.00 of the 252-session trend, about 4. The flip side is that costs and capacity punish the short horizon much more.

3. According to the square-root law, if an order is multiplied by 4, how does its average impact per share change?

   - A. It is multiplied by 4
   - B. It does not change
   - C. It is divided by 2
   - D. It is multiplied by 2

   *Answer D: It is multiplied by 2.* Relative impact is Y·σ·sqrt(Q/V): proportional to the square root of the size. The total dollar cost, impact times size, grows as Q^1.5: four times the size, eight times the cost.

4. Why does a strategy's capacity fall so fast with turnover?

   - A. Because turnover multiplies three times: it spreads the annual edge over more trades, it makes each session trade more and it multiplies the number of times impact is paid
   - B. Because brokers charge more to those who trade a lot
   - C. It does not fall: capacity does not depend on turnover
   - D. Because high-turnover strategies have a lower Sharpe

   *Answer A: Because turnover multiplies three times: it spreads the annual edge over more trades, it makes each session trade more and it multiplies the number of times impact is paid.* With the square-root law, the capital at which the edge goes to zero is proportional to the volume divided by T and by the square of T·Y·σ over the annual edge; that is, to 1/T³ with everything else fixed. Doubling turnover divides capacity by eight.

5. The KAPA gap strategy has a gross Sharpe of 1.56 on the most liquid ETF in the data. What limits its capacity?

   - A. Impact: with an edge of about 4.7 bp per trade and two trades a day, square-root impact eats it with a few million USD
   - B. The volume of the first half hour: it cannot be executed
   - C. Commission costs
   - D. Nothing: it takes any amount of capital

   *Answer A: Impact: with an edge of about 4.7 bp per trade and two trades a day, square-root impact eats it with a few million USD.* With the assumption Y = 1, the impact of a USD 5 million order on a daily volume of about 20,800 million is σ·sqrt(Q/V) ≈ 1.1% × 0.016 ≈ 1.7 bp per side. Two sides are already 3.4 bp of the 4.7 available. Being able to execute the order (a 10% participation in the half hour) is not the limit.

## Key points

- The horizon sets turnover, and turnover sets what costs cost: net SR ≈ gross SR − turnover × cost / volatility. With the futures' reference costs, the 2-session trend goes from a gross Sharpe of 0.36 to −0.95; the 252-session one, from 1.00 to 0.88.
- The short horizon has a statistical advantage: more bets and fewer years to tell the edge apart from chance ((2 / Sharpe)² years for a t of 2).
- Market impact grows with the square root of the order size (Y·σ·sqrt(Q/V)); the total cost, with its 1.5 power. Y is an empirical parameter: here it is assumed to be 1, and that is stated.
- Capacity falls as 1/T³ with turnover. The KAPA gap strategy, with a gross Sharpe of 1.56, stops making money at about USD 8 million; the 252-session trend takes orders of magnitude more.
- The figures come from synthetic data with planted effects and from stated cost and impact assumptions; they serve to rank the horizons, not as an estimate for any real strategy.

## Resources

### Research

- [Quantitative models and algorithmic execution](https://pythonparatrading.com/en/quantitative-models-algorithmic-execution/) · Research-0009 · 11 min
- [Intraday seasonality: backtesting](https://pythonparatrading.com/en/seasonality-intraday-backtesting/) · Research-0025 · 8 min

### Books

- [Tóth, Lempérière, Deremble, de Lataillade, Kockelkoren and Bouchaud (2011): Anomalous Price Impact and the Critical Nature of Liquidity in Financial Markets. Physical Review X 1, 021006](https://doi.org/10.1103/PhysRevX.1.021006) · doi.org
- [Bouchaud, Bonart, Donier and Gould (2018): Trades, Quotes and Prices. Cambridge University Press](https://doi.org/10.1017/9781316659335) · doi.org

### Data

- [Level 2 data: KAPA 5-minute bars (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/indice2_5min.parquet) · Download · indice2_5min.parquet
- [Level 2 data: fictitious broker fees (CSV)](https://pythonparatrading.com/curso/datos/nivel-2/costes2_brokers.csv) · Download · costes2_brokers.csv

Previous lesson: [2.2.2 · Universe, signal, filter, entry, exit, sizing and risk management](https://pythonparatrading.com/en/python-trading-course/level-2/universe-signal-entry-exit-position-sizing/)

Next lesson: [2.2.4 · Research journal and experiment tracking against data snooping](https://pythonparatrading.com/en/python-trading-course/level-2/research-journal-experiment-tracking/)
