σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademySkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
Skills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Python skill›Module 3 · Probability & Monte Carlo simulation›Topic 3.1·~40 minIntermediate

Mean and variance of returns

Skill positionModule 3 of 11 · Topic 1 of 12
3.1 Mean and variance of returns
—Lab score— / 100Total XP—Streak—

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.

variance = sum((x - mean)^2) / (n - 1) std = sqrt(variance)
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.55

Annualising, 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)
A statistic comes from a sampleThe mean of 8 days is a very noisy estimate: its standard error is about 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 * periods and 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
Explorer · lab-54-1
▾ lab-54-1
…
bash · in-browser
learner@quantcraft:~/lab-54-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-54-1 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes

Log in to run your code.

Every lecture is free to read. Create an account or log in to run code, run tests and submit this lab.

  • In-browser IDE with a bash terminal — nothing to install
  • Instant grading, XP and your place on the skill
Create an accountLog in
OR
Continue with GitHub