Volatility and annualization
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
import qadata
prices = qadata.trending_prices()
rets = prices.pct_change().dropna() # 755 daily returns
daily = rets.std() # ddof=1, pandas default
print(f"daily std : {daily:.6%}")
print(f"monthly (x sqrt 21) : {daily * np.sqrt(21):.4%}")
print(f"annual (x sqrt 252): {daily * np.sqrt(252):.4%}")
print(f"planted truth : {0.01 * np.sqrt(252):.4%}")
print()
print(f"wrong road 1, x252 : {daily * 252:.2%}")
print(f"wrong road 2, std(prices): {prices.std():.2f} (dollars of wandering, not risk)")
print(f"numpy ddof=0 flavour : {rets.to_numpy().std() * np.sqrt(252):.4%}")
The whole annualization story in one screen: 1.0170% a day is 4.66% a month is 16.14% a year — each hop multiplies by the square root of the horizon ratio, never the ratio itself. The sample lands near the planted 15.87% (why not exactly? 755 draws of noise — the next example puts a number on that). And the wrong roads announce themselves: a 256% "vol" or a 16-dollar "std of prices" should never survive a sanity glance again.
import numpy as np
import qadata
prices = qadata.trending_prices() # planted vol: 1%/day, constant
rets = prices.pct_change().dropna()
roll = rets.rolling(21).std() * np.sqrt(252)
print(f"valid windows: {roll.notna().sum()} of {len(roll)}")
print(f"min : {roll.min():.4%} on {roll.idxmin().date()}")
print(f"max : {roll.max():.4%} on {roll.idxmax().date()}")
print(f"last: {roll.iloc[-1]:.4%}")
print(f"max/min ratio: {roll.max() / roll.min():.2f}x — on CONSTANT true vol")
The estimate roams from 10.22% (Nov 2021) to 21.38% (Sep 2022) — a 2.09× swing — while the generator's true vol never moved. Nothing is wrong with the code; 21 observations is simply a small sample. Real markets DO have vol regimes (m08 trades them), but this is the null hypothesis you must beat before claiming one: a rolling window manufactures regimes for free.
import numpy as np
rng = np.random.default_rng(2024)
TRUE_DAILY = 0.01 # truth: 15.87% annualized
windows = rng.standard_normal((20000, 21)) * TRUE_DAILY
est = windows.std(axis=1, ddof=1) * np.sqrt(252)
lo, mid, hi = np.percentile(est, [5, 50, 95])
print(f"true ann vol : {TRUE_DAILY * np.sqrt(252):.4%}")
print(f"5th pct : {lo:.4%}")
print(f"median : {mid:.4%}")
print(f"95th pct : {hi:.4%}")
print(f"outside [13%, 19%]: {((est < 0.13) | (est > 0.19)).mean():.2%} of estimates")
print(f"theory: relative se = 1/sqrt(2n) = {1 / np.sqrt(2 * 21):.4%}")
Twenty thousand honest 21-day windows on a known 15.87% vol: the middle 90% of estimates spans 11.6% to 19.9%, and nearly a quarter fall outside 13-19%. The 1/√(2n) rule (≈15.4% relative error for n=21) is the back-of-envelope worth memorizing: to HALVE the noise you need 4× the window — and by then the market may genuinely have changed. That tension — precision vs staleness — never goes away.
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.