Risk Management¶
🎯 Learning Objectives
- Master position sizing techniques
- Learn stop loss strategies
- Understand portfolio risk management
- Calculate and use Value at Risk (VaR)
Risk management is the most important aspect of trading. This chapter covers essential risk management techniques to protect your capital.
Position Sizing¶
Fixed Dollar Amount¶
Risk a fixed dollar amount per trade:
# Risk fixed dollar amount per trade
account_value = 100000
risk_per_trade = 0.02 # 2% of account
risk_amount = account_value * risk_per_trade
entry_price = 150
stop_loss = 145
risk_per_share = entry_price - stop_loss
position_size = risk_amount / risk_per_share
print(f"Position Size: {position_size:.0f} shares")
print(f"Position Value: ${position_size * entry_price:.2f}")
Fixed Percentage of Account¶
# Risk fixed percentage of account
account_value = 100000
position_pct = 0.10 # 10% of account
position_value = account_value * position_pct
entry_price = 150
position_size = position_value / entry_price
print(f"Position Size: {position_size:.0f} shares")
Volatility-Based Position Sizing¶
import pandas as pd
import numpy as np
import yfinance as yf
# Size position based on volatility
data = yf.download("AAPL", period="1y")
returns = data['Close'].pct_change()
volatility = returns.std() * np.sqrt(252) # Annualized volatility
account_value = 100000
target_volatility = 0.15 # 15% target portfolio volatility
position_volatility = volatility
# Adjust position size to achieve target volatility
position_size_pct = target_volatility / position_volatility
position_size_pct = min(position_size_pct, 1.0) # Cap at 100%
position_value = account_value * position_size_pct
print(f"Position Size: {position_size_pct:.2%} of account")
print(f"Position Value: ${position_value:.2f}")
Kelly Criterion¶
Optimal position sizing based on win rate and risk-reward:
def kelly_criterion(win_rate, avg_win, avg_loss):
"""Calculate optimal position size using Kelly Criterion"""
if avg_loss == 0:
return 0
win_loss_ratio = avg_win / abs(avg_loss)
kelly = win_rate - ((1 - win_rate) / win_loss_ratio)
# Kelly can be negative (don't trade) or > 1 (too aggressive)
# Use fractional Kelly (e.g., 25% of full Kelly)
return max(0, min(kelly, 1)) # Cap between 0 and 1
# Example
win_rate = 0.6 # 60% win rate
avg_win = 0.05 # 5% average win
avg_loss = -0.03 # 3% average loss
kelly_pct = kelly_criterion(win_rate, avg_win, avg_loss)
fractional_kelly = kelly_pct * 0.25 # Use 25% of Kelly
print(f"Full Kelly: {kelly_pct:.4f} ({kelly_pct*100:.2f}%)")
print(f"Fractional Kelly (25%): {fractional_kelly:.4f} ({fractional_kelly*100:.2f}%)")
Risk Parity¶
Equal risk contribution from each position:
def risk_parity_weights(returns):
"""Equal risk contribution from each asset"""
inv_vol = 1 / returns.std()
weights = inv_vol / inv_vol.sum()
return weights
# Multiple assets
stocks = ['AAPL', 'GOOGL', 'MSFT']
data = yf.download(stocks, period="1y")['Close']
returns = data.pct_change().dropna()
weights = risk_parity_weights(returns)
print("Risk Parity Weights:")
for stock, weight in zip(stocks, weights):
print(f"{stock}: {weight:.2%}")
Stop Losses¶
Fixed Percentage Stop¶
entry_price = 150
stop_loss_pct = 0.05 # 5% stop loss
stop_loss_price = entry_price * (1 - stop_loss_pct)
print(f"Stop Loss Price: ${stop_loss_price:.2f}")
print(f"Risk per share: ${entry_price - stop_loss_price:.2f}")
Fixed Dollar Stop¶
entry_price = 150
max_loss_per_share = 5 # $5 maximum loss per share
stop_loss_price = entry_price - max_loss_per_share
print(f"Stop Loss Price: ${stop_loss_price:.2f}")
ATR-Based Stop Loss¶
Use Average True Range for dynamic stops:
def calculate_atr(high, low, close, period=14):
"""Calculate Average True Range"""
tr1 = high - low
tr2 = abs(high - close.shift())
tr3 = abs(low - close.shift())
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
atr = tr.rolling(window=period).mean()
return atr
data = yf.download("AAPL", period="1y")
atr = calculate_atr(data['High'], data['Low'], data['Close'])
# Stop loss at 2 ATR below entry
entry_price = data['Close'].iloc[-1]
stop_loss = entry_price - 2 * atr.iloc[-1]
print(f"Entry: ${entry_price:.2f}")
print(f"ATR: ${atr.iloc[-1]:.2f}")
print(f"Stop Loss (2 ATR): ${stop_loss:.2f}")
Trailing Stop¶
Stop loss that follows price up:
def trailing_stop(prices, trail_pct=0.05):
"""Calculate trailing stop"""
highest_price = prices.expanding().max()
trailing_stop = highest_price * (1 - trail_pct)
return trailing_stop
data = yf.download("AAPL", period="6m")['Close']
trailing_stops = trailing_stop(data, trail_pct=0.05)
# Plot
import matplotlib.pyplot as plt
plt.figure(figsize=(12, 6))
plt.plot(data.index, data.values, label='Price')
plt.plot(data.index, trailing_stops.values, label='Trailing Stop', linestyle='--')
plt.legend()
plt.title('Trailing Stop Loss')
plt.show()
Time-Based Stop¶
Exit after certain time period:
# Exit position after N days if not profitable
entry_date = pd.Timestamp('2023-01-01')
current_date = pd.Timestamp('2023-01-15')
max_holding_days = 10
if (current_date - entry_date).days > max_holding_days:
print("Time stop triggered - exit position")
Portfolio Risk¶
Portfolio Volatility¶
import numpy as np
# Portfolio with multiple stocks
weights = np.array([0.4, 0.3, 0.3])
covariance_matrix = np.array([[0.04, 0.02, 0.01],
[0.02, 0.03, 0.015],
[0.01, 0.015, 0.025]])
# Portfolio variance
portfolio_variance = np.dot(weights.T, np.dot(covariance_matrix, weights))
portfolio_volatility = np.sqrt(portfolio_variance)
print(f"Portfolio Volatility: {portfolio_volatility:.4f} ({portfolio_volatility*100:.2f}%)")
Portfolio Beta¶
# Calculate portfolio beta
stock_betas = np.array([1.2, 0.8, 1.0]) # Individual stock betas
weights = np.array([0.4, 0.3, 0.3])
portfolio_beta = np.dot(weights, stock_betas)
print(f"Portfolio Beta: {portfolio_beta:.2f}")
# Interpret
if portfolio_beta > 1:
print("Portfolio is more volatile than market")
elif portfolio_beta < 1:
print("Portfolio is less volatile than market")
else:
print("Portfolio moves with market")
Correlation Risk¶
# High correlation increases portfolio risk
stocks = ['AAPL', 'GOOGL', 'MSFT']
data = yf.download(stocks, period="1y")['Close']
returns = data.pct_change().dropna()
correlation_matrix = returns.corr()
print("Correlation Matrix:")
print(correlation_matrix)
# Average correlation
avg_correlation = correlation_matrix.values[np.triu_indices_from(correlation_matrix.values, k=1)].mean()
print(f"\nAverage Correlation: {avg_correlation:.4f}")
if avg_correlation > 0.7:
print("Warning: High correlation - limited diversification")
Value at Risk (VaR)¶
Parametric VaR¶
from scipy import stats
returns = np.array([0.01, 0.02, -0.01, 0.03, -0.02, 0.01, -0.01, 0.02])
confidence = 0.95
# Parametric VaR (assuming normal distribution)
mean_return = returns.mean()
std_return = returns.std()
z_score = stats.norm.ppf(1 - confidence)
var = mean_return - z_score * std_return
print(f"Mean Return: {mean_return:.4f}")
print(f"Std Dev: {std_return:.4f}")
print(f"95% VaR: {var:.4f} ({var*100:.2f}%)")
# VaR in dollars
portfolio_value = 100000
var_dollars = abs(var) * portfolio_value
print(f"95% VaR: ${var_dollars:.2f}")
Historical VaR¶
# Historical VaR (no distribution assumption)
confidence = 0.95
var_historical = np.percentile(returns, (1 - confidence) * 100)
print(f"95% Historical VaR: {var_historical:.4f} ({var_historical*100:.2f}%)")
Conditional VaR (CVaR)¶
Expected loss given VaR is exceeded:
# CVaR (Expected Shortfall)
var_threshold = np.percentile(returns, 0.05) # 5th percentile
cvar = returns[returns <= var_threshold].mean()
print(f"95% VaR: {var_threshold:.4f}")
print(f"95% CVaR: {cvar:.4f} ({cvar*100:.2f}%)")
Portfolio VaR¶
# VaR for portfolio
portfolio_returns = np.dot(returns, weights)
portfolio_var = np.percentile(portfolio_returns, 0.05)
print(f"Portfolio 95% VaR: {portfolio_var:.4f}")
Risk Metrics¶
Maximum Drawdown¶
def max_drawdown(returns):
"""Calculate maximum drawdown"""
cumulative = (1 + returns).cumprod()
running_max = cumulative.expanding().max()
drawdown = (cumulative - running_max) / running_max
return drawdown.min()
returns = pd.Series([0.01, 0.02, -0.01, -0.02, 0.03, -0.01, 0.02])
max_dd = max_drawdown(returns)
print(f"Maximum Drawdown: {max_dd:.4f} ({max_dd*100:.2f}%)")
Drawdown Duration¶
def drawdown_duration(returns):
"""Calculate time to recover from drawdown"""
cumulative = (1 + returns).cumprod()
running_max = cumulative.expanding().max()
drawdown = (cumulative - running_max) / running_max
# Find periods in drawdown
in_drawdown = drawdown < 0
drawdown_periods = in_drawdown.groupby((in_drawdown != in_drawdown.shift()).cumsum()).sum()
max_duration = drawdown_periods.max()
return max_duration
max_duration = drawdown_duration(returns)
print(f"Maximum Drawdown Duration: {max_duration} periods")
Risk-Adjusted Returns¶
def sharpe_ratio(returns, risk_free_rate=0.02):
"""Calculate Sharpe ratio"""
excess_returns = returns - risk_free_rate / 252
sharpe = np.sqrt(252) * excess_returns.mean() / returns.std()
return sharpe
def sortino_ratio(returns, risk_free_rate=0.02):
"""Calculate Sortino ratio (only penalizes downside volatility)"""
excess_returns = returns - risk_free_rate / 252
downside_returns = excess_returns[excess_returns < 0]
downside_std = downside_returns.std()
sortino = np.sqrt(252) * excess_returns.mean() / downside_std
return sortino
def calmar_ratio(returns):
"""Calculate Calmar ratio (return / max drawdown)"""
annual_return = returns.mean() * 252
max_dd = abs(max_drawdown(returns))
calmar = annual_return / max_dd
return calmar
sharpe = sharpe_ratio(returns)
sortino = sortino_ratio(returns)
calmar = calmar_ratio(returns)
print(f"Sharpe Ratio: {sharpe:.2f}")
print(f"Sortino Ratio: {sortino:.2f}")
print(f"Calmar Ratio: {calmar:.2f}")
Risk Limits¶
Daily Loss Limit¶
# Set daily loss limit
daily_loss_limit = 0.02 # 2% daily loss limit
account_value = 100000
def check_daily_loss(current_value, starting_value, limit):
daily_return = (current_value - starting_value) / starting_value
if daily_return < -limit:
print(f"Daily loss limit exceeded: {daily_return:.2%}")
return True
return False
Position Limits¶
# Maximum position size
max_position_pct = 0.20 # 20% max per position
max_correlation = 0.70 # Max correlation between positions
def check_position_limits(new_position_pct, existing_positions, new_correlation):
if new_position_pct > max_position_pct:
print(f"Position size exceeds limit: {new_position_pct:.2%} > {max_position_pct:.2%}")
return False
if new_correlation > max_correlation:
print(f"Correlation too high: {new_correlation:.2f} > {max_correlation:.2f}")
return False
return True
Key Takeaways¶
- Position Sizing: Risk only 1-2% of account per trade
- Stop Losses: Always use stops to limit losses
- Portfolio Risk: Diversify to reduce overall risk
- VaR: Quantify potential losses at confidence levels
- Risk Metrics: Monitor drawdown, Sharpe, Sortino ratios
- Risk Limits: Set and enforce daily and position limits
- Never risk more than you can afford to lose
Previous: Trading Strategies | Next: Advanced Trading Strategies