Skip to content

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)

Reference Resources