Skip to main content
Back to Blog
Trading

Portfolio Risk Math Explained: VaR, CVaR, and Why Covariance Estimation Matters

March 22, 202615 min read
PythonRisk ManagementTradingMathematicsPortfolioscipy

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

  1. Use CVaR, not just VaR — regulators and sophisticated investors care about tail risk
  2. Use Ledoit-Wolf, not sample covariance — especially with >10 assets
  3. Monte Carlo > Parametric — financial returns have fat tails
  4. 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.

Want to see this in action?

Check out the projects and case studies behind these articles.