一、说在前面的话众所周知股票、债券、商品等投资市场存在着各种各样明显的轮动效应最常见的有3种。第一种是资产轮动也就是大类资产间的轮动在不同的市场/宏观环境下配置不同的类别的资产可以获得更显著的投资收益比如资产配置中最经典的美林时钟按照经济增长与通胀将经济周期划分为4个阶段每个阶段都有最适合的投资品种衰退期买债券复苏期炒股票过热期投大宗商品滞涨期持现金。对了常见的股债轮动策略也是属于这个类别。第二种是行业轮动也可以称之为板块轮动说的就是各行业/板块在相同时期会表现出差异性各自支棱的时间不同步因此可以通过不同时期配置不同行业/板块从而博取高于市场基准的投资收益。第三种就是风格轮动跟行业轮动和板块轮动原理类似只不是走强的不是轮动/板块而是某类投资风格比方我们常见的大小盘轮动、成长价值轮动就属于这种晨星的投资风格箱就是按此划分的。第一种是各类资产之间的轮动后两种更多的是某类资产(如股票)内的轮动不过抽象出来它们都是具有价格走势的证券/品种只要它们有价格走势就可以非常容易地构建出轮动策略核心因素有两个。第一个是候选池也就是在哪个“池子”里面进行轮动是在股票、债券、商品、现金里面进行轮动呢还是在大小盘风格里面进行轮动这需要咱一开始就进行框定。第二个就是强弱排序也就是采用什么方法规则对池子里面的品种进行排序每期买入最强的那个或前几个品种就跟和联胜社团每两年要选举一次那样是靠叔父辈发话还是靠才智拳头拿到龙头棍当上坐馆。这些问题咱都会在本篇文章内解决就不扯闲篇儿了赶紧一起出发lets go~哦对了记得戴上头盔~~二、轮动策略构建1.候选池确定第一步咱先要确定轮动策略的候选池后面的所有步骤都是围绕着这个候选池展开的为了讲解方便最开始先构建A股里面常见的风格轮动后续要换成别的轮动形式只要更换里面证券代码列表就可以了。在大小盘风格轮动中常用沪深300指数代表大盘风格中证500指数代表小盘风格为了体现可交易性这里用的是ETFExchange Traded Fund交易型开放式指数基金沪深300ETF代码是510300中证500ETF代码是510500。其实现在代表小盘风格感觉中证1000指数/中证2000指数更为合适但是最早上市的中证1000ETF从2016年底才开始数据不够10年不方便长期回测于是还是用中证500这些细节不用太在意理解整个轮动策略的框架和回测思路就可以了。在价值成长风格轮动中个人喜欢用红利ETF代码510880代表价值风格创业板ETF代码159915代表成长风格这里没有太死板的规定根据个人的理解和喜好来决定就可以了。至此咱有了风格轮动策略的候选池沪深300ETF510300中证500ETF510500红利ETF510880创业板ETF159915。再啰唆一句候选池的确定没有什么条条框框全凭自己的对交易的理解和喜好例如你就放余额宝和银华日利都可以只要能取到序列数据。2.数据获取在获取数据之前咱要先把编程环境搭上本次策略开发用的编程语言是Python 3.13代码编辑和执行软件是Jupyter Notebook你使用别的Python版本和集成开发环境IDE都是可以的只要安装的库大体对应就行磨刀不误砍柴工咱先把接下来要用的库导入进来。import os import time import numpy as np import pandas as pd import akshare as ak import quantstats as qs from datetime import datetime import matplotlib.pyplot as plt from sklearn.linear_model import LinearRegression import warnings warnings.filterwarnings(ignore)os、time、datetime和warning这些库都是原生Python环境自带的无需单独安装另外一些库如果你还没有的话可以通过以下命令一次性安装。pip install numpy pandas matplotlib akshare scikit-learn quantstats-reloaded这些库的源头在国外国内安装可能有点儿慢想加速的话可以带上国内的镜像源。通过阿里镜像源安装pip install numpy pandas matplotlib akshare scikit-learn quantstats-reloaded -i https://mirrors.aliyun.com/pypi/simple/通过清华镜像源安装pip install numpy pandas matplotlib akshare scikit-learn quantstats-reloaded -i https://pypi.tuna.tsinghua.edu.cn/simple成功导入这些库之后根据之前定的候选池把池子里面证券标的数据取出来就可以了这里用到是一个免费开源的金融数据接口库akshare无需注册就可以直接使用。在这里咱取得是这4个ETF的收盘价序列数据Dataframe中的列名就是对应每个ETF的证券代码。# 510300沪深300ETF代表大盘 # 510500中证500ETF代表小盘 # 510880红利ETF代表价值 # 159915创业板ETF代表成长 code_list [510300, 510500, 510880, 159915] start_date 20150101 end_date 20250828 df_list [] for code in code_list: print(f正在获取[{code}]行情数据...) # adjust-不复权、qfq-前复权、hfq-后复权 df ak.fund_etf_hist_em(symbolcode, perioddaily, start_datestart_date, end_dateend_date, adjusthfq) df.insert(0, code, code) df_list.append(df) time.sleep(3) print(数据获取完毕) all_df pd.concat(df_list, ignore_indexTrue) data all_df.pivot(index日期, columnscode, values收盘)[code_list] data.index pd.to_datetime(data.index) data data.sort_index() data.head(10)3.数据计算轮动策略的第二个核心就是强弱排序这里采用的是动量策略的规则每天买入前N个交易日涨幅最大的那一个ETF因此需要计算出每个ETF在每一天的前N个交易日的涨幅。为了方便后面的回测还需要顺带计算出每个ETF的日涨幅计算代码和运行结果如下。# 动量长度 N 10 # 计算每日涨跌幅和N日涨跌幅 for code in code_list: data[日收益率_code] data[code] / data[code].shift(1) - 1.0 data[涨幅_code] data[code] / data[code].shift(N1) - 1.0 # 去掉缺失值 data data.dropna() data[[涨幅_v for v in code_list]].head(10)4.信号生成与回测经过上一步的计算咱就知道了每一天所有ETF的区间涨幅就可以筛选出涨幅最大的那个ETF根据这个信号买入这个最强的ETF作为轮动策略的持仓。知道了策略每一天的持仓就知道策略每一天的收益率采用连乘的方式进而计算出策略的净值曲线有了这条净值曲线就可以统计出回测的各项绩效指标。# 取出每日涨幅最大的证券 data[信号] data[[涨幅_v for v in code_list]].idxmax(axis1).str.replace(涨幅_, ) # 今日的涨幅由昨日的持仓产生 data[信号] data[信号].shift(1) data data.dropna() data[轮动策略日收益率] data.apply(lambda x: x[日收益率_x[信号]], axis1) # 第一天尾盘交易当日涨幅不纳入 data.loc[data.index[0],轮动策略日收益率] 0.0 data[轮动策略净值] (1.0 data[轮动策略日收益率]).cumprod() data[[涨幅_v for v in code_list][信号,轮动策略日收益率,轮动策略净值]].head(10)这里需要特别说明两点第一点是交易信号列要前移1格对应第4行代码当中的shift因为策略今日的涨跌幅/收益率是由昨日的持仓产生的这里其实也暗含着一个回测设定就是当日收盘价计算交易信号当日收盘价成交如果前移2格就是当日收盘价计算交易信号明日收盘价成交。第二点就是回测第一个交易日收益率为0这跟第一点暗含的设定有关当日收盘价才成交产生持仓故第一个交易日收盘前没有持仓不纳入信号证券的涨跌幅收益率为0。5.结果统计和画图经过上一步终于回测计算出轮动策略的净值曲线了咱先把这4个ETF和策略净值都画出来直观对比一下。# 显示中文设置 plt.rcParams[font.sans-serif][SimHei] plt.rcParams[axes.unicode_minus]False # 获取ETF名称 etf_df ak.fund_etf_spot_em() code_to_name_dict etf_df.set_index(代码)[名称].to_dict() # 绘制净值曲线图 fig, ax plt.subplots(figsize(15, 6)) ax.set_xlabel(日期) ax.set_ylabel(净值) name_list [] for code in code_list: if code not in code_to_name_dict.keys(): continue name code_to_name_dict[code] name_list.append(name) data[name净值] data[code] / data[code].iloc[0] ax.plot(data.index, data[name净值].values, linestyle--) ax.plot(data.index, data[轮动策略净值].values, linestyle-, color#FF8124) name_list.append(轮动策略) # 显示图例和标题 ax.legend(name_list) ax.set_title(轮动策略净值曲线对比) plt.show()图中那条黄色实线就是轮动策略的净值曲线它最终跑赢了这4个ETF中的任何一个实现了通过轮动配置增强了策略收益。光看净值曲线还不够还需要获得收益率、夏普率等回测统计指标信息在这里咱可以通过量化工具库quantstats快速实现导入该库后可以通过reports.html函数生成html格式的完整回测报告回测报告会存储在与策略代码同级的文件夹下也可以通过reports.basic函数输出基本的回测报告信息也还有很多类似的函数有空可以逐一去探索。#将完整回测报告存为HTML文件 title 轮动策略回测报告_原始版 output_file os.path.join(os.getcwd(), f{title}_{datetime.now().strftime(%Y%m%d_%H%M%S)}.html) qs.reports.html(data[轮动策略净值], benchmarkdata[沪深300ETF净值], titletitle, outputoutput_file) print(f已将回测报告保存到文件: {output_file}) #输出基本回测报告信息 qs.reports.basic(data[轮动策略净值], benchmarkdata[沪深300ETF净值])输出的部分结果截图第一幅图显示的是回测当中的各种统计指标其中关键的是策略累计收益率(Cumulative Return)是333.09%年化收益率(CAGR)是10.01%比基准沪深300ETF的年化收益2.47%高出了7个百分点策略的夏普率(Sharpe)为0.66也高于基准的0.28不过最大回撤(Max Drawdown)达到了47.68%超过了基准不是大心脏真扛不下来。第二幅图是轮动策略和基准的累计收益率曲线第三幅图是策略相对基准每个月的超额收益这个就很直观了就不用过多解释了。三、改进1修改侯选池初版的轮动策略咱已经完成了虽然能跑赢基准但是各项回测统计指标看上去还有很大的提升空间革命尚未成功同志仍需努力~还记得轮动策略的两个核心吗一是候选池二是强弱排序要改进可以先从候选池下手。侯选池的重要性不言而喻因为后续所有的动作都是围绕着侯选池展开的就跟从中国足球队随机挑选球员还是从巴西足球队随机挑选球员去点球一样底子好的自然胜率高。比如说咱除了A股之外还考虑国外市场选入代表漂亮国的纳指ETF代码513100还考虑大宗商品市场于是选入黄金ETF代码518880同时剔除原来候选池当中的沪深300ETF和中证500ETF于是整个候选池就变为红利ETF510880创业板ETF159915纳指ETF513100黄金ETF518880。于是乎获取数据部分的代码做出对应的调整。# 510880红利ETF代表价值 # 159915创业板ETF代表成长 # 513100纳指ETF代表外盘 # 518880黄金ETF代表商品 code_list [510880, 159915, 513100, 518880] start_date 20150101 end_date 20250828 df_list [] for code in code_list: print(f正在获取[{code}]行情数据...) # adjust-不复权、qfq-前复权、hfq-后复权 df ak.fund_etf_hist_em(symbolcode, perioddaily, start_datestart_date, end_dateend_date, adjusthfq) df.insert(0, code, code) df_list.append(df) time.sleep(3) print(数据获取完毕) all_df pd.concat(df_list, ignore_indexTrue) data all_df.pivot(index日期, columnscode, values收盘)[code_list] data.index pd.to_datetime(data.index) data data.sort_index() data.head(10)咱按照原始策略的流程重新回测一遍注意在回测结果统计的部分要将原来的沪深300ETF修改成纳指ETF。从结果图中可以看出修改候选池后的轮动策略的回测绩效取得了明显的提升年化收益率从原来的10.01%提升到了14.74%夏普率也从原来的0.66提高到了0.94最大回撤也从原来的47.68%降低到了39.65%。四、改进2修改强弱排序方式候选池改过了咱接下来就是改强弱排序还是保存动量规则不变原来采用的是区间涨跌幅这次改进则打算采用区间走势候选池依旧是上一步改进策略的候选池红利ETF510880创业板ETF159915纳指ETF513100黄金ETF518880。区间走势用什么指标来衡量呢这里借鉴RSRS指标的构建思路用收盘价序列的斜率来表征斜率越大走势越猛越强。同时也引入决定系数R2的概念它是对线性拟合效果好坏的判断指标取值范围一般在0~1之间数值越大表示线性拟合的效果就越好当直线能完美拟合所有数据点时取值为1更详细的说明可以见之前的文章《(续)复现网红阻力支撑指标RSRS手把手教你构建大盘择时策略》。于是强弱规则就修改为“斜率和决定系数的乘积”乘积作为ETF动量强弱的趋势得分得分数值越高就表示动量越强每日都选入强弱得分最高的ETF数据计算部分的代码也做出相应的调整。# 计算强弱得分 def calculate_score(srs, N25): if srs.shape[0] N: return np.nan x np.arange(1, N1) y srs.values / srs.values[0] lr LinearRegression().fit(x.reshape(-1, 1), y) # 斜率 slope lr.coef_[0] # 决定系数R2 r_squared lr.score(x.reshape(-1, 1), y) # 得分 score 10000 * slope * r_squared return score # 斜率计算长度 N 25 # 计算每日涨跌幅和得分 for code in code_list: data[日收益率_code] data[code] / data[code].shift(1) - 1.0 data[得分_code] data[code].rolling(N).apply(lambda x: calculate_score(x, N)) # 去掉缺失值 data data.dropna() data[[得分_v for v in code_list]].head(10)在趋势得分计算当中有两点要补充说明一是在斜率计算时要每次都对收盘价序列“归一化”因为ETF的数值范围不一样会导致即使相同走势斜率也不一样比如说序列y1[1,2,3,4]和y2[2,4,6,8]分别都对x[1,2,3,4]求斜率前者的斜率值是1后者是2其实它们的走势都是一样的涨幅也是一样的。二是计算强弱得分求乘积时乘以了系数10000那个只是为了让数值“好看”一些更像是得分的范畴横截面比较同时都乘以一个任意正实数都不影响排序。之前是按照区间涨幅排序现在是改为强弱得分排序重新回测一遍记得第4步信号生成与回测中的“涨幅”要改为“得分”。从结果图中看出改进策略2在改进策略1的基础上又取得了进一步的提升累计收益率从十年7倍提升到了十年19倍年化收益率从14.74%提升到了21.67%夏普率从0.94提升到了1.33最大回撤也从39.65%下降到了30.31%。为了更直观地对比改进结果咱把原始策略、改进策略1和改进策略2都放在一张图里面进行展示绩效对比一目了然。五、总结和补充说明咱戴上头盔撸起袖子终于吭哧吭哧干完了走通了从零开始构建轮动策略的全流程并且进行了两番改进从十年3倍提升到了十年19倍虽然效果看起来很不错但仍然存在一些不足。一是为了方便回测是按照当日收盘价计算信号并且按收盘价交易的虽然这在实际当中可以在尾盘近似实现但依然会存在差距。二是在回测当中并没有考虑滑点和费率这也是为了方便回测当中的计算。三是候选池的选取这也是所有轮动策略中最大玄学部分的存在像改进策略1一样候选池选对了年化收益直接爆升你可以将本文当中所有候选池的选取都当做是我的主观臆断有后视镜的存在后面存在失效的可能。本文主要还是让大伙儿快速感受和上手轮动策略的构建过程有好的idea了可以快速地进行回测验证思路如果有惊艳的结果产生仍需细细验证有什么到不到的地方望大伙儿多多包涵。