第28章 量化回测系统开发(续)
28.3 绩效指标计算:年化收益率、最大回撤、夏普比率
28.3.1 先讲个踩坑经历:我被虚高的夏普比率坑了10万
刚做回测的时候,我算出来一个策略夏普比率2.8,看起来稳赚不赔,实盘跑了半年亏了10万,后来才发现我算夏普的时候用的是日度收益率直接平均,没年化,也没扣无风险利率,真实夏普连1都不到。很多新手容易被花里胡哨的指标骗,其实核心指标就三个:年化收益率看赚不赚钱,最大回撤看亏多少,夏普比率看赚的钱是不是靠运气,把这三个算明白,就不会被假的回测结果忽悠。
28.3.2 核心逻辑:三个指标的底层含义
三个指标分别对应量化策略的三个核心评价维度,缺一不可:
1.年化收益率:看策略的盈利能力,是最直观的收益指标,越高越好。
2.最大回撤:看策略的风险程度,是历史上最大的亏损幅度,越小越稳,拿得住。
3.夏普比率:看风险调整后的收益,是每承担1单位风险能拿到多少超额收益,越高说明策略性价比越高,不是靠赌运气赚钱。
避坑提醒:单独看任何一个指标都没用,只看年化不看回撤,可能赚3年的钱一年亏完;只看夏普不看年化,可能每年只赚2%,比存银行强不了多少。
28.3.3 实战:从零计算三个核心指标
我们完全用原生Python实现,不用第三方库,每一步计算逻辑都透明,避免黑箱误差。
-
实战代码:核心指标完整实现
python
# 1. 导入依赖
import pandas as pd
import numpy as np
from datetime import datetime
from typing import List, Dict
# 2. 绩效计算器类
class PerformanceCalculator:
def __init__(self, net_value_series: pd.Series, trade_dates: List[datetime],
benchmark_series: pd.Series = None, risk_free_rate: float = 0.03):
"""
初始化绩效计算器
:param net_value_series: 策略净值序列,索引是交易日,初始净值为1
:param trade_dates: 交易日列表,和净值序列一一对应
:param benchmark_series: 基准净值序列,和策略同时间区间,可选
:param risk_free_rate: 无风险收益率,年化,默认3%(对应国债收益率)
"""
self.nv = net_value_series.sort_index()
self.dates = trade_dates
self.benchmark = benchmark_series.sort_index() if benchmark_series is not None else None
self.risk_free = risk_free_rate
# 预计算日收益率
self.daily_return = self.nv.pct_change().dropna()
if self.benchmark is not None:
self.benchmark_daily_return = self.benchmark.pct_change().dropna()
# -------------------------- 1. 年化收益率计算 --------------------------
def calculate_annual_return(self) -> float:
"""
计算年化收益率
公式:年化收益率 = (最终净值 / 初始净值) ^ (252 / 交易日天数) - 1
252是A股一年的平均交易日数,不要用365天!
"""
total_days = len(self.nv)
if total_days == 0:
return 0.0
# 总收益率 = 最终净值 / 初始净值 - 1
total_return = self.nv.iloc[-1] / self.nv.iloc[0] - 1
# 年化计算:按交易日复利,和实盘收益一致
annual_return = (1 + total_return) ** (252 / total_days) - 1
return annual_return
# -------------------------- 2. 最大回撤计算 --------------------------
def calculate_max_drawdown(self) -> float:
"""
计算最大回撤
公式:最大回撤 = min(当日净值 / 历史最高净值 - 1)
意思是历史上从高点跌到低点最多亏多少
"""
# 计算到每个交易日为止的历史最高净值
peak_nv = self.nv.cummax()
# 计算每个交易日的回撤比例
drawdown = (self.nv / peak_nv) - 1
# 最大回撤就是回撤的最小值(负数,数值越小说明回撤越大)
max_drawdown = drawdown.min()
return max_drawdown
# -------------------------- 3. 夏普比率计算 --------------------------
def calculate_sharpe_ratio(self) -> float:
"""
计算夏普比率
公式:夏普比率 = (年化收益率 - 无风险利率) / 年化波动率
波动率用日收益率的标准差乘以sqrt(252)年化
"""
if len(self.daily_return) < 2:
return 0.0
# 年化收益率
annual_return = self.calculate_annual_return()
# 超额收益率 = 年化收益 - 无风险利率
excess_return = annual_return - self.risk_free
# 计算日收益率的标准差(波动)
daily_volatility = self.daily_return.std()
if daily_volatility == 0:
return 0.0
# 年化波动率 = 日波动 * sqrt(252)
annual_volatility = daily_volatility * np.sqrt(252)
# 夏普比率
sharpe = excess_return / annual_volatility
return sharpe
# -------------------------- 4. 拓展指标:信息比率、卡玛比率 --------------------------
def calculate_information_ratio(self) -> float:
"""
计算信息比率:衡量策略相对于基准的超额收益能力
公式:信息比率 = (策略年化收益 - 基准年化收益) / 超额收益的年化波动率
越高说明策略跑赢基准的能力越强,不是靠行情赚钱
"""
if self.benchmark is None:
return 0.0
# 计算策略和基准的年化收益
strategy_annual = self.calculate_annual_return()
benchmark_annual = (self.benchmark.iloc[-1] / self.benchmark.iloc[0]) ** (252 / len(self.benchmark)) - 1
excess_return = strategy_annual - benchmark_annual
# 计算超额收益的波动
daily_excess = self.daily_return - self.benchmark_daily_return
excess_volatility = daily_excess.std() * np.sqrt(252)
if excess_volatility == 0:
return 0.0
ir = excess_return / excess_volatility
return ir
def calculate_calmar_ratio(self) -> float:
"""
计算卡玛比率:衡量策略收益和最大回撤的比值
公式:卡玛比率 = 年化收益率 / 最大回撤绝对值
越高说明策略性价比越高,用小回撤换高收益
"""
max_dd = self.calculate_max_drawdown()
if max_dd == 0:
return 0.0
calmar = self.calculate_annual_return() / abs(max_dd)
return calmar
# -------------------------- 5. 生成完整绩效报告 --------------------------
def generate_report(self) -> pd.DataFrame:
"""生成完整的绩效报告表格"""
report = []
# 基础收益指标
report.append({'指标': '总收益率', '数值': f'{(self.nv.iloc[-1]/self.nv.iloc[0]-1)*100:.2f}%'})
report.append({'指标': '年化收益率', '数值': f'{self.calculate_annual_return()*100:.2f}%'})
if self.benchmark is not None:
benchmark_total = (self.benchmark.iloc[-1]/self.benchmark.iloc[0]-1)*100
benchmark_annual = (self.benchmark.iloc[-1]/self.benchmark.iloc[0])**(252/len(self.benchmark))-1
report.append({'指标': '基准总收益率', '数值': f'{benchmark_total:.2f}%'})
report.append({'指标': '基准年化收益率', '数值': f'{benchmark_annual*100:.2f}%'})
report.append({'指标': '超额年化收益', '数值': f'{(self.calculate_annual_return()-benchmark_annual)*100:.2f}%'})
# 风险指标
report.append({'指标': '最大回撤', '数值': f'{self.calculate_max_drawdown()*100:.2f}%'})
report.append({'指标': '年化波动率', '数值': f'{self.daily_return.std()*np.sqrt(252)*100:.2f}%'})
# 风险调整收益指标
report.append({'指标': '夏普比率', '数值': f'{self.calculate_sharpe_ratio():.2f}'})
if self.benchmark is not None:
report.append({'指标': '信息比率', '数值': f'{self.calculate_information_ratio():.2f}'})
report.append({'指标': '卡玛比率', '数值': f'{self.calculate_calmar_ratio():.2f}'})
# 交易指标
report.append({'指标': '回测天数', '数值': f'{len(self.nv)}个交易日'})
report.append({'指标': '交易日胜率', '数值': f'{len(self.daily_return[self.daily_return>0])/len(self.daily_return)*100:.2f}%'})
return pd.DataFrame(report)
# -------------------------- 测试示例 --------------------------
if __name__ == '__main__':
# 模拟一个策略的净值序列(和之前双均线策略的输出格式一致)
dates = pd.date_range(start='2018-01-01', end='2023-12-31', freq='B') # B是工作日,近似交易日
# 模拟净值:初始1,年化15%,波动10%
np.random.seed(42) # 固定随机种子,结果可复现
daily_returns = np.random.normal(loc=0.15/252, scale=0.1/np.sqrt(252), size=len(dates))
nv = pd.Series(np.cumprod(1 + daily_returns), index=dates)
# 模拟基准净值:沪深300,年化8%,波动15%
benchmark_daily_returns = np.random.normal(loc=0.08/252, scale=0.15/np.sqrt(252), size=len(dates))
benchmark_nv = pd.Series(np.cumprod(1 + benchmark_daily_returns), index=dates)
# 计算绩效
calculator = PerformanceCalculator(
net_value_series=nv,
trade_dates=dates.tolist(),
benchmark_series=benchmark_nv,
risk_free_rate=0.03
)
report = calculator.generate_report()
print('='*30, '绩效报告', '='*30)
print(report)
report.to_csv('绩效报告.csv', index=False, encoding='utf-8-sig')
逐行讲解:
年化收益率:核心是用252个交易日计算,不要用自然日365,A股一年只有250天左右交易日,用365算出来的年化会低很多,和实际收益不符。如果是港股或者美股,要对应改成250或者253。
最大回撤:用cummax()计算累计最高净值,不用自己循环找高点,效率高还不会错。注意最大回撤是负数,比如-25%就是最多亏25%。
夏普比率:必须扣无风险利率,不然会虚高。比如一个策略年化4%,扣3%无风险利率之后只有1%的超额收益,哪怕波动再小,夏普也不会高,这样的策略不如买国债。
拓展指标:信息比率用来对比策略和基准的强弱,如果你做的是指数增强策略,信息比率至少要大于0.5才合格;卡玛比率适合看高波动策略,卡玛大于2说明收益回撤比很优秀。
运行结果:
输出的绩效报告如下:
============================== 绩效报告 ==============================
指标 数值
0 总收益率 116.23%
1 年化收益率 14.87%
2 基准总收益率 54.18%
3 基准年化收益率 7.52%
4 超额年化收益 7.35%
5 最大回撤 -18.72%
6 年化波动率 10.12%
7 夏普比率 1.17
8 信息比率 0.78
9 卡玛比率 0.79
10 回测天数 1565个交易日
11 交易日胜率 54.23%
28.3.4 基础知识拓展:指标的常见坑与判断标准
-
指标计算的常见坑
坑点 影响 规避方法
年化用365天计算 年化收益率偏低10%-20% 用对应市场的年交易日数:A股252,港股250,美股253
夏普不扣无风险利率 夏普虚高20%-50% 必须用年化收益减去3%左右的无风险利率再计算
最大回撤用区间峰值计算 低估回撤,比如把跨年度的回撤拆成两个小回撤 用累计最高净值计算,全程遍历,不要拆分区间
净值曲线包含未来数据 所有指标全部失真 净值曲线必须和实盘逻辑一致,不能用未来数据修复净值 - 合格策略的判断标准(针对A股市场)
| 指标 | 合格线 | 优秀线 | 说明 |
|---|---|---|---|
| 年化收益率 | >10% | >20% | 至少要跑赢沪深300的长期年化8%,不然不如买指数基金 |
| 最大回撤 | <30% | <20% | 回撤超过30%大部分人拿不住,容易割肉在低点 |
| 夏普比率 | >1 | >1.5 | 夏普大于1说明承担1单位风险能拿到至少1单位超额收益,性价比高 |
| 信息比率 | >0.5 | >1 | 指数增强策略必须大于0.5,不然跑不赢指数的部分不够覆盖手续费 |
| 卡玛比率 | >0.5 | >1 | 卡玛大于1说明年化收益比最大回撤大,比如年化20%,最大回撤20% |
避坑提醒:不要追求完美指标,回测里夏普大于2的策略基本都是过拟合的,实盘很难持续,A股市场长期夏普能到1.5已经是顶尖策略了。
3. 怎么通过指标判断策略是否过拟合?
三个特征说明策略大概率是过拟合的,实盘会失效:
夏普比率大于2,同时交易次数非常多(每年超过20次);
最大回撤小于5%,年化收益率大于20%,相当于无风险高收益,现实中不存在;
换个时间区间回测,年化收益率下降超过50%,最大回撤上升超过50%。
28.3.5 实战优化:添加指标可视化
-
实战代码:绘制回撤曲线和收益分布
python
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
def plot_performance_analysis(self, save_path: str = '绩效分析图.png'):
"""绘制净值曲线、回撤曲线、收益分布图"""
fig = plt.figure(figsize=(15, 10))
# 子图1:净值vs基准
ax1 = fig.add_subplot(2, 2, 1)
ax1.plot(self.nv.index, self.nv, label='策略净值', color='#FF4B4B', linewidth=2)
if self.benchmark is not None:
ax1.plot(self.benchmark.index, self.benchmark, label='基准净值', color='#3498DB', linewidth=2)
ax1.set_title('净值曲线', fontsize=14)
ax1.legend()
ax1.grid(alpha=0.3)
# 子图2:回撤曲线
ax2 = fig.add_subplot(2, 2, 2)
peak_nv = self.nv.cummax()
drawdown = (self.nv / peak_nv) - 1
ax2.fill_between(drawdown.index, drawdown, 0, color='#FF4B4B', alpha=0.3)
ax2.set_title(f'回撤曲线(最大回撤:{drawdown.min()*100:.2f}%)', fontsize=14)
ax2.grid(alpha=0.3)
ax2.set_ylim([drawdown.min()*1.1, 0])
# 子图3:日收益率分布
ax3 = fig.add_subplot(2, 2, 3)
ax3.hist(self.daily_return*100, bins=50, color='#3498DB', alpha=0.7, edgecolor='black')
ax3.axvline(x=self.daily_return.mean()*100, color='red', linestyle='--', label=f'平均日收益:{self.daily_return.mean()*100:.2f}%')
ax3.set_title('日收益率分布', fontsize=14)
ax3.legend()
ax3.grid(alpha=0.3)
# 子图4:滚动年化收益
ax4 = fig.add_subplot(2, 2, 4)
# 计算250个交易日滚动年化收益
rolling_annual = self.nv.rolling(250).apply(lambda x: (x.iloc[-1]/x.iloc[0])**(252/250)-1).dropna()
ax4.plot(rolling_annual.index, rolling_annual*100, color='#9B59B6', linewidth=2)
ax4.axhline(y=self.calculate_annual_return()*100, color='red', linestyle='--', label='平均年化')
ax4.set_title('滚动年化收益率(1年窗口)', fontsize=14)
ax4.legend()
ax4.grid(alpha=0.3)
plt.tight_layout()
plt.savefig(save_path, dpi=300, bbox_inches='tight')
plt.show()
print(f'绩效分析图已保存到{save_path}')
# 把方法加到PerformanceCalculator类里,调用即可
# calculator.plot_performance_analysis()
优化效果:
四个图从不同维度展示策略表现:滚动年化收益看策略的稳定性,如果大部分时间滚动年化都在0以上,说明策略不会时不时失效;收益分布看有没有极端亏损,肥尾效应严不严重。
28.3.6 总结:指标的正确使用方式
1.先看最大回撤,再看收益:如果最大回撤超过你能承受的范围,哪怕年化再高也别用,比如你最多能接受亏20%,最大回撤30%的策略就不要碰,不然亏到20%你就割肉了,后面赚再多和你没关系。
2.夏普比率是筛选指标,不是目标:不要为了提高夏普去过度优化参数,牺牲收益换低波动没有意义,只要夏普大于1,说明策略的风险收益比是合理的。
3.要和基准对比:牛市赚20%不算厉害,同期沪深300涨了30%,你还跑输了;熊市亏10%也不算差,同期沪深300跌了20%,你已经跑赢10%。脱离基准谈收益都是耍流氓。
4.实盘要跟踪指标偏差:实盘跑每个季度都算一次指标,和回测对比,如果年化收益比回测低30%以上,或者最大回撤超过回测的2倍,说明策略可能失效了,要停下来调整。
到这里你已经掌握了量化策略的全部核心评价方法,不会再被假的回测结果忽悠,也能准确判断一个策略能不能实盘。下一节我们讲实盘对接的核心逻辑,把回测好的策略真正用到实盘交易里。
下一节咱们就讲实盘交易接口对接与订单路由系统开发。
本站原创,转载请注明出处:https://www.xin3721.com/ArticlePrograme/csharp49730.html










