What the bell curve promises
Many risk models assume returns follow a normal distribution, because it makes everything computable with a mean and a standard deviation. The normal distribution is very precise about extremes. The probability of being more than k standard deviations away from the mean, in either direction, is erfc(k / sqrt(2)): 31.7% for 1, 4.55% for 2, 0.27% for 3, 0.0063% for 4. A 4-sigma day would then happen about once in 15,800 trading days, roughly once in 63 years, and a 5-sigma day once in several thousand years.
import math
import numpy as np
for k in (1, 2, 3, 4, 5):
probability = math.erfc(k / math.sqrt(2)) # P(|Z| > k) for a normal variable
print(k, f"{probability:.2e}")
# 1 3.17e-01
# 2 4.55e-02
# 3 2.70e-03
# 4 6.33e-05
# 5 5.73e-07
print(round(1 / math.erfc(4 / math.sqrt(2)))) # 15787 trading days between 4-sigma days, if returns were normal
print(round(1 / math.erfc(5 / math.sqrt(2)) / 252)) # 6922 years between 5-sigma daysMarkets have fat tails
Real markets produce 4-sigma and 5-sigma days many times in a career, in 1987, 2008, 2020. Their return distributions have fat tails: more extreme days than the bell allows (and a taller, narrower middle). A simple way to see it is the tail ratio: the observed share of returns beyond k standard deviations divided by the share a normal would give. A ratio near 1 means normal tails; 5 means extremes five times too frequent; for daily data at k = 4 it can be in the hundreds. Risk models that assume normality underestimate exactly the days that matter most.
returns = np.array([0.010, 0.012, 0.008, 0.011, 0.009, 0.010, 0.012, -0.060, 0.010, 0.009])
def tail_ratio(x, k):
observed = np.mean(np.abs(x - x.mean()) > k * x.std(ddof=1)) # share beyond k standard deviations
return observed / math.erfc(k / math.sqrt(2)) # divided by what a normal would give
print(round(float(tail_ratio(returns, 2)), 2)) # 2.2 the one crash is 2.2 times too frequent at 2 sigma (a tiny sample, though)The Jarque-Bera test
A formal test asks whether the sample could plausibly come from a normal distribution. The Jarque-Bera statistic combines the two shape measures of the previous topic: JB = n / 6 * (S² + K² / 4), with S the skewness and K the excess kurtosis. For a normal sample both are near 0, so JB is small. Under normality JB follows a chi-square distribution with 2 degrees of freedom, whose tail has the closed form exp(-JB / 2): that is the p-value. If it is below 0.05 you reject normality. The critical value is JB = 5.99. Our 10-day sample with one crash has JB = 22.4 and a p-value of 0.00001.
x = returns
n = len(x)
deviations = x - x.mean()
m2, m3, m4 = (np.mean(deviations ** k) for k in (2, 3, 4))
skew, kurt = m3 / m2 ** 1.5, m4 / m2 ** 2 - 3
jb = n / 6 * (skew ** 2 + kurt ** 2 / 4)
p_value = math.exp(-jb / 2)
print(round(float(jb), 2), p_value < 0.05) # 22.37 True normality rejected
print(f"{p_value:.1e}") # 1.4e-05# The p-value falls fast as the statistic grows: 5.99 is the 5% critical value
for jb in (0, 2, 5.99, 10, 22.37):
print(jb, round(math.exp(-jb / 2), 5))
# 0 1.0
# 2 0.36788
# 5.99 0.05004
# 10 0.00674
# 22.37 1e-05- A normal variable is more than k standard deviations away with probability
erfc(k / sqrt(2)): 4.55% for 2, 0.0063% for 4. - The tail ratio, observed share divided by the normal share, shows fat tails: markets are far above 1 at 4 sigma.
- Jarque-Bera:
n / 6 * (S**2 + K**2 / 4); the p-value isexp(-JB / 2); below 0.05, reject normality (JB above 5.99).
Next up: hypothesis tests: the t-test, deciding whether a mean return is really different from zero.
Lab · what's graded
- Score how far a sample is from normal with
jarque_bera25 pts - Turn the statistic into a p-value with
normality_p_value25 pts - Compute what the bell curve promises with
normal_tail_probability25 pts - Count how much fatter the tails are with
tail_ratio25 pts