Value at Risk (VaR) and Conditional VaR (CVaR)¶
Tail risk metrics for understanding potential losses.
Overview¶
VaR and CVaR quantify downside risk by measuring potential losses at specified confidence levels. These are essential risk management tools used throughout finance.
Value at Risk (VaR)¶
Definition¶
VaR answers: "What is the maximum loss I can expect with X% confidence over a given time period?"
Interpretation: At 95% confidence, you expect to lose no more than VaR 95% of the time.
Calculation Methods¶
Historical VaR¶
Based on actual historical returns:
from rustybt.analytics import RiskAnalytics
import numpy as np
risk = RiskAnalytics(backtest_result)
# 95% VaR using historical method
var_95 = risk.calculate_var(confidence_level=0.95, method='historical')
print(f"VaR (95%): {var_95:.2%}")
How it works: 1. Sort historical returns 2. Find 5th percentile 3. VaR = negative of that percentile
def historical_var(returns, confidence_level=0.95):
"""Calculate historical VaR."""
# Sort returns (worst to best)
sorted_returns = np.sort(returns)
# Find percentile
index = int((1 - confidence_level) * len(returns))
var = -sorted_returns[index]
return var
# Example
returns = backtest_result['returns']
var_95 = historical_var(returns, 0.95)
Parametric VaR¶
Assumes returns are normally distributed:
How it works:
def parametric_var(returns, confidence_level=0.95):
"""Calculate parametric VaR assuming normality."""
from scipy import stats
mean = returns.mean()
std = returns.std()
# Z-score for confidence level
z = stats.norm.ppf(1 - confidence_level)
# VaR = -(mean + z * std)
var = -(mean + z * std)
return var
Assumption: Returns are normally distributed (often violated in finance).
VaR Example¶
from rustybt.analytics import RiskAnalytics
# Calculate VaR at multiple confidence levels
risk = RiskAnalytics(backtest_result)
for confidence in [0.90, 0.95, 0.99]:
var = risk.calculate_var(confidence_level=confidence)
print(f"VaR ({confidence:.0%}): {var:.2%}")
# Example output:
# VaR (90%): -2.3% (expect to lose more than 2.3% only 10% of the time)
# VaR (95%): -3.1% (expect to lose more than 3.1% only 5% of the time)
# VaR (99%): -4.5% (expect to lose more than 4.5% only 1% of the time)
Conditional VaR (CVaR)¶
Definition¶
CVaR answers: "If VaR is exceeded, how bad will the loss be on average?"
Also called Expected Shortfall (ES) or Average VaR.
Interpretation: CVaR is the average loss in the worst X% of cases.
Calculation¶
# Calculate CVaR (Expected Shortfall)
cvar_95 = risk.calculate_cvar(confidence_level=0.95)
print(f"CVaR (95%): {cvar_95:.2%}")
Implementation:
def calculate_cvar(returns, confidence_level=0.95):
"""Calculate Conditional VaR (Expected Shortfall)."""
# Sort returns
sorted_returns = np.sort(returns)
# Find VaR cutoff
cutoff_index = int((1 - confidence_level) * len(returns))
# CVaR = average of returns worse than VaR
cvar = -np.mean(sorted_returns[:cutoff_index])
return cvar
CVaR Example¶
# Compare VaR and CVaR
var_95 = risk.calculate_var(confidence_level=0.95)
cvar_95 = risk.calculate_cvar(confidence_level=0.95)
print(f"VaR (95%): {var_95:.2%}")
print(f"CVaR (95%): {cvar_95:.2%}")
# Example output:
# VaR (95%): -3.1% (5% chance of losing more than this)
# CVaR (95%): -4.8% (average loss when VaR is exceeded)
Key insight: CVaR is always >= VaR, telling you about tail risk.
Comparison¶
VaR vs CVaR¶
| Aspect | VaR | CVaR |
|---|---|---|
| Question | "How bad could it get?" | "How bad is it when it's bad?" |
| Measures | Threshold loss | Average tail loss |
| Properties | Not coherent risk measure | Coherent risk measure |
| Sensitivity | Ignores tail shape | Captures tail risk |
| Usefulness | Regulatory reporting | Risk management |
Coherent Risk Measures¶
CVaR satisfies all axioms of coherent risk measures: 1. Monotonicity: If X ≤ Y, then Risk(X) ≤ Risk(Y) 2. Sub-additivity: Risk(X + Y) ≤ Risk(X) + Risk(Y) 3. Positive homogeneity: Risk(λX) = λRisk(X) 4. Translation invariance: Risk(X + c) = Risk(X) + c
VaR fails sub-additivity, making CVaR theoretically superior.
Practical Usage¶
Risk Budgeting¶
# Set risk budget based on CVaR
max_acceptable_cvar = 0.05 # 5%
cvar = risk.calculate_cvar(confidence_level=0.95)
if cvar > max_acceptable_cvar:
print(f"⚠ Strategy exceeds risk budget: {cvar:.2%} > {max_acceptable_cvar:.2%}")
# Reduce position sizes or leverage
else:
print(f"✓ Strategy within risk budget")
Position Sizing¶
def calculate_position_size(capital, max_cvar, strategy_cvar):
"""Size position based on CVaR limit."""
# If strategy CVaR is 10% and we want 5% portfolio CVaR:
# Position size = 5% / 10% = 50% of capital
position_size = capital * (max_cvar / strategy_cvar)
return position_size
capital = 100000
max_portfolio_cvar = 0.05
strategy_cvar = 0.10
size = calculate_position_size(capital, max_portfolio_cvar, strategy_cvar)
print(f"Recommended position size: ${size:,.0f}")
Comparative Analysis¶
# Compare strategies by tail risk
strategies = ['momentum', 'mean_reversion', 'breakout']
for strategy_name in strategies:
result = run_backtest(strategy_name)
risk = RiskAnalytics(result)
var = risk.calculate_var(confidence_level=0.95)
cvar = risk.calculate_cvar(confidence_level=0.95)
print(f"{strategy_name}:")
print(f" VaR (95%): {var:.2%}")
print(f" CVaR (95%): {cvar:.2%}")
print(f" Tail risk ratio: {cvar/var:.2f}")
Limitations¶
VaR Limitations¶
- Doesn't capture tail shape: Only tells you the threshold, not how bad tails are
- Not sub-additive: Diversification may not reduce portfolio VaR
- Backward-looking: Based on historical data
- Model risk: Parametric VaR assumes normality
CVaR Limitations¶
- Backward-looking: Assumes future similar to past
- Sample size dependent: Needs sufficient data in tail
- Estimation error: Uncertain with limited data
- Regime changes: Past tails may not predict future
Best Practices¶
1. Use Multiple Confidence Levels¶
# Don't rely on single confidence level
for conf in [0.90, 0.95, 0.99]:
var = risk.calculate_var(confidence_level=conf)
cvar = risk.calculate_cvar(confidence_level=conf)
print(f"{conf:.0%}: VaR={var:.2%}, CVaR={cvar:.2%}")
2. Prefer CVaR for Risk Management¶
3. Stress Testing¶
# Test VaR/CVaR under stress scenarios
def stress_test(strategy, stress_scenarios):
"""Test strategy under extreme scenarios."""
for scenario_name, scenario_data in stress_scenarios.items():
result = run_backtest(strategy, data=scenario_data)
risk = RiskAnalytics(result)
cvar = risk.calculate_cvar(confidence_level=0.95)
print(f"{scenario_name}: CVaR={cvar:.2%}")
4. Combine with Other Metrics¶
# Don't use VaR/CVaR in isolation
metrics = {
'Sharpe': calculate_sharpe(result),
'Max Drawdown': calculate_max_drawdown(result),
'VaR (95%)': risk.calculate_var(0.95),
'CVaR (95%)': risk.calculate_cvar(0.95)
}
# Comprehensive risk assessment
for metric, value in metrics.items():
print(f"{metric}: {value:.3f}")