How Much Can You Lose on a Bad Day?
Every major bank, hedge fund, and now DeFi protocol needs to answer one question: how much could we lose in a really bad day?
Value at Risk (VaR) is the industry's standard answer. It's a single number that regulators require, risk managers report, and traders live by. Today we derive three calculation methods, expose VaR's biggest limitation, and build a complete risk reporting system in Python.
1. What Is VaR?
Value at Risk is defined as:
The maximum loss not exceeded with probability over a given time horizon.
At 95% confidence over one day (5% VaR), you're saying: on 95 out of 100 trading days, losses will not exceed this number.
Formal definition
For a portfolio with return distribution , the -VaR is:
Where is the -quantile of the return distribution. For (5% VaR), we're looking at the 5th percentile of returns.
Scaled to portfolio value :
2. Three Methods to Calculate VaR
Method 1: Historical Simulation (Non-Parametric)
The simplest approach. Take your portfolio's actual historical P&L series β say 500 days. Sort losses from worst to best. The 5% VaR is the loss at the 25th worst observation (500 Γ 0.05 = 25).
Where is the empirical -quantile of historical returns.
Pros:
- Fully non-parametric β no distributional assumptions
- Captures fat tails and actual market behavior
- Easy to implement
Cons:
- Completely dependent on the historical window
- If your window doesn't include a crisis, VaR underestimates crisis risk severely
- Cannot extrapolate beyond observed history
Method 2: Parametric (Normal Distribution)
Assume returns are normally distributed: . Then:
Where is the standard normal quantile:
- (95% VaR)
- (99% VaR)
For a portfolio with value :
Pros:
- Fast, analytically tractable
- Easy to decompose by asset (component VaR)
- Minimal data requirements
Cons:
- Financial returns have fat tails β the normal distribution massively underestimates tail losses
- A 5-sigma event that should happen once every 3.5 million days happens every few years in markets
Method 3: Parametric (Student-t Distribution)
Replace the normal with a t-distribution with estimated degrees-of-freedom :
Where is the t-distribution quantile with degrees of freedom.
As , you recover the normal. For , you get realistic fat tails matching equity return data.
Estimating :
from scipy import stats
def estimate_t_dof(returns):
"""Estimate degrees of freedom for t-distribution fit."""
params = stats.t.fit(returns)
return params[0] # df parameter
# Example
returns = pd.read_csv('portfolio_returns.csv')['returns']
nu = estimate_t_dof(returns)
print(f"Estimated degrees of freedom: {nu:.2f}")
# Typical output: 4.5 to 6.5 for equity returnsThis is strictly better than the normal method for financial returns with excess kurtosis above 3.
3. Interactive: VaR Methods Comparison
Upload a return series or use simulated data. Compare Historical, Normal, and Student-t VaR estimates across confidence levels.
Compare Historical, Normal, and Student-t VaR. Notice Normal VaR underestimates tail risk.
Typical pattern:
- Normal VaR: lowest estimate (dangerously optimistic)
- Student-t VaR: 20-40% higher than Normal (more realistic)
- Historical VaR: depends on window β may be higher or lower
4. Python Implementation
import numpy as np
import pandas as pd
from scipy import stats
from scipy.optimize import brentq
def var_historical(returns, alpha=0.05):
"""
Historical VaR: empirical quantile.
Args:
returns: Array of historical returns
alpha: Confidence level (0.05 for 95% VaR)
Returns:
VaR as a positive number (loss amount)
"""
return -np.percentile(returns, alpha * 100)
def var_normal(returns, alpha=0.05):
"""
Parametric VaR assuming normal distribution.
"""
mu = np.mean(returns)
sigma = np.std(returns)
z_alpha = stats.norm.ppf(alpha)
return -(mu + z_alpha * sigma)
def var_student_t(returns, alpha=0.05):
"""
Parametric VaR assuming Student-t distribution.
"""
mu = np.mean(returns)
sigma = np.std(returns)
# Fit t-distribution to get degrees of freedom
df, loc, scale = stats.t.fit(returns)
# VaR from fitted t-distribution
t_quantile = stats.t.ppf(alpha, df, loc, scale)
return -t_quantile
def expected_shortfall(returns, alpha=0.05):
"""
Expected Shortfall (CVaR): expected loss given VaR is breached.
Also called: Conditional VaR, Average VaR, Tail VaR
"""
var = var_historical(returns, alpha)
tail_losses = returns[returns <= -var]
return -tail_losses.mean() if len(tail_losses) > 0 else var
def complete_var_report(returns, portfolio_value=1_000_000, alpha=0.05):
"""
Complete VaR reporting suite.
"""
report = {
'Portfolio Value': f'${portfolio_value:,.0f}',
'Confidence Level': f'{(1-alpha)*100:.0f}%',
'Observations': len(returns),
# VaR estimates (daily)
'Historical VaR (daily)': f'${var_historical(returns, alpha) * portfolio_value:,.0f}',
'Normal VaR (daily)': f'${var_normal(returns, alpha) * portfolio_value:,.0f}',
'Student-t VaR (daily)': f'${var_student_t(returns, alpha) * portfolio_value:,.0f}',
# Expected Shortfall
'Expected Shortfall (CVaR)': f'${expected_shortfall(returns, alpha) * portfolio_value:,.0f}',
# Distribution diagnostics
'Mean Return (daily)': f'{np.mean(returns):.4%}',
'Volatility (daily)': f'{np.std(returns):.4%}',
'Skewness': f'{stats.skew(returns):.3f}',
'Excess Kurtosis': f'{stats.kurtosis(returns):.3f}',
# t-distribution fit
't-distribution df': f'{stats.t.fit(returns)[0]:.2f}',
}
# 10-day VaR (square-root-of-time scaling)
report['Historical VaR (10-day)'] = f'${var_historical(returns, alpha) * np.sqrt(10) * portfolio_value:,.0f}'
report['Normal VaR (10-day)'] = f'${var_normal(returns, alpha) * np.sqrt(10) * portfolio_value:,.0f}'
return pd.Series(report)
# βββ Example: Compare VaR methods on real-like data βββββββββββββββββββββββ
np.random.seed(42)
n_days = 500
# Simulate fat-tailed returns (t-distribution with df=5)
returns_t = stats.t.rvs(df=5, loc=0.0003, scale=0.015, size=n_days)
# Also generate normal returns for comparison
returns_norm = np.random.normal(0.0003, 0.015, n_days)
print("=" * 60)
print("VaR Report: Fat-Tailed Returns (t-distribution, df=5)")
print("=" * 60)
report = complete_var_report(returns_t, portfolio_value=1_000_000, alpha=0.05)
print(report)
print("\n" + "=" * 60)
print("Comparison: Normal Returns")
print("=" * 60)
report_norm = complete_var_report(returns_norm, portfolio_value=1_000_000, alpha=0.05)
print(report_norm)Typical output:
============================================================
VaR Report: Fat-Tailed Returns (t-distribution, df=5)
============================================================
Portfolio Value: $1,000,000
Confidence Level: 95%
Observations: 500
Historical VaR (daily): $28,450
Normal VaR (daily): $24,120 β Underestimates by ~15%
Student-t VaR (daily): $29,800 β Closest to historical
Expected Shortfall: $41,200 β 45% higher than VaR!
...
Excess Kurtosis: 4.82 β Fat tails detected
t-distribution df: 5.1 β Correctly identified
============================================================
Comparison: Normal Returns
============================================================
...
Normal VaR (daily): $24,680 β Similar to historical
Student-t VaR (daily): $25,100 β Converges to normal
Excess Kurtosis: 0.12 β Near-normal
5. VaR Scaling: The Square-Root-of-Time Rule
To convert 1-day VaR to T-day VaR:
This holds only if returns are iid (independent, identically distributed).
Why it works (under iid)
If daily returns are independent with variance :
Since VaR is proportional to in the normal case:
When it fails
Volatility clustering: Bad days follow bad days. During crisis periods, returns are positively autocorrelated in variance. The square-root rule understates multi-day VaR.
Example: March 2020. Daily VaR might suggest 10-day VaR = 1-day Γ β10 β 3.16Γ. But with volatility clustering, actual 10-day losses were 4-5Γ the 1-day VaR.
Better scaling: GARCH adjustment
Fit a GARCH(1,1) model to capture volatility clustering:
Then scale VaR using forecasted multi-day volatility from the GARCH model rather than βT.
from arch import arch_model
def var_with_garch(returns, alpha=0.05, horizon=10):
"""
VaR with GARCH volatility scaling.
"""
# Fit GARCH(1,1)
model = arch_model(returns, vol='GARCH', p=1, q=1)
fit = model.fit(disp='off')
# Forecast T-day volatility
forecasts = fit.forecast(horizon=horizon)
sigma_T = np.sqrt(np.sum(forecasts.variance.values[-1, :horizon]))
# VaR using forecasted volatility
mu = np.mean(returns) * horizon
z_alpha = stats.norm.ppf(alpha)
var_T = -(mu + z_alpha * sigma_T)
return var_T6. The Biggest VaR Limitation: It Ignores the Tail
VaR tells you the threshold. It says nothing about how bad losses can get beyond that threshold.
Consider two strategies:
Strategy A:
- 95% of the time: lose exactly $10,000 (the VaR amount)
- 5% of the time: lose exactly $10,000
Strategy B:
- 95% of the time: lose exactly $10,000 (the VaR amount)
- 5% of the time: lose $500,000
Both have identical 95% VaR = $10,000. But Strategy B is catastrophically more dangerous.
This is why Expected Shortfall (CVaR) was developed.
7. Expected Shortfall (CVaR): The Tail-Aware Metric
Expected Shortfall answers: given that VaR is breached, how bad is the loss on average?
Also called:
- Conditional VaR
- Average VaR
- Tail VaR
Properties
Coherence: CVaR satisfies all four axioms of a coherent risk measure (monotonicity, subadditivity, positive homogeneity, translation invariance). VaR fails subadditivity β the VaR of a combined portfolio can exceed the sum of individual VaRs, violating diversification logic.
Basel III and Modern Risk Frameworks
Basel III now requires both VaR and Expected Shortfall for market risk capital calculations. Most sophisticated funds report CVaR alongside VaR as standard practice.
8. Component VaR: Decomposing Risk by Asset
For a portfolio, Component VaR tells you how much each position contributes to total VaR:
In the normal case:
Where is the covariance of asset with the portfolio.
def component_var_normal(weights, cov_matrix, portfolio_value, alpha=0.05):
"""
Component VaR decomposition (normal assumption).
"""
sigma_p = np.sqrt(weights @ cov_matrix @ weights)
z_alpha = stats.norm.ppf(alpha)
# Marginal contribution to risk
marginal_risk = (cov_matrix @ weights) / sigma_p
# Component VaR
component_var = weights * marginal_risk * z_alpha * portfolio_value
return {
'Total VaR': abs(np.sum(component_var)),
'Components': component_var,
'Percent Contribution': abs(component_var) / abs(np.sum(component_var)) * 100
}
# Example
assets = ['SPY', 'TLT', 'GLD', 'VNQ', 'EEM']
weights = np.array([0.35, 0.25, 0.15, 0.15, 0.10])
# Covariance matrix (annual, convert to daily)
cov_annual = ... # From Episode 1 or 2
cov_daily = cov_annual / 252
result = component_var_normal(weights, cov_daily, portfolio_value=1_000_000, alpha=0.05)
print(f"Total 95% VaR: ${result['Total VaR']:,.0f}")
print("\nComponent breakdown:")
for asset, cv, pct in zip(assets, result['Components'], result['Percent Contribution']):
print(f" {asset}: ${abs(cv):>8,.0f} ({pct:>5.1f}%)")Use case: Identify concentration risk. If one asset contributes >40% of total VaR, you have a hidden concentration despite diversified weights.
9. VaR Backtesting: Did Your Model Work?
After computing VaR, you must backtest it. Count the number of "exceptions" β days when actual loss exceeded VaR.
For 95% VaR over 252 days, expect ~13 exceptions (5% Γ 252). Significantly more β model underestimates risk. Significantly fewer β model is conservative (acceptable but capital-inefficient).
Kupiec Test (Proportion of Failures)
Where:
- = expected exception rate (e.g., 0.05)
- = observed exception rate
- = number of exceptions
- = number of observations
Under the null hypothesis (correct model), .
def var_backtest_kupiec(actual_returns, var_estimates, alpha=0.05):
"""
Kupiec test for VaR model validation.
"""
T = len(actual_returns)
exceptions = actual_returns < -var_estimates
N = np.sum(exceptions)
p = alpha # Expected exception rate
p_hat = N / T # Observed exception rate
if N == 0 or N == T:
return {'status': 'INVALID', 'reason': 'Zero or all exceptions'}
# Likelihood ratio
lr_uc = -2 * (
np.log((1-p)**(T-N) * p**N) -
np.log((1-p_hat)**(T-N) * p_hat**N)
)
# p-value from chi-squared distribution
p_value = 1 - stats.chi2.cdf(lr_uc, df=1)
# Decision
if p_value < 0.05:
status = 'REJECT' # Model is invalid
elif p_hat > p * 1.5:
status = 'AMBER' # Too many exceptions
else:
status = 'PASS'
return {
'Status': status,
'Expected Exceptions': p * T,
'Actual Exceptions': N,
'Exception Rate': f'{p_hat:.2%}',
'LR Statistic': f'{lr_uc:.3f}',
'p-value': f'{p_value:.4f}',
}10. DeFi-Specific VaR Considerations
Standard VaR models underestimate DeFi portfolio risk. Two additional terms must be considered:
1. Liquidation Cascade Risk
During market stress, DeFi protocols experience correlated liquidations. A 20% ETH drop can trigger cascading liquidations across Aave, Compound, MakerDAO simultaneously.
Adjustment: Stress-test with crisis-period correlations (as in Episode 1). Multiply normal-market VaR by 1.5-2.0Γ for DeFi portfolios with leveraged positions.
2. Smart Contract Exploit Risk
Rare but catastrophic. A standard VaR model that ignores this will chronically underestimate tail risk.
Approximation: Model as a Bernoulli process with small probability and large loss :
For a $1M position in a protocol with estimated 0.5% annual exploit risk:
Small in normal times, but the CVaR is $1M β the full loss given an exploit occurs.
Key Takeaways
-
Three VaR methods: Historical (non-parametric), Normal (fast but optimistic), Student-t (fat-tail aware, recommended).
-
Always report CVaR alongside VaR. VaR tells you the threshold; CVaR tells you the tail severity.
-
Square-root-of-time scaling is approximate. It fails during volatility clustering β use GARCH for multi-day VaR in stressed markets.
-
Component VaR reveals concentration risk. A portfolio can be diversified by weights but concentrated by risk contribution.
-
Backtest your VaR model. Use the Kupiec test to validate exception rates.
-
DeFi requires VaR adjustments. Liquidation cascade risk and smart contract exploit risk are not captured by standard models.
What's Next
Episode 5: Factor Models β how quants decompose every return into systematic exposures. We derive the Fama-French 5-factor model, show you how to run factor regressions in Python, and expose the humbling truth: most "alpha" is just hidden beta.
References
Foundational VaR Theory
-
Jorion, P. (2007). Value at Risk: The New Benchmark for Managing Financial Risk (3rd ed.). McGraw-Hill. ISBN: 978-0-07-146495-6
-
Duffie, D. & Pan, J. (1997). "An Overview of Value at Risk." Journal of Derivatives, 4(3), 7β49. https://doi.org/10.3905/jod.1997.407971
-
RiskMetrics Group. (1996). RiskMetrics Technical Document (4th ed.). JP Morgan. https://www.msci.com/documents/10199/5915b101-4206-4ba0-aee2-3449d5c7e95a
Expected Shortfall & Coherent Risk Measures
-
Artzner, P., Delbaen, F., Eber, J.-M. & Heath, D. (1999). "Coherent Measures of Risk." Mathematical Finance, 9(3), 203β228. https://doi.org/10.1111/1467-9965.00068
-
Acerbi, C. & Tasche, D. (2002). "Expected Shortfall: A Natural Coherent Alternative to Value at Risk." Economic Notes, 31(2), 379β388. https://doi.org/10.1111/1468-0300.00091
-
Rockafellar, R.T. & Uryasev, S. (2000). "Optimization of Conditional Value-at-Risk." Journal of Risk, 2(3), 21β41. https://doi.org/10.21314/JOR.2000.038
VaR Backtesting
-
Kupiec, P.H. (1995). "Techniques for Verifying the Accuracy of Risk Measurement Models." Journal of Derivatives, 3(2), 73β84. https://doi.org/10.3905/jod.1995.407942
-
Christoffersen, P.F. (1998). "Evaluating Interval Forecasts." International Economic Review, 39(4), 841β862. https://doi.org/10.2307/2527341
Fat Tails & Student-t VaR
-
Blattberg, R.C. & Gonedes, N.J. (1974). "A Comparison of the Stable and Student Distributions as Statistical Models for Stock Prices." Journal of Business, 47(2), 244β280. https://doi.org/10.1086/295634
-
McNeil, A.J. & Frey, R. (2000). "Estimation of Tail-Related Risk Measures for Heteroscedastic Financial Time Series: An Extreme Value Approach." Journal of Empirical Finance, 7(3-4), 271β300. https://doi.org/10.1016/S0927-5398(00)00012-8
GARCH & Volatility Clustering
-
Engle, R.F. (1982). "Autoregressive Conditional Heteroscedasticity with Estimates of the Variance of United Kingdom Inflation." Econometrica, 50(4), 987β1007. https://doi.org/10.2307/1912773
-
Bollerslev, T. (1986). "Generalized Autoregressive Conditional Heteroskedasticity." Journal of Econometrics, 31(3), 307β327. https://doi.org/10.1016/0304-4076(86)90063-1
Component VaR & Risk Decomposition
-
Litterman, R. (1996). "Hot Spots and Hedges." In Risk Management: Approaches for Fixed Income Markets. Wiley.
-
Meucci, A. (2007). "Risk Contributions from Generic User-Defined Factors." SSRN Working Paper. https://ssrn.com/abstract=1031800
DeFi Risk & VaR
-
Harvey, C.R., Ramachandran, A. & Santoro, J. (2021). DeFi and the Future of Finance. Wiley. ISBN: 978-1-119-83601-8. (Chapter IX: Risk Management in DeFi)
-
Klages-Mundt, A., Harz, D., Gudgeon, L., Liu, J.-Y. & Minca, A. (2020). "While Mining Was Sleeping: Systemic Risk in Decentralized Finance." SSRN Working Paper. https://ssrn.com/abstract=3700747
Textbooks
-
McNeil, A.J., Frey, R. & Embrechts, P. (2015). Quantitative Risk Management: Concepts, Techniques and Tools (Revised ed.). Princeton University Press. ISBN: 978-0-691-16627-8
-
Chan, E.P. (2009). Quantitative Trading: How to Build Your Own Algorithmic Trading Business. Wiley. ISBN: 978-0-470-28488-4