量化回测:夏普比率与最大回撤

夏普比率和最大回撤是评估量化策略最核心的两个指标。前者衡量风险调整后的收益,后者衡量最坏情况下的亏损幅度。

夏普比率 (Sharpe Ratio)

定义

夏普比率由威廉·夏普在1966年提出,公式为:

Sharpe Ratio=RpRfσp\text{Sharpe Ratio} = \frac{R_p - R_f}{\sigma_p}

  • RpR_p:策略的年化收益率
  • RfR_f:无风险利率(通常取国债收益率,A股常用3%~4%)
  • σp\sigma_p:策略收益率的年化标准差

直觉理解:每承受一单位风险,能获得多少超额收益

解读

夏普比率 评价
< 0 亏钱,不如买国债
0 ~ 1 一般,风险补偿不足
1 ~ 2 良好
2 ~ 3 优秀
> 3 极佳(要警惕过拟合)

注意:夏普比率假设收益率服从正态分布,对于有尾部风险的策略,可能会高估表现。

Python实现

import numpy as np
import pandas as pd

def sharpe_ratio(returns: pd.Series, risk_free_rate: float = 0.03,
                 periods_per_year: int = 252) -> float:
    # 计算年化夏普比率
    # returns: 日收益率序列(如 0.01 表示1%)
    # risk_free_rate: 年化无风险利率
    # periods_per_year: 每年交易日数(日频=252,周频=52,月频=12)
    # 日无风险利率
    rf_daily = (1 + risk_free_rate) ** (1 / periods_per_year) - 1
    
    excess_returns = returns - rf_daily
    
    if excess_returns.std() == 0:
        return 0.0
    
    # 年化
    annualized_mean = excess_returns.mean() * periods_per_year
    annualized_std = excess_returns.std() * np.sqrt(periods_per_year)
    
    return annualized_mean / annualized_std

最大回撤 (Maximum Drawdown)

定义

最大回撤衡量从历史最高点到随后最低点的最大跌幅:

MDD=PeakTroughPeak\text{MDD} = \frac{\text{Peak} - \text{Trough}}{\text{Peak}}

这个指标回答的问题是:如果在最差的时间点买入,最多会亏多少?

Python实现

def max_drawdown(equity_curve: pd.Series) -> dict:
    # 计算最大回撤
    # equity_curve: 净值曲线(初始为1.0)
    # 返回dict: 包含回撤比例、峰值日期、谷值日期
    running_max = equity_curve.cummax()
    drawdown = (equity_curve - running_max) / running_max
    
    mdd = drawdown.min()
    trough_date = drawdown.idxmin()
    
    # 找到峰值日期(回撤起点)
    peak_date = equity_curve[:trough_date].idxmax()
    
    return {
        "max_drawdown": abs(mdd),
        "peak_date": peak_date,
        "trough_date": trough_date,
        "peak_value": equity_curve[peak_date],
        "trough_value": equity_curve[trough_date],
    }

回撤持续时间

除了回撤幅度,恢复时间也很重要:

def drawdown_duration(equity_curve: pd.Series) -> pd.Series:
    # 计算每个时间点的回撤持续天数
    running_max = equity_curve.cummax()
    is_drawdown = equity_curve < running_max
    
    # 计算连续回撤天数
    duration = pd.Series(0, index=equity_curve.index)
    for i in range(1, len(equity_curve)):
        if is_drawdown.iloc[i]:
            duration.iloc[i] = duration.iloc[i-1] + 1
    
    return duration

年化收益率

为了让不同时间周期的策略可比,需要做年化处理:

def annualized_return(equity_curve: pd.Series,
                      periods_per_year: int = 252) -> float:
    # 计算年化收益率
    # equity_curve: 净值曲线
    # periods_per_year: 每年交易周期数
    total_return = equity_curve.iloc[-1] / equity_curve.iloc[0] - 1
    n_periods = len(equity_curve) - 1
    n_years = n_periods / periods_per_year
    
    if n_years <= 0:
        return 0.0
    
    annualized = (1 + total_return) ** (1 / n_years) - 1
    return annualized

综合回测报告

把上面的指标整合到一起:

def backtest_report(equity_curve: pd.Series, risk_free_rate: float = 0.03):
    # 生成回测统计报告
    # 日收益率
    daily_returns = equity_curve.pct_change().dropna()
    
    # 年化收益
    ann_ret = annualized_return(equity_curve)
    
    # 夏普比率
    sharpe = sharpe_ratio(daily_returns, risk_free_rate)
    
    # 最大回撤
    mdd_info = max_drawdown(equity_curve)
    
    # 收益回撤比(Calmar Ratio)
    calmar = ann_ret / mdd_info["max_drawdown"] if mdd_info["max_drawdown"] > 0 else float('inf')
    
    # 胜率
    win_rate = (daily_returns > 0).sum() / len(daily_returns)
    
    # 盈亏比
    avg_win = daily_returns[daily_returns > 0].mean()
    avg_loss = abs(daily_returns[daily_returns < 0].mean())
    profit_loss_ratio = avg_win / avg_loss if avg_loss > 0 else float('inf')
    
    report = {
        "年化收益率": f"{ann_ret:.2%}",
        "夏普比率": f"{sharpe:.2f}",
        "最大回撤": f"{mdd_info['max_drawdown']:.2%}",
        "回撤起始": str(mdd_info["peak_date"]),
        "回撤最低": str(mdd_info["trough_date"]),
        "Calmar比率": f"{calmar:.2f}",
        "日胜率": f"{win_rate:.2%}",
        "盈亏比": f"{profit_loss_ratio:.2f}",
        "交易天数": len(daily_returns),
    }
    
    print("=" * 40)
    print("       回测统计报告")
    print("=" * 40)
    for k, v in report.items():
        print(f"  {k:>10}: {v}")
    print("=" * 40)
    
    return report

注意事项

几个容易踩的坑:

  1. 年化标准差的计算:日标准差乘以252\sqrt{252},不是乘以252。波动率按平方根法则缩放。
  2. 无风险利率要匹配频率:年化利率3%不能直接减去日收益率,要先转成日利率。
  3. 样本量:回测区间太短的夏普比率没有参考价值,至少覆盖一个完整牛熊周期。
  4. 过拟合:夏普比率超过3要打个问号。如果参数稍微调一下就暴跌,大概率是过拟合。
  5. 幸存者偏差:用当前的股票池回测历史数据,会遗漏已退市的股票,导致结果偏乐观。