Risk Management: Volatility, VaR, and Position Sizing
Every investment decision carries risk. Understanding how to measure risk (volatility, VaR, drawdown, and position sizing) is the foundation of protecting capital.
The first rule of investing is not losing money. The second rule is remembering the first rule. Risk management is the discipline of understanding, measuring, and controlling risk.
What Is Risk?
In finance, risk is the possibility that actual returns differ from expected returns. Typically, we care about the downside, the possibility of losing money.
Two types of risk:
- Systematic (market) risk: Affects all assets (recessions, wars)
- Idiosyncratic (specific) risk: Affects individual companies or sectors
Diversification can eliminate idiosyncratic risk but not systematic risk.
Volatility: The Primary Risk Measure
Volatility is the standard deviation of returns, a measure of how much returns typically deviate from the mean.
Annualized volatility: Daily or monthly volatility scaled to annual terms:
- Daily vol × √252 (252 trading days per year)
- Monthly vol × √12
High volatility: Larger swings in portfolio value. Low volatility: Smaller, steadier portfolio value.
Volatility in Python
import numpy as np
import pandas as pd
def annualized_volatility(returns, periods_per_year=252):
"""
Calculate annualized volatility from return series.
"""
return np.std(returns, ddof=1) * np.sqrt(periods_per_year)
def rolling_volatility(returns, window=20, periods_per_year=252):
"""
Calculate rolling volatility.
"""
return returns.rolling(window).std() * np.sqrt(periods_per_year)
def downside_volatility(returns, target=0, periods_per_year=252):
"""
Calculate downside (semi) volatility: only negative deviations from target.
"""
downside_returns = returns[returns < target]
return np.std(downside_returns - target, ddof=1) * np.sqrt(periods_per_year)
PROMPT
Use numpy and pandas to compute annualized volatility, a rolling 20-day volatility, and downside volatility from a return series.
Value at Risk (VaR)
VaR is a loss threshold over a chosen time horizon that is exceeded only with probability (1 - confidence). Formally: with confidence α and horizon h, VaR is the α-quantile of the loss distribution such that
VaR is not a worst-case or “maximum” loss, it is a quantile of the loss distribution. The remaining (1 - α) tail contains outcomes worse than VaR; their magnitude is not described by VaR at all (see Expected Shortfall / CVaR for that).
Example: “Our 1-day 95% VaR is $1 million." Interpretation: There is a 95% probability that the loss over one day will not exceed $1 million, and a 5% probability (about once every 20 trading days) that it will. VaR says nothing about how much larger the loss could be on those bad days.
VaR Calculation Methods
1. Historical VaR: Form the empirical distribution of historical multi-period losses and read off the (1 - α) quantile. Each path is a sum/compound of periods sampled daily returns, not a scaled single-period return.
2. Parametric (Variance-Covariance) VaR: Assume losses are normally distributed. With horizon h expressed in the same units as the returns (e.g., h days), μ_h = h · μ and σ_h = σ · √h, and the loss quantile is
For 95% confidence, z_{0.05} ≈ -1.645, so -z_{0.05} ≈ 1.645.
3. Monte Carlo VaR: Sample return paths of length h from the fitted return distribution (or directly from the empirical distribution). Each path’s loss is the compounded return over the horizon. The VaR is the (1 - α) percentile of those path losses.
from scipy import stats
def historical_var(returns, confidence=0.95, periods=1, n_scenarios=10000):
"""
Historical VaR via path-based aggregation: build multi-period returns by
summing/compounding sampled daily returns, then take the loss percentile.
"""
if len(returns) < periods:
raise ValueError("Not enough returns to build paths of length `periods`.")
rng = np.random.default_rng()
n_paths = n_scenarios
sampled = rng.choice(returns, size=(n_paths, periods), replace=True)
multi_period_returns = sampled.sum(axis=1) # arithmetic aggregation
# For a buy-and-hold P&L: wealth_path = prod(1 + r_t); loss = V * (1 - prod(1+r_t))
# We report the return quantile; multiply by portfolio_value to get $ VaR externally.
return np.percentile(multi_period_returns, (1 - confidence) * 100)
def parametric_var(returns, confidence=0.95, periods=1):
"""
Parametric VaR assuming normal returns over an h-period horizon.
VaR (as a positive loss) = -(mu_h + z_{1-alpha} * sigma_h), where
mu_h = periods * mu
sigma_h = sigma * sqrt(periods)
and z_{1-alpha} is the (1-alpha) standard-normal quantile (negative).
"""
mu = np.mean(returns)
sigma = np.std(returns, ddof=1)
mu_h = periods * mu
sigma_h = sigma * np.sqrt(periods)
z = stats.norm.ppf(1 - confidence) # negative, e.g. -1.645 at 95%
# VaR expressed as the negative quantile return (positive number = loss)
return -(mu_h + z * sigma_h)
def monte_carlo_var(returns, n_simulations=10000, confidence=0.95, periods=1):
"""
Monte Carlo VaR: build multi-period return paths by resampling from the
empirical distribution, then take the loss percentile of the path returns.
"""
rng = np.random.default_rng()
paths = rng.choice(returns, size=(n_simulations, periods), replace=True)
multi_period_returns = paths.sum(axis=1)
return np.percentile(multi_period_returns, (1 - confidence) * 100)
PROMPT
Use numpy and scipy.stats to compute three Value-at-Risk variants from a return series: historical, parametric normal, and Monte Carlo.
Maximum Drawdown
Drawdown is the peak-to-trough decline in portfolio value:
Maximum drawdown (MDD): The largest drawdown observed over a period.
Maximum drawdown is particularly important because:
- It represents the worst-case scenario historically
- Recovery requires gains equal to the percentage lost (a 50% loss requires a 100% gain to recover)
- Psychologically, large drawdowns cause investors to abandon strategies at the worst time
def max_drawdown(prices):
"""
Calculate maximum drawdown from a price series.
"""
wealth_index = prices / prices.iloc[0]
previous_peaks = wealth_index.cummax()
drawdowns = (wealth_index - previous_peaks) / previous_peaks
return drawdowns.min()
def drawdown_series(prices):
"""
Calculate the drawdown series.
"""
wealth_index = prices / prices.iloc[0]
previous_peaks = wealth_index.cummax()
return (wealth_index - previous_peaks) / previous_peaks
PROMPT
Use pandas to compute the full drawdown series and the maximum drawdown from a price series using cumulative peak tracking.
Position Sizing
How much should you bet on any given trade? This is one of the most important decisions in portfolio management.
Fixed fractional position sizing: Risk a fixed percentage of portfolio on each trade.
Example: $100,000 portfolio, willing to risk 1% per trade, entry $50, stop $48.
Kelly Criterion
The Kelly criterion maximizes expected log wealth (geometric growth rate):
Where:
- = fraction of portfolio to bet
- = odds received on the bet (profit / loss)
- = probability of winning
- = probability of losing (= 1 - p)
Most practitioners use half-Kelly or quarter-Kelly due to estimation error and risk aversion.
def kelly_fraction(win_prob, win_loss_ratio):
"""
Calculate Kelly fraction for position sizing.
win_prob: probability of winning
win_loss_ratio: average win / average loss
"""
q = 1 - win_prob
f = (win_loss_ratio * win_prob - q) / win_loss_ratio
return max(0, f)
def half_kelly(win_prob, win_loss_ratio):
"""Half Kelly: more conservative than full Kelly."""
return kelly_fraction(win_prob, win_loss_ratio) / 2
PROMPT
Compute the Kelly fraction and a half-Kelly variant for position sizing from win probability and a win/loss ratio.
Risk Budgeting
Institutional portfolios often allocate risk budgets across:
- Asset classes (equities, bonds, alternatives)
- Geographic regions
- Factors (market beta, size, value)
- Individual positions
The goal: ensure no single position or factor dominates portfolio risk.
Key Takeaways
- Risk = possibility of losing money (mostly downside)
- Volatility (std dev of returns) is the primary risk measure
- VaR summarizes worst-case loss at a given confidence level
- Maximum drawdown measures peak-to-trough decline
- Position sizing is the most controllable risk decision
- Kelly criterion maximizes geometric growth but is too aggressive for most investors
References
- Taleb, Dynamic Hedging. Wiley, 1996. ISBN: 978-0-471-15253-2.
- Ang, Asset Management. Oxford University Press, 2014. ISBN: 978-0-19-995932-3.
- Hull, Risk Management and Financial Institutions. Wiley, 2018. ISBN: 978-1-119-48238-2.