A formula instead of a history
Historical VaR reads the loss from past returns. Parametric VaR assumes returns follow a normal distribution and needs only two numbers, the mean and the standard deviation. The loss exceeded with probability 1 - confidence is then z * sigma - mu, where z is the standard normal quantile (1.645 at 95%, 2.326 at 99%). It is fast, smooth, and extends easily to a whole portfolio, which is why it was the industry standard for decades.
from statistics import NormalDist
def normal_var(mu, sigma, confidence):
z = NormalDist().inv_cdf(confidence)
return z * sigma - mu
print(round(NormalDist().inv_cdf(0.95), 4), round(NormalDist().inv_cdf(0.99), 4))
print(round(normal_var(0.0005, 0.012, 0.95), 5)) # a 95% one-day loss limit
print(round(normal_var(0.0005, 0.012, 0.99), 5)) # a higher confidence, a larger limit# The multiplier z for common confidence levels
from statistics import NormalDist
for confidence in (0.90, 0.95, 0.975, 0.99, 0.999):
print(confidence, round(NormalDist().inv_cdf(confidence), 3)) # 1.282, 1.645, 1.96, 2.326, 3.09From one asset to a portfolio, and to ten days
For a portfolio, the mean is weights @ mu and the standard deviation is sqrt(weights @ cov @ weights), the quantities from the portfolio-variance topic. Diversification shows up directly: low correlations shrink the standard deviation and the VaR. For a longer horizon, the mean grows with the number of days and the standard deviation with its square root, so a 10-day VaR is z * sigma * sqrt(10) - mu * 10.
import numpy as np
from statistics import NormalDist
def normal_var(mu, sigma, confidence):
return NormalDist().inv_cdf(confidence) * sigma - mu
weights = np.array([0.6, 0.4])
mu = np.array([0.0005, 0.0003])
cov = np.array([[0.00016, 0.00004], [0.00004, 0.00009]]) # volatilities 1.26% and 0.95%, correlation 0.33
mean, std = float(weights @ mu), float(np.sqrt(weights @ cov @ weights))
print(round(mean, 5), round(std, 5))
print(round(normal_var(mean, std, 0.99), 5))
print(round(normal_var(mean * 10, std * np.sqrt(10), 0.99), 5)) # the 10-day figureWhere the normal assumption fails
Real returns have fat tails: extreme days are much more frequent than a normal distribution allows. The parametric VaR then looks too small, most of all at high confidence. Comparing it with the historical VaR on the same data shows the gap: positive means the normal model understates the risk. At 95% the two are often close, at 99% they can differ a lot.
# Fat-tailed returns: the normal model understates the 99% loss
import numpy as np
from statistics import NormalDist
rng = np.random.default_rng(2)
returns = rng.standard_t(3, size=5000) * 0.008 # a fat-tailed distribution (3 degrees of freedom)
for confidence in (0.95, 0.99):
historical = -np.quantile(returns, 1 - confidence)
parametric = NormalDist().inv_cdf(confidence) * returns.std(ddof=1) - returns.mean()
print(confidence, round(float(historical), 5), round(float(parametric), 5), round(float(historical - parametric), 5))- Parametric VaR =
z * sigma - muwithz = NormalDist().inv_cdf(confidence): only the mean and the standard deviation are needed. - A portfolio uses
weights @ muandsqrt(weights @ cov @ weights); a horizon scales the mean bydaysand the volatility bysqrt(days). - Real returns have fat tails, so compare with the historical VaR: a positive gap means the normal model understates the risk.
Next up: expected shortfall, the average loss on the days when the VaR is exceeded.
Lab · what's graded
- Turn two numbers into a limit with
normal_var25 pts - Cover a whole portfolio with
portfolio_normal_var25 pts - Look further ahead with
horizon_var25 pts - Measure the fat tails with
normality_gap25 pts