σμ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.6·~45 minIntermediate

Volatility

Skill positionModule 3 of 11 · Topic 6 of 12
3.6 Volatility
—Lab score— / 100Total XP—Streak—

Risk, in one number

Volatility is the standard deviation of returns, quoted per year so that assets can be compared: about 15 to 20% for a stock index, 30 to 60% for a single stock, near 0 for cash. It sizes positions, prices options and sets risk limits. The simplest estimate is the realized volatility over a window: the sample standard deviation of the returns times sqrt(periods per year). It treats every day in the window equally, which is both its strength (stable) and its weakness (slow).

import math
import numpy as np

# Eight quiet days, one -3% shock, three more days
returns = np.array([0.004, -0.002, 0.001, 0.003, -0.004, 0.002, 0.006, -0.001, -0.030, 0.004, 0.002, -0.003])

print(round(returns[:8].std(ddof=1) * math.sqrt(252), 4))    # 0.0526   before the shock: 5.3% a year
print(round(returns.std(ddof=1) * math.sqrt(252), 4))        # 0.1505   with the shock: 15.1% a year

Reacting faster: EWMA

Volatility clusters: calm periods and stormy periods each last a while, so yesterday's shock says more about tomorrow than a quiet day a month ago. The exponentially weighted moving average (EWMA, popularised as RiskMetrics) updates the variance each day: variance_t = lam * variance_(t-1) + (1 - lam) * return_t². With lam = 0.94 the newest return carries 6% of the weight and old ones fade geometrically. The estimate jumps at a shock and decays gradually, instead of staying high until the shock leaves a window, then dropping all at once.

lam = 0.94
variance = np.empty(len(returns))
variance[0] = returns[0] ** 2
for t in range(1, len(returns)):
    variance[t] = lam * variance[t - 1] + (1 - lam) * returns[t] ** 2    # the recursion needs the previous value

ewma = np.sqrt(variance)
print(ewma.round(5).tolist())
# [0.004, 0.00391, 0.0038, 0.00375, 0.00377, 0.00369, 0.00387, 0.00376, 0.0082, 0.00801, 0.00778, 0.00758]
#                                                                       ^ the shock doubles the estimate, then it fades
print(round(ewma[-1] * math.sqrt(252), 4))     # 0.1204   today's annualised estimate

Using the range, and targeting a volatility

Closing prices ignore what happened during the day. The daily high and low carry more information: Parkinson's estimator sqrt(mean(ln(high/low)²) / (4 ln 2)) is several times more efficient than the close-to-close estimate, meaning it reaches the same accuracy with far fewer days (it assumes no big gaps between days). A practical use of any volatility estimate is volatility targeting: if you want 10% risk and the realized volatility is 15%, hold 10 / 15 = 0.67 of the position. The leverage shrinks in storms and grows in calm.

high = np.array([101.5, 102.0, 103.2, 102.8, 104.0])
low = np.array([99.8, 100.9, 101.7, 101.1, 102.4])
parkinson = math.sqrt(np.mean(np.log(high / low) ** 2) / (4 * math.log(2)))
print(round(parkinson, 5))                      # 0.00905   per-day volatility from the daily ranges

target = 0.10
realized = returns.std(ddof=1) * math.sqrt(252)
print(round(target / realized, 3))              # 0.664   hold two thirds of the position to run at 10% risk
# Close-to-close versus range: the same five days
close = np.array([100.0, 101.2, 102.5, 101.9, 103.4])
print(round(float(np.diff(np.log(close)).std(ddof=1)), 5))     # 0.00955   from the closes only
print(round(parkinson, 5))                                      # 0.00905   from the high-low ranges of the same days
Volatility is not direction, and it movesVolatility treats a +3% day and a -3% day alike, but investors fear only the second. It is also not constant: every estimate depends on its window or decay, and the 252 trading days assumption is a convention. State which estimator, window and annualisation you used whenever you quote a volatility.
  • Realized volatility: sample std times sqrt(periods per year); stable but slow to react.
  • EWMA: variance_t = lam * variance_(t-1) + (1 - lam) * return_t²; recent shocks weigh more and fade geometrically.
  • Parkinson uses the daily high/low range; volatility targeting scales the position by target / realized.

Next up: the Sharpe ratio, reward per unit of risk, and how far to trust it.

Lab · what's graded

  • Measure the yearly volatility with realized_volatility25 pts
  • Let recent shocks count more with ewma_volatility25 pts
  • Use the daily range with parkinson_volatility25 pts
  • Size a position to a risk budget with volatility_target_leverage25 pts
Explorer · lab-59-1
▾ lab-59-1
…
bash · in-browser
learner@quantcraft:~/lab-59-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-59-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