19 — Value at Risk and Expected Shortfall¶
Free learning pack¶
resources/portfolio_foundations.mdreference/concepts/value_at_risk.mdreference/concepts/portfolio_volatility.mdreference/concepts/downside_risk.md
Do not search for more material until these are insufficient.
PREDICT¶
A portfolio has a 95% 1-day VaR of $50,000. Roughly how many trading days a year (out of ~252) would you expect a loss worse than $50,000, if returns really were normally distributed? Is Expected Shortfall at the same confidence bigger or smaller than $50,000?
Formula (parametric / Gaussian)¶
VaR = PortfolioValue * sigma * z, where z = norm.ppf(confidence)
ES = PortfolioValue * sigma * phi(z) / (1 - confidence), where phi is
the standard normal density.
from scipy.stats import norm
portfolio_value = 1_000_000
daily_vol = 0.02
confidence = 0.95
# MANUAL FIRST:
# compute z = norm.ppf(confidence), then VaR and ES using the formulas
# above. Don't call src yet.
var_95 = None
es_95 = None
print(var_95, es_95)
# CHECK (uncomment after your attempt):
# import numpy as np
# assert np.isclose(var_95, 32897.07, atol=1)
# assert np.isclose(es_95, 41254.26, atol=1)
# assert es_95 > var_95, "ES should always be worse than VaR at the same confidence"
Plot¶
Visualize where the 5% VaR cutoff and the ES (the average loss beyond that cutoff) actually sit on the return distribution.
import matplotlib.pyplot as plt
import numpy as np
z = norm.ppf(confidence)
es_z = norm.pdf(z) / (1 - confidence) # ES in standardized (z) units
x = np.linspace(-4, 4, 500)
y = norm.pdf(x)
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(x, y, color="#171A21", linewidth=1.5)
tail_x = np.linspace(-4, -z, 200)
ax.fill_between(tail_x, norm.pdf(tail_x), color="#A6472F", alpha=0.5,
label=f"worst {int((1 - confidence) * 100)}% of outcomes")
ax.axvline(-z, color="#9A5F26", linestyle="--", linewidth=1.2, label=f"VaR cutoff (z={-z:.2f})")
ax.axvline(-es_z, color="#A6472F", linestyle=":", linewidth=1.2, label=f"ES: mean of the tail (z={-es_z:.2f})")
ax.set_xlabel("standardized return (z)")
ax.set_ylabel("density")
ax.set_title(f"{int(confidence * 100)}% VaR vs Expected Shortfall")
ax.legend()
plt.show()
Experiment¶
Recompute at 99% confidence. ES grows faster than VaR as confidence rises — explain why, using the shape of the normal density's tail.
PREDICT (ex-ante vs. ex-post)¶
The parametric VaR above assumed daily_vol = 2%. Suppose you then
collect 60 real daily returns and their realized volatility comes out
to 2.5%, not 2%. Without recalculating anything: was the VaR figure
above too conservative (overstated the risk) or not conservative enough
(understated it)?
Formula (ex-post / realized risk)¶
realized_volatility(returns, periods_per_year) — the annualized sample
standard deviation of an actual return series, the ex-post counterpart
to the daily_vol assumption fed into parametric_var above.
downside_deviation(returns, target, periods_per_year) — like
volatility, but counting only shortfalls below target.
from pm.returns import downside_deviation, realized_volatility
rng = np.random.default_rng(7)
sample_returns = rng.normal(0.0, 0.028, 60) # true daily vol is 2.8%, unknown to the model
# MANUAL FIRST:
# compute the realized volatility of sample_returns (periods_per_year=1,
# since these are already daily figures and we're not annualizing here),
# and compare it with the daily_vol=0.02 the VaR model above assumed.
realized_vol = None
print(realized_vol, "vs the model's assumed", daily_vol)
# CHECK (uncomment after your attempt):
# assert np.isclose(realized_vol, realized_volatility(sample_returns, periods_per_year=1))
# assert realized_vol > daily_vol, (
# "this sample's realized volatility should come in above the model's "
# "2% assumption - the whole point of the exercise"
# )
Reference¶
reference/concepts/value_at_risk.md
reference/concepts/downside_risk.md
reference/concepts/stress_testing.md (for what parametric VaR/ES miss)
Promote¶
Use src/pm/risk.py (parametric_var, expected_shortfall) and
src/pm/returns.py (realized_volatility, downside_deviation) only
after your own implementation.
Test¶
pytest tests/test_var.py tests/test_returns.py
ORAL CHECK¶
Explain to a PM why VaR alone can understate risk, why ES is a
meaningfully different (and generally more conservative) number at the
same confidence level, and why comparing daily_vol (the risk model's
assumption) against realized_volatility (what actually happened) is a
basic sanity check every risk model needs — not a one-time exercise.
Try /tutor value at risk or /tutor downside deviation for an
adaptive walkthrough.