Two numbers for a distribution
A column of returns is hard to compare with another. Two numbers summarise most of it: the mean says where the returns are centred and the variance how widely they scatter around that centre. For n returns, the mean is sum(x) / n; the deviations x - mean always add up to zero, so to measure the scatter you square them first. The sample variance is the sum of the squared deviations divided by n - 1, not n: the mean was estimated from the same data, which makes the deviations a little too small, and dividing by n - 1 corrects that.
import numpy as np
returns = np.array([0.004, -0.002, 0.001, 0.003, -0.004, 0.002, 0.006, -0.001])
mean = returns.mean()
deviations = returns - mean
print(round(mean, 5)) # 0.00113
print(abs(deviations.sum()) < 1e-12) # True the deviations always add up to zero
variance = (deviations ** 2).sum() / (len(returns) - 1)
print(round(variance, 8), round(returns.var(ddof=1), 8)) # 1.098e-05 1.098e-05 the same as numpy's var(ddof=1)Standard deviation and volatility
Squaring changed the units: a variance of returns is in "percent squared". Its square root, the standard deviation, is back in the units of the returns, and in finance it is called volatility. It is the most common measure of risk. A typical day for a large stock moves about 1 to 2%, which is a volatility of 0.01 to 0.02 per day. It is read relative to the mean: with a volatility of 1.2% a day, a -3.6% day is three standard deviations away.
std = np.sqrt(variance)
print(round(std, 6), round(returns.std(ddof=1), 6)) # 0.003314 0.003314 daily volatility of these returns
# How many standard deviations is the worst day from the mean?
print(round((returns.min() - mean) / std, 2)) # -1.55Annualising, and the Sharpe ratio
Daily numbers are small and hard to feel, so they are quoted per year. The mean grows linearly with time (252 trading days a year: multiply by 252), but the volatility grows with the square root of time: multiply by sqrt(252), about 15.9. The reason is that random daily moves partly cancel each other, so uncertainty accumulates more slowly than the mean. The Sharpe ratio divides the average return above the risk-free rate (the excess return) by the volatility: reward per unit of risk, annualised the same way. It lets you compare a quiet strategy with a wild one.
periods = 252
print(round(mean * periods, 4)) # 0.2835 annual mean: 28.35% (a made-up, very lucky sample)
print(round(std * np.sqrt(periods), 4)) # 0.0526 annual volatility: 5.26%
risk_free = 0.0001 # risk-free return of one day
excess = returns - risk_free
sharpe = excess.mean() / np.sqrt(((excess - excess.mean()) ** 2).sum() / (len(excess) - 1)) * np.sqrt(periods)
print(round(sharpe, 3)) # 4.91 absurdly good, because 8 lucky days prove nothing# How precisely do 8 days pin down the mean? The standard error is std / sqrt(n)
standard_error = std / np.sqrt(len(returns))
print(round(float(standard_error), 6)) # 0.001172 larger than the mean itself (0.00113)std / sqrt(n), here 0.0012, larger than the mean itself (0.0011). A great Sharpe ratio over a few weeks says little; the more observations, the more you can trust a mean, and risk figures are far more stable than average returns.- Mean:
x.mean(); sample variance:((x - mean) ** 2).sum() / (n - 1); volatility is its square root. - Annualise the mean by
* periodsand the volatility by* sqrt(periods): risk grows with the square root of time. - Sharpe = mean excess return / its standard deviation *
sqrt(periods): reward per unit of risk; short samples make it unreliable.
Next up: skewness and kurtosis, the two statistics that describe the shape of the distribution beyond its centre and spread.
Lab · what's graded
- Measure the spread of returns with
sample_variance25 pts - Put the spread in the units of the returns with
sample_std25 pts - Annualise the mean and the volatility with
annualized_stats25 pts - Rate the return per unit of risk with
sharpe_ratio25 pts