Portfolio Risk Math Explained: VaR, CVaR, and Why Covariance Estimation Matters
When I built RiskRadar, I needed to implement institutional-grade risk calculations. Most risk management tutorials either oversimplify ("just calculate standard deviation") or assume PhD-level math.
Here's the middle ground — the math you actually need to implement portfolio risk, explained for engineers.
Value at Risk (VaR): What's the Worst That Could Happen?
VaR answers: "What's the maximum I could lose in a day, with 95% confidence?"
If your portfolio's 1-day 95% VaR is $10,000, that means: on 95% of days, your losses won't exceed $10,000. On the other 5% of days... they might.
Three ways to calculate VaR:
Historical VaR (simplest)
Sort your historical daily returns. The 5th percentile is your 95% VaR.
import numpy as np
def historical_var(returns, confidence=0.95):
return -np.percentile(returns, (1 - confidence) * 100)
Parametric VaR (assumes normal distribution)
from scipy.stats import norm
def parametric_var(returns, confidence=0.95):
mu = returns.mean()
sigma = returns.std()
z_score = norm.ppf(1 - confidence)
return -(mu + z_score * sigma)
Problem: returns aren't normally distributed. They have fat tails (extreme events happen more than a bell curve predicts).
Monte Carlo VaR (most realistic)
Simulate 10,000 possible futures and measure the worst outcomes:
def monte_carlo_var(returns, n_simulations=10000, confidence=0.95):
mu = returns.mean()
sigma = returns.std()
# Simulate future returns
simulated = np.random.normal(mu, sigma, n_simulations)
return -np.percentile(simulated, (1 - confidence) * 100)
CVaR: What Happens in the Tail?
VaR tells you the threshold. CVaR (Conditional VaR, aka Expected Shortfall) tells you: "Given that we've exceeded VaR, how bad does it get on average?"
def cvar(returns, confidence=0.95):
var = historical_var(returns, confidence)
return -returns[returns <= -var].mean()
CVaR is always worse than VaR (by definition). If your 95% VaR is -$10K, your CVaR might be -$15K — meaning on those bad 5% of days, you lose $15K on average.
Why CVaR matters more than VaR: VaR tells you "95% of the time, you'll be fine." CVaR tells you "when things go wrong, here's how wrong." Regulators increasingly prefer CVaR because it captures tail risk.
The Covariance Problem
For a portfolio, you need to understand how assets move together. Two assets that are both volatile but negatively correlated create a safer portfolio than two calm assets that move in lockstep.
The covariance matrix captures all pairwise relationships. For N assets, it's an N×N matrix.
The problem: With 50 assets and 252 trading days per year, you have 1,275 covariance estimates (50×51/2) from only 252 data points. The sample covariance matrix is noisy and often singular (mathematically broken).
Ledoit-Wolf Shrinkage: The Fix
The Ledoit-Wolf estimator "shrinks" the sample covariance toward a structured target (like the identity matrix). This reduces noise while preserving the real signal:
from sklearn.covariance import LedoitWolf
def robust_covariance(returns):
lw = LedoitWolf()
lw.fit(returns)
# lw.shrinkage_ tells you how much it corrected
# 0.0 = trusted sample fully, 1.0 = ignored sample completely
return lw.covariance_, lw.shrinkage_
In RiskRadar, Ledoit-Wolf shrinkage is 0.15-0.30 for typical portfolios — meaning the sample covariance is decent but needs correction. For portfolios with more assets than data points, shrinkage can be 0.80+, meaning the sample covariance was nearly useless.
Portfolio Optimization: Putting It Together
With reliable covariance estimates, you can optimize portfolio weights:
from scipy.optimize import minimize
def maximize_sharpe(expected_returns, cov_matrix, risk_free_rate=0.05):
n_assets = len(expected_returns)
def neg_sharpe(weights):
port_return = weights @ expected_returns
port_vol = np.sqrt(weights @ cov_matrix @ weights)
return -(port_return - risk_free_rate) / port_vol
# Constraints: weights sum to 1, all positive (long-only)
constraints = {'type': 'eq', 'fun': lambda w: np.sum(w) - 1}
bounds = [(0, 1) for _ in range(n_assets)]
result = minimize(neg_sharpe,
x0=np.ones(n_assets) / n_assets,
method='SLSQP',
bounds=bounds,
constraints=constraints)
return result.x # Optimal weights
The Practical Takeaway
- Use CVaR, not just VaR — regulators and sophisticated investors care about tail risk
- Use Ledoit-Wolf, not sample covariance — especially with >10 assets
- Monte Carlo > Parametric — financial returns have fat tails
- Backtest your risk model — did your predicted 95% VaR actually contain 95% of days?
Risk math isn't about predicting the future. It's about sizing your bets so that when you're wrong, you survive to trade another day.