夏普比率和最大回撤是评估量化策略最核心的两个指标。前者衡量风险调整后的收益,后者衡量最坏情况下的亏损幅度。
夏普比率 (Sharpe Ratio)
定义
夏普比率由威廉·夏普在1966年提出,公式为:
- :策略的年化收益率
- :无风险利率(通常取国债收益率,A股常用3%~4%)
- :策略收益率的年化标准差
直觉理解:每承受一单位风险,能获得多少超额收益。
解读
| 夏普比率 | 评价 |
|---|---|
| < 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)
定义
最大回撤衡量从历史最高点到随后最低点的最大跌幅:
这个指标回答的问题是:如果在最差的时间点买入,最多会亏多少?
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
注意事项
几个容易踩的坑:
- 年化标准差的计算:日标准差乘以,不是乘以252。波动率按平方根法则缩放。
- 无风险利率要匹配频率:年化利率3%不能直接减去日收益率,要先转成日利率。
- 样本量:回测区间太短的夏普比率没有参考价值,至少覆盖一个完整牛熊周期。
- 过拟合:夏普比率超过3要打个问号。如果参数稍微调一下就暴跌,大概率是过拟合。
- 幸存者偏差:用当前的股票池回测历史数据,会遗漏已退市的股票,导致结果偏乐观。