Lesson 2.2.3 · 65 min
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.
- Module
- 2.2 · Anatomy of a trading system
- Duration
- 65 min
- Prerequisites
- 2.2.2 · Universe, signal, filter, entry, exit, sizing and risk management, 1.1.5 · Costs, settlement, leverage and margin, 1.6.3 · Seasonality: day of the week, month, turn of the month and intraday sessions
- Author
- Jesús Cuesta
- Updated
Level 2 lessons
2.1 · Statistics and econometrics for trading
- 2.1.1 Hypothesis tests, p-values, bootstrap and regression with robust standard errors
- 2.1.2 Stationarity: ADF, KPSS, differencing and fractional differencing
- 2.1.3 ARMA and ARIMA models: autocorrelation (ACF) and partial autocorrelation (PACF)
- 2.1.4 Volatility models: ARCH, GARCH, EGARCH and GJR with the arch library
- 2.1.5 Cointegration: Engle-Granger, Johansen and the half-life of mean reversion
- 2.1.6 Kalman filter for dynamic hedge ratios
- 2.1.7 Regime detection: hidden Markov models and structural breaks
- 2.1.8 The multiple testing problem: Bonferroni, FDR and the “factor zoo”
2.2 · Anatomy of a trading system
2.3 · Strategy families
- 2.3.1 Trend following and momentum
- 2.3.2 Mean reversion: RSI and Bollinger, intraday reversal and gaps
- 2.3.3 Pairs trading and statistical arbitrage: distance, cointegration and Kalman
- 2.3.4 Carry in currencies, futures and bonds; seasonality and calendar effects
- 2.3.5 Factors (value, quality, low volatility, size) and event strategies
- 2.3.6 Introduction to market making: inventory, spread and adverse selection (Avellaneda-Stoikov)
- 2.3.7 Crypto-specific strategies: momentum, funding arbitrage and basis
2.4 · Backtesting: engines and paradigms
- 2.4.1 Vectorized backtest from scratch: matrices, lag, costs and limits
- 2.4.2 Event-driven backtest from scratch: loop, portfolio, orders and fills
- 2.4.3 vectorbt: massive parameter sweeps and portfolio simulation
- 2.4.4 backtesting.py and backtrader: two teaching engines and their status
- 2.4.5 NautilusTrader: architecture, data, strategies and the same code in backtest and live
- 2.4.6 LEAN/QuantConnect and Zipline Reloaded; bt for asset allocation
- 2.4.7 Cost modeling: commissions, bid-ask spread, impact, financing and securities borrowing
- 2.4.8 Fill modeling: limit orders, queue, partial fills, gaps and stops
2.5 · Biases and validation
- 2.5.1 Backtest biases: look-ahead, survivorship, selection and narrative
- 2.5.2 In-sample and out-of-sample: anchored and rolling walk-forward
- 2.5.3 Combinatorial purged cross-validation for strategies
- 2.5.4 Deflated Sharpe Ratio and Probability of Backtest Overfitting (PBO)
- 2.5.5 Monte Carlo: trade resampling, block bootstrap and signal permutations
- 2.5.6 Parameter sensitivity: plateaus versus peaks, robustness across assets and periods
- 2.5.7 Synthetic data for stress testing: GBM, GARCH and bootstrapping
- 2.5.8 “Honest backtest” checklist before moving to paper trading
2.6 · Performance metrics
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 source
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.
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)1002 sessions with a gap
hueco r_30m
mean 3.8 -0.3
std 60.8 47.9The 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.
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}")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.56The 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.
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:
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")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 bpWith 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:
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) 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.884Turnover 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:
with the annual turnover, the cost per unit traded and 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:
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) 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.64Two 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_t2column is : 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 law1Note 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.: the average impact of an order (as a fraction of the price) is
with the size of the order, the asset's daily volume (in the same units), its daily volatility and an empirical factor. In this lesson 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 , which grows as .
With impact, the net return is no longer constant and falls with the capital . For a strategy with an annual gross edge that trades its capital times a year, split into daily orders of size :
The capital that cancels the edge, capacity in the strict sense, comes from setting it to zero:
Turnover appears cubed in the denominator: doubling turnover with the same annual edge divides capacity by eight. And the dollar profit, , is at its maximum at : 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 and :
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")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 yearThat 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 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:
# 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)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.30Futures 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.
Exercises
- 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
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}")from the open: 4.69 bp; from 09:35: 3.99 bp
net Sharpe from 09:35: 1.44Entering 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.
- Repeat the capacity calculation of the gap strategy with and with . How does scale with ? Check the answer with the formula.
Solution
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")Y = 0.5: A0 = 32.8 M USD
Y = 1.0: A0 = 8.2 M USD
Y = 2.0: A0 = 2.0 M USDCapacity scales as : with half the impact, four times the capital; with double, a quarter. Since is uncertain, the estimated capacity is much more so: an error of a factor of 2 in 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.
- 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 and the same asset, how many times more capacity does the second one have? (Ignore proportional costs.)
Solution
With , . For the swing one, ; for the position one, . 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.
Footnotes
-
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. ↩
Each answer is folded under its question
Self-assessment
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?
- A0.8
- B0.7: the annual cost is 50 × 0.0002 = 1%, and divided by the volatility it takes 0.1 off the Sharpe
- C0.4
- DNegative
Show answer
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.
Why can an intraday strategy be checked with fewer years of data than a position one?
- ABecause intraday data are more reliable
- BBecause it has no costs
- CBecause 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
- DBecause it is not affected by survivorship bias
Show answer
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.
According to the square-root law, if an order is multiplied by 4, how does its average impact per share change?
- AIt is multiplied by 4
- BIt does not change
- CIt is divided by 2
- DIt is multiplied by 2
Show answer
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.
Why does a strategy's capacity fall so fast with turnover?
- ABecause 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
- BBecause brokers charge more to those who trade a lot
- CIt does not fall: capacity does not depend on turnover
- DBecause high-turnover strategies have a lower Sharpe
Show answer
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.
The KAPA gap strategy has a gross Sharpe of 1.56 on the most liquid ETF in the data. What limits its capacity?
- AImpact: 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
- BThe volume of the first half hour: it cannot be executed
- CCommission costs
- DNothing: it takes any amount of capital
Show answer
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.