Risk Metrics in Python
Risk management is what separates professionals from gamblers. This guide covers the essential risk metrics every quant should know โ with Python code for each method.
Value at Risk (VaR)
VaR answers one question: What is the maximum I could lose in a given time period with X% confidence? For example, a 1-day 95% VaR of 2.5% means: on 95% of days, you will NOT lose more than 2.5%.
Setup: Get the Data
import yfinance as yf import numpy as np import pandas as pd import matplotlib.pyplot as plt from scipy import stats # Download stock data ticker = "AAPL" df = yf.download(ticker, start="2020-01-01", end="2024-01-01") returns = df["Close"].pct_change().dropna() portfolio_value = 100000 # $100,000 portfolio confidence = 0.95Method 1: Historical VaR
The simplest approach. Sort all historical returns and find the 5th percentile. No assumptions about distribution needed.
# Historical VaR: just sort returns and find the percentile historical_var = np.percentile(returns, (1 - confidence) * 100) print(f"Historical VaR (95%, 1-day): {{historical_var:.4f}} ({{historical_var:.2%}})") print(f"On a ${{portfolio_value:,.0f}} portfolio: ${{abs(historical_var * portfolio_value):,.2f}} max daily loss") # Visualise plt.figure(figsize=(10, 5)) plt.hist(returns, bins=100, color="teal", alpha=0.7, edgecolor="black") plt.axvline(x=historical_var, color="red", linewidth=2, label=f"95% VaR: {{historical_var:.2%}}") plt.title(f"{{ticker}} Daily Returns Distribution with VaR") plt.xlabel("Daily Return") plt.ylabel("Frequency") plt.legend() plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()Method 2: Parametric VaR
Assumes returns follow a normal distribution. Faster to compute but less accurate (real returns have fat tails).
# Parametric (Gaussian) VaR mean_return = returns.mean() std_return = returns.std() # Z-score for 95% confidence = 1.645 z_score = stats.norm.ppf(1 - confidence) parametric_var = mean_return + z_score * std_return print(f"Parametric VaR (95%, 1-day): {{parametric_var:.4f}} ({{parametric_var:.2%}})") print(f"On a ${{portfolio_value:,.0f}} portfolio: ${{abs(parametric_var * portfolio_value):,.2f}}")Method 3: Monte Carlo VaR
Simulate thousands of possible future returns using random sampling. The most flexible method โ you can model any distribution.
# Monte Carlo VaR: simulate 10,000 possible next-day returns np.random.seed(42) num_simulations = 10000 # Simulate returns from the historical distribution simulated_returns = np.random.normal(mean_return, std_return, num_simulations) # VaR from simulated distribution mc_var = np.percentile(simulated_returns, (1 - confidence) * 100) print(f"Monte Carlo VaR (95%, 1-day): {{mc_var:.4f}} ({{mc_var:.2%}})") print(f"On a ${{portfolio_value:,.0f}} portfolio: ${{abs(mc_var * portfolio_value):,.2f}}")Conditional VaR (Expected Shortfall)
CVaR Goes Beyond VaR
VaR tells you the threshold. CVaR (Expected Shortfall) tells you the average loss WHEN you exceed that threshold. If VaR says you will not lose more than 2.5% on 95% of days, CVaR tells you what the average loss is on the other 5% of days โ the really bad ones.
# CVaR: average of returns beyond the VaR threshold cvar = returns[returns <= historical_var].mean() print(f"CVaR (Expected Shortfall, 95%): {{cvar:.4f}} ({{cvar:.2%}})") print(f"When things go wrong, average loss: ${{abs(cvar * portfolio_value):,.2f}}")Maximum Drawdown
# Maximum Drawdown: largest peak-to-trough decline cumulative = (1 + returns).cumprod() rolling_max = cumulative.cummax() drawdown = (cumulative - rolling_max) / rolling_max max_dd = drawdown.min() max_dd_date = drawdown.idxmin() print(f"Maximum Drawdown: {{max_dd:.2%}}") print(f"Occurred on: {{max_dd_date.date()}}") # Plot drawdown plt.figure(figsize=(12, 4)) plt.fill_between(drawdown.index, drawdown, 0, color="red", alpha=0.3) plt.plot(drawdown.index, drawdown, color="red", linewidth=0.5) plt.title(f"{{ticker}} Drawdown Over Time") plt.xlabel("Date") plt.ylabel("Drawdown") plt.grid(True, alpha=0.3) plt.tight_layout() plt.show()Summary of All Risk Metrics
# All risk metrics in one place print(f"=== Risk Report for {{ticker}} ===") print(f"Portfolio value: ${{portfolio_value:,.0f}}") print(f"Historical VaR 95%: {{historical_var:.2%}} (${{abs(historical_var * portfolio_value):,.0f}})") print(f"Parametric VaR 95%: {{parametric_var:.2%}} (${{abs(parametric_var * portfolio_value):,.0f}})") print(f"Monte Carlo VaR 95%:{{mc_var:.2%}} (${{abs(mc_var * portfolio_value):,.0f}})") print(f"CVaR (Exp. Short.): {{cvar:.2%}} (${{abs(cvar * portfolio_value):,.0f}})") print(f"Max Drawdown: {{max_dd:.2%}}") print(f"Annual Volatility: {{std_return * np.sqrt(252):.2%}}")