VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > 编程开发 > python数据分析 >
  • 绩效指标计算:年化收益率、最大回撤、夏普比率

第28章 量化回测系统开发(续)
28.3 绩效指标计算:年化收益率、最大回撤、夏普比率
28.3.1 先讲个踩坑经历:我被虚高的夏普比率坑了10万
刚做回测的时候,我算出来一个策略夏普比率2.8,看起来稳赚不赔,实盘跑了半年亏了10万,后来才发现我算夏普的时候用的是日度收益率直接平均,没年化,也没扣无风险利率,真实夏普连1都不到。很多新手容易被花里胡哨的指标骗,其实核心指标就三个:年化收益率看赚不赚钱,最大回撤看亏多少,夏普比率看赚的钱是不是靠运气,把这三个算明白,就不会被假的回测结果忽悠。

28.3.2 核心逻辑:三个指标的底层含义
三个指标分别对应量化策略的三个核心评价维度,缺一不可:
1.年化收益率:看策略的盈利能力,是最直观的收益指标,越高越好。
2.最大回撤:看策略的风险程度,是历史上最大的亏损幅度,越小越稳,拿得住。
3.夏普比率:看风险调整后的收益,是每承担1单位风险能拿到多少超额收益,越高说明策略性价比越高,不是靠赌运气赚钱。
避坑提醒:单独看任何一个指标都没用,只看年化不看回撤,可能赚3年的钱一年亏完;只看夏普不看年化,可能每年只赚2%,比存银行强不了多少。

28.3.3 实战:从零计算三个核心指标
我们完全用原生Python实现,不用第三方库,每一步计算逻辑都透明,避免黑箱误差。

  1. 实战代码:核心指标完整实现
    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 基础知识拓展:指标的常见坑与判断标准

  1. 指标计算的常见坑
    坑点 影响 规避方法
    年化用365天计算 年化收益率偏低10%-20% 用对应市场的年交易日数:A股252,港股250,美股253
    夏普不扣无风险利率 夏普虚高20%-50% 必须用年化收益减去3%左右的无风险利率再计算
    最大回撤用区间峰值计算 低估回撤,比如把跨年度的回撤拆成两个小回撤 用累计最高净值计算,全程遍历,不要拆分区间
    净值曲线包含未来数据 所有指标全部失真 净值曲线必须和实盘逻辑一致,不能用未来数据修复净值
  2. 合格策略的判断标准(针对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 实战优化:添加指标可视化

  1. 实战代码:绘制回撤曲线和收益分布
    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

 
 

相关教程