σμ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 7 · Alpha research & risk›Topic 7.4·~50 minAdvanced

Historical VaR

Skill positionModule 7 of 11 · Topic 4 of 9
7.4 Historical VaR
—Lab score— / 100Total XP—Streak—

A loss limit with a confidence level

Value at Risk answers: how much could I lose on a bad day, but not the worst? A 1-day 95% VaR of 2% says that on 95 days out of 100 the loss should not exceed 2%. The historical method needs no model. Take the past returns, sort them, and read off the loss at the 5th percentile: the VaR is minus that quantile. Regulators and risk limits use it everywhere because it is one number a manager can compare with a limit.

import numpy as np

rng = np.random.default_rng(3)
returns = rng.normal(0.0004, 0.012, size=500)           # two years of daily returns

var_95 = -np.quantile(returns, 0.05)                    # minus the 5th percentile
var_99 = -np.quantile(returns, 0.01)
print(round(float(var_95), 5), round(float(var_99), 5))  # 0.01928 0.027  the 99% loss limit is larger

Backtesting a VaR: exceptions

A VaR is a prediction, so it can be checked. An exception is a day on which the loss exceeded the VaR, return < -VaR. With a 95% VaR you expect about 5% of days to be exceptions: far more means the model understates risk, far fewer that it is too cautious. Regulators count exceptions over 250 days for exactly this reason. Note the strict inequality: a loss equal to the VaR is not an exception.

import numpy as np

rng = np.random.default_rng(3)
returns = rng.normal(0.0004, 0.012, size=500)
var_95 = -np.quantile(returns, 0.05)

exceptions = int(np.sum(returns < -var_95))
print(exceptions, "exceptions in", len(returns), "days; expected about", int(0.05 * len(returns)))     # 25 25

Longer horizons, and as it happened

A 10-day VaR is often required. With independent daily returns, volatility grows with the square root of time, so the square-root-of-time rule scales: VaR_T = VaR_1 * sqrt(T). It is an approximation, weaker for fat tails and autocorrelated returns. Honest backtests also respect time: the VaR used on day t may only use the returns before day t, the same discipline as avoiding look-ahead. A rolling VaR does that with a window of the last n days.

# The square-root-of-time rule for several horizons
import math

one_day = 0.0186
for days in (1, 5, 10, 20):
    print(days, round(one_day * math.sqrt(days), 4))        # 1: 0.0186, 5: 0.0416, 10: 0.0588, 20: 0.0832
import numpy as np

def historical_var(returns, confidence):
    return float(-np.quantile(returns, 1 - confidence))

def rolling_var(returns, window, confidence):
    out = np.full(len(returns), np.nan)
    for t in range(window, len(returns)):
        out[t] = historical_var(returns[t - window:t], confidence)      # the days BEFORE day t
    return out

rng = np.random.default_rng(3)
returns = rng.normal(0.0004, 0.012, size=500)
rolling = rolling_var(returns, 250, 0.99)
print(np.isnan(rolling[:250]).all(), round(float(rolling[250]), 5), round(float(rolling[-1]), 5))
exceptions = int(np.sum(returns[250:] < -rolling[250:]))
print(exceptions, "exceptions in", len(returns) - 250, "days (about 2 expected at 99%)")
print(round(historical_var(returns, 0.99) * np.sqrt(10), 5))                 # a 10-day figure by the square-root rule
What VaR does not tell youVaR is a threshold, not a worst case: it says nothing about how bad the losses beyond it are, which is why the next topics look at expected shortfall and stress tests. The historical method is only as good as its window: a calm two years gives a small VaR just before a crisis, and the 99% figure rests on only a handful of observations.
  • Historical VaR is minus the (1 - confidence) quantile of past returns (np.quantile): a positive loss fraction needing no model.
  • Backtest by counting exceptions (return < -VaR): about 1 - confidence of the days; more means the VaR is too low.
  • Scale to a horizon with var * sqrt(days); a rolling window uses only earlier days; VaR gives a threshold, not the size of losses beyond it.

Next up: parametric VaR, computing the same threshold from a normal distribution instead of the raw history.

Lab · what's graded

  • Read the loss from history with historical_var25 pts
  • Count the breaches with var_exceptions25 pts
  • Stretch the horizon with scale_var25 pts
  • Estimate it as it happened with rolling_var25 pts
Explorer · lab-96-1
▾ lab-96-1
…
bash · in-browser
learner@quantcraft:~/lab-96-1$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

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