Risk Management Complete Guide
This document introduces risk management strategies for quantitative trading, including position sizing, stop loss/take profit, capital management, and drawdown control.
The Importance of Risk Management
In quantitative trading, not losing money is more important than making money. Good risk management can: - Avoid catastrophic single-trade losses - Reduce overall portfolio volatility - Improve long-term stable returns
1. Position Sizing
1.1 Set Single Holding Limit
from finlab import data
from finlab.backtest import sim
close = data.get('price:收盤價')
position = close > close.average(20)
# Single holding limit at 10%
report = sim(
position,
resample='M',
position_limit=0.1 # 單一持股最多佔總資金 10%
)
1.2 Set Maximum Number of Holdings
# Method 1: Use is_largest to limit holdings count
top_n_position = (close > close.average(20)).is_largest(30)
# Method 2: Limit during backtesting
report = sim(
position,
resample='M',
position_limit=0.1,
# Maximum 30 holdings (10% each, 30 holdings = 300% leverage)
)
1.3 Adjust Position Size Based on Volatility
# Calculate volatility
returns = close.pct_change()
volatility = returns.rolling(60).std()
# Higher volatility = smaller position
inverse_vol_weight = 1 / volatility
normalized_weight = inverse_vol_weight / inverse_vol_weight.sum(axis=1)
# Combine with stock selection signal
position_with_vol = (close > close.average(20)) * normalized_weight
2. Stop Loss & Take Profit
2.1 Fixed Percentage Stop Loss/Take Profit
# Stop loss at 10%, take profit at 20%
position = entry_signal.hold_until(
exit=exit_signal,
stop_loss=0.10,
take_profit=0.20
)
report = sim(position, resample='M')
2.2 Optimize Stop Loss/Take Profit Using MAE/MFE
# Run initial backtest
report_initial = sim(position_without_stops, resample='M', upload=False)
# Examine MAE/MFE
trades = report_initial.get_trades()
mae_q75 = abs(trades['mae'].quantile(0.75)) # e.g., 8.5%
gmfe_q75 = trades['gmfe'].quantile(0.75) # e.g., 18.3%
print(f"建議停損: {mae_q75*1.2*100:.1f}%") # MAE Q75 * 1.2 = 10.2%
print(f"建議停利: {gmfe_q75*0.8*100:.1f}%") # GMFE Q75 * 0.8 = 14.6%
# Use optimized stop loss/take profit
position_optimized = entry_signal.hold_until(
exit=exit_signal,
stop_loss=mae_q75 * 1.2,
take_profit=gmfe_q75 * 0.8
)
2.3 Trailing Stop
A trailing stop is set via the trail_stop parameter in sim():
# Exit when price drops 8% from peak
position = entry_signal.hold_until(
exit=exit_signal,
stop_loss=0.10
)
report = sim(position, resample='M', trail_stop=0.08) # 從最高點回檔 8%
2.4 Time-Based Stop
import pandas as pd
# Auto-exit after holding for 30 days
def add_time_stop(position, max_hold_days=30):
"""新增時間停損"""
hold_days = (position > 0).astype(int).groupby(level=1).cumsum()
time_exit = hold_days > max_hold_days
return position & (~time_exit)
position_with_time_stop = add_time_stop(position, max_hold_days=30)
3. Capital Management
3.1 Kelly Criterion
def kelly_criterion(win_rate, avg_win, avg_loss):
"""
Calculate Kelly fraction
Args:
win_rate: win rate (0-1)
avg_win: average win rate
avg_loss: average loss rate (positive value)
Returns:
float: recommended investment fraction
"""
if avg_loss == 0:
return 0
kelly = (win_rate * avg_win - (1 - win_rate) * avg_loss) / avg_win
return max(0, min(kelly, 1)) # 限制在 0-1 之間
# Usage example
stats = report.get_stats()
kelly_ratio = kelly_criterion(
win_rate=stats['win_ratio'],
avg_win=trades[trades['return'] > 0]['return'].mean(),
avg_loss=abs(trades[trades['return'] < 0]['return'].mean())
)
print(f"凱利建議投入比例: {kelly_ratio*100:.1f}%")
# In practice, use 0.5 * Kelly (half-Kelly) for a more conservative approach
print(f"半凱利建議: {kelly_ratio*0.5*100:.1f}%")
3.2 Fixed Fractional Method
# Invest a fixed fraction of total capital each time
RISK_PER_TRADE = 0.02 # 2% risk per trade
# Calculate position size based on stop loss level
stop_loss = 0.10 # 10% stop loss
position_size = RISK_PER_TRADE / stop_loss # = 0.2 = 20%
print(f"建議部位大小: {position_size*100:.0f}%")
4. Drawdown Control
4.1 Monitor Drawdown and Pause Trading
def check_drawdown_limit(report, max_drawdown=0.20):
"""Check if maximum drawdown limit is exceeded"""
stats = report.get_stats()
current_dd = stats['max_drawdown']
if abs(current_dd) > max_drawdown:
print(f"警告:回撤 {current_dd*100:.1f}% 超過限制 {max_drawdown*100:.0f}%")
print("建議暫停交易,檢視策略!")
return False
return True
# Usage
if not check_drawdown_limit(report, max_drawdown=0.25):
# Pause trading logic
pass
4.2 Dynamic Position Adjustment Based on Drawdown
def adjust_position_by_drawdown(position, creturn, base_size=1.0):
"""Dynamically adjust positions based on current drawdown
Larger drawdown = smaller positions
"""
# Calculate current drawdown
peak = creturn.expanding().max()
drawdown = (creturn - peak) / peak
# Adjustment factor (at -20% drawdown, positions halved)
adj_factor = 1 + drawdown / 0.20
adj_factor = adj_factor.clip(0.5, 1.0) # 限制在 0.5-1.0
# Adjust positions
adjusted_position = position * adj_factor.reindex(position.index, method='ffill')
return adjusted_position
# Usage
position_adjusted = adjust_position_by_drawdown(
position,
report.creturn,
base_size=1.0
)
5. Risk Metric Monitoring
5.1 Set Risk Alerts
def risk_alert(report):
"""Risk alert check"""
stats = report.get_stats()
alerts = []
# Check 1: Sharpe ratio too low
if stats['daily_sharpe'] < 0.5:
alerts.append(f"⚠️ 夏普率過低: {stats['daily_sharpe']:.2f}")
# Check 2: Max drawdown too large
if abs(stats['max_drawdown']) > 0.30:
alerts.append(f"⚠️ 最大回撤過大: {stats['max_drawdown']*100:.1f}%")
# Check 3: Win rate too low
if stats['win_ratio'] < 0.40:
alerts.append(f"⚠️ 勝率過低: {stats['win_ratio']*100:.1f}%")
# Check 4: Average profit/loss ratio too low
trades = report.get_trades()
avg_win = trades[trades['return'] > 0]['return'].mean()
avg_loss = abs(trades[trades['return'] < 0]['return'].mean())
win_loss_ratio = avg_win / avg_loss if avg_loss > 0 else 0
if win_loss_ratio < 1.5:
alerts.append(f"⚠️ 獲利/虧損比過低: {win_loss_ratio:.2f}")
# Output alerts
if alerts:
print("=" * 50)
print("風險警報:")
for alert in alerts:
print(alert)
print("=" * 50)
else:
print("✅ 風險檢查通過")
return len(alerts) == 0
# Usage
risk_alert(report)
5.2 Periodic Risk Report
def generate_risk_report(report):
"""Generate risk report"""
stats = report.get_stats()
trades = report.get_trades()
risk_metrics = {
'年化報酬率': f"{stats['daily_mean']*100:.2f}%",
'年化波動率': f"{stats['daily_std']*100:.2f}%",
'夏普率': f"{stats['daily_sharpe']:.2f}",
'最大回撤': f"{stats['max_drawdown']*100:.2f}%",
'勝率': f"{stats['win_ratio']*100:.1f}%",
'平均持有天數': f"{trades['period'].mean():.1f} 天",
'總交易次數': len(trades),
}
print("\n" + "=" * 50)
print("風險報告")
print("=" * 50)
for key, value in risk_metrics.items():
print(f"{key:12s}: {value}")
print("=" * 50 + "\n")
# Usage
generate_risk_report(report)
6. Practical Risk Management Checklist
Backtesting Stage
- [ ] Single holding weight <= 10%
- [ ] Reasonable stop loss (8-12%) and take profit (15-25%) set
- [ ] Max drawdown < 30%
- [ ] Sharpe ratio > 1.0
- [ ] Win rate > 45% or average profit/loss ratio > 1.5
Live Trading Stage
- [ ] Tested with simulated orders for at least 1 month
- [ ] Initial capital <= 20% of total capital
- [ ] Monitor drawdown and holdings daily
- [ ] Review live vs backtest divergence weekly
- [ ] Generate risk report monthly
Emergency Situations
- [ ] Drawdown > 20%: Pause adding new positions
- [ ] Drawdown > 30%: Close 50% of positions
- [ ] Drawdown > 40%: Close all positions and review the strategy
- [ ] Consecutive losses > 5 trades: Pause trading and review the strategy
Complete Code Example
from finlab import data
from finlab.backtest import sim
# Load data
close = data.get('price:收盤價')
# Strategy logic
entry = close > close.average(20)
exit_signal = close < close.average(10)
# Add risk controls
position = entry.hold_until(
exit=exit_signal,
stop_loss=0.10, # 停損 10%
take_profit=0.20 # 停利 20%
)
# Backtest (single holding limit 10%, trailing stop 8%)
report = sim(
position,
resample='M',
position_limit=0.1,
trail_stop=0.08,
upload=False
)
# Risk check
if risk_alert(report):
print("策略通過風險檢查,可考慮實盤")
else:
print("策略未通過風險檢查,需要優化")
# Generate risk report
generate_risk_report(report)