The root-N law of trading
Worked examples
Read the claim, then run it and check the machine agrees. One at a time — nothing here is taken on trust.
import numpy as np
rng = np.random.default_rng(28)
mu, sigma = 0.01 / np.sqrt(252), 0.01 # true annual Sharpe exactly 1.0
rets = rng.standard_normal(252 * 400) * sigma + mu # 400 years, daily
def sharpe_at(returns, k): # SR of k-day aggregated returns
m = len(returns) // k * k
agg = returns[:m].reshape(-1, k).sum(axis=1)
return agg.mean() / agg.std(ddof=1)
daily = sharpe_at(rets, 1)
for label, k in [("daily", 1), ("weekly", 5), ("monthly", 21), ("yearly", 252)]:
print(f"{label:8s} SR {sharpe_at(rets, k):.4f} root-N law says {daily * np.sqrt(k):.4f}")
One return stream, measured at four horizons: 0.0628 daily, 0.1406 weekly, 0.2897 monthly, 1.0110 yearly — against the law's √k predictions of 0.0628, 0.1403, 0.2876, 0.9963. Four hundred years of data and the yearly estimate still wobbles 1.5% off the prediction; that residue is exactly the estimation noise the rest of this lesson is about. Note what the law needs: independent days. Autocorrelated returns break the √N stacking — m01-4 measures that.
import numpy as np
rng = np.random.default_rng(17)
mu, sigma = 0.01 / np.sqrt(252), 0.01 # true annual Sharpe 1.0
months = rng.standard_normal((120, 21)) * sigma + mu # 120 months
msr = months.mean(axis=1) / months.std(axis=1, ddof=1) * np.sqrt(252)
print(f"120 monthly estimates: mean {msr.mean():.2f}, std {msr.std(ddof=1):.2f}")
print(f"negative months : {(msr < 0).sum()} of 120")
print(f"months above 2.0 : {(msr > 2).sum()}; above 3.0: {(msr > 3).sum()}")
best, worst = msr.argmax(), msr.argmin()
print(f"best month : SR {msr[best]:+.2f} (return {months[best].sum():+.2%})")
print(f"worst month: SR {msr[worst]:+.2f} (return {months[worst].sum():+.2%})")
This is the slide's histogram, generated: the estimator is honest on average (mean 0.99 vs truth 1.00) and useless in any single month (std 4.15). The strategy that "made Sharpe 13 last month" and the one that "lost 10.5% last month" are the same strategy. Flip it around and the same table is a warning about SELECTION: if you run 120 strategy variants for one month and keep the best, you will report Sharpe 13 — m05-2 is entirely about that trap.
import numpy as np
import qadata
prices = qadata.trending_prices() # planted drift: +4bp/day, real
rets = prices.pct_change().dropna()
def t_stat_of(returns):
return returns.mean() / returns.std() * np.sqrt(len(returns))
for days, label in [(21, "1 month"), (63, "1 quarter"), (252, "1 year"), (755, "3 years")]:
print(f"after {label:9s}: t = {t_stat_of(rets.iloc[:days]):+.2f}")
daily_sr = rets.mean() / rets.std()
print(f"realized daily Sharpe {daily_sr:.4f} (annual {daily_sr * np.sqrt(252):.2f})")
print(f"days for t = 1.96: {(1.96 / daily_sr) ** 2:.0f} (~{(1.96 / daily_sr) ** 2 / 252:.1f} years)")
The t-stat stumbles upward exactly as √N promises — +0.34 after a month, −0.12 after a quarter (the sign is WRONG a quarter in), +0.77 after a year, +1.09 after all three — and never reaches 1.96. We buried a real 4bp/day edge in this market and three years of daily data cannot convict; the realized Sharpe of 0.63 needs ~9.7 years. When your m02 backtests start producing Sharpe numbers, this is the yardstick to hold against them.
Check the concept
One question at a time. Unsure? Revisit the lecture, then answer.
The challenge
Pass the quiz to unlock the challenge — your code will still be waiting here.