指数成分与行业轮动用Python构建动态行业配置策略 IG50免费开源股票数据API接口
指数成分与行业轮动用Python构建动态行业配置策略大概从2022年开始我逐渐意识到一个问题在A股市场选对行业往往比选对个股更重要。同样是在2023年买了AI行业的和买了消费行业的收益可能差了好几倍。从那以后我就开始研究行业轮动策略试图通过量化的方法捕捉行业之间的资金流向和动量切换。这篇文章就来聊聊我是如何用指数成分数据和行业资金流向数据构建一套动态行业配置策略的。行业轮动的核心逻辑其实很简单市场上的资金不是均匀分布在各个行业的而是会在不同时间段集中流向某些行业。如果我们能及时发现这些资金流向的变化在资金流入的行业重仓在资金流出的行业减仓就能获得超越市场的超额收益。我这套策略的数据来源主要有五块第一块是指数成分列表用来获取各个行业指数的代码第二块是指数成分股也就是每个具体指数下面包含哪些股票第三块是指数/行业/概念树用来理清行业之间的层级关系第四块是证监会行业资金流向看每个行业当天的资金流入流出情况第五块是行业资金路线图统计近3、5、10天的行业资金流向趋势。先从数据读取开始。指数成分列表在base/zscf具体指数的成分股在time/indextree/{指数代码}行业树在base/it行业资金流向在all/zjlx/zjhhy行业资金路线图在all/zjlx/zjhhyzjlx。importjsonimportosimportpandasaspdimportnumpyasnpfromcollectionsimportdefaultdictimportdatetime data_dirD:/stock_datadefread_zscf():file_pathos.path.join(data_dir,base,zscf)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[dm,mc,jys]returndfdefread_indextree(zs_dm):file_pathos.path.join(data_dir,time,indextree,zs_dm)ifnotos.path.exists(file_path):returnNonewithopen(file_path,r,encodingutf-8)asf:datajson.load(f)ifnotdata:returnNonedfpd.DataFrame(data)df.columns[dm,mc,jys]returndfdefread_it():file_pathos.path.join(data_dir,base,it)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[mc,dm,yjFl,ejFl,cj,fjdm,fjMc,isLeaf]returndfdefread_zjhhy():file_pathos.path.join(data_dir,all,zjlx,zjhhy)ifnotos.path.exists(file_path):returnNonewithopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[gxSj,hyMc,hyDm,jj,zdf,lrZj,lcZj,jlr,jlrLv,lzgMc,lzgDm,lzgJlrLv]returndfdefread_zjhhyzjlx():file_pathos.path.join(data_dir,all,zjlx,zjhhyzjlx)ifnotos.path.exists(file_path):returnNonewithopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[gxSj,hyMc,hyDm,jsrZdf,jsrJlr,jsrJlrLv,jwrZdf,jwrJlr,jwrJlrLv,jsrZdf2,jsrJlr2,jsrJlrLv2]returndf字段名还是用了简写dm是代码mc是名称jys是交易所。行业树里yjFl是一级分类ejFl是二级分类cj是层级fjdm是父节点代码fjMc是父节点名称isLeaf是是否为叶子节点。资金流向相关的gxSj是更新时间hyMc是行业名称hyDm是行业代码jj是均价zdf是涨跌幅lrZj是流入资金lcZj是流出资金jlr是净流入jlrLv是净流入率lzg是领涨股。资金路线图里jsr代表近三日jwr代表近五日后面的jsr2代表近十日。拿到数据之后我会先从行业树里筛选出证监会行业分类的叶子节点。因为行业树里包含了指数、概念、申万行业等多种分类我只需要证监会行业这一块。这样就得到了所有的行业列表大概有几十个一级行业。defget_zjh_industries(df_it):df_filtereddf_it[(df_it[yjFl]0)(df_it[ejFl]5)(df_it[isLeaf]1)].copy()returndf_filtered[[dm,mc]]有了行业列表之后就该分析资金流向了。行业资金流向是整个策略的核心我主要看三个维度当日的净流入率、近几日的净流入趋势、涨跌幅和资金流向的配合度。当日净流入率反映的是当天资金对这个行业的态度。净流入率越高说明资金越看好这个行业。但光看一天的数据不够因为单日的资金流入可能是偶然事件。所以还要结合近3日、5日、10日的资金流向趋势来看如果一个行业连续多日都是资金净流入而且净流入率在逐步放大那信号就很强了。涨跌幅和资金流向的配合度也很重要。如果一个行业涨了很多但资金是净流出的说明上涨过程中资金在撤退可能是诱多。反过来如果行业下跌但资金是净流入的说明有人在逆势吸筹可能是布局机会。defanalyze_industry_capital(df_zjhhy,df_zjlx):ifdf_zjhhyisNoneorlen(df_zjhhy)0:returnNonedf_mergeddf_zjhhy.merge(df_zjlx[[hyDm,jsrZdf,jsrJlr,jsrJlrLv,jwrZdf,jwrJlr,jwrJlrLv,jsrZdf2,jsrJlr2,jsrJlrLv2]],onhyDm,howleft)df_merged[lr_trend]np.where((df_merged[jlrLv]0)(df_merged[jsrJlrLv]0)(df_merged[jwrJlrLv]0),2,np.where((df_merged[jlrLv]0)((df_merged[jsrJlrLv]0)|(df_merged[jwrJlrLv]0)),1,np.where(df_merged[jlrLv]0,-1,0)))df_merged[price_capital_match]np.where((df_merged[zdf]0)(df_merged[jlrLv]0),2,np.where((df_merged[zdf]0)(df_merged[jlrLv]0),1,np.where((df_merged[zdf]0)(df_merged[jlrLv]0),-1,0)))df_merged[total_score](df_merged[jlrLv].rank(pctTrue)*0.3df_merged[jsrJlrLv].rank(pctTrue)*0.25df_merged[jwrJlrLv].rank(pctTrue)*0.2df_merged[lr_trend]*0.15df_merged[price_capital_match]*0.1)df_sorteddf_merged.sort_values(total_score,ascendingFalse)print(行业资金流向排名TOP10)foridx,rowindf_sorted.head(10).iterrows():print({} | 当日净流入率{:.2f}% | 近3日净流入率{:.2f}% | 近5日净流入率{:.2f}% | 综合得分{:.3f}.format(row[hyMc],row[jlrLv],row[jsrJlrLv],row[jwrJlrLv],row[total_score]))print(\n行业资金流向排名后10)foridx,rowindf_sorted.tail(10).iterrows():print({} | 当日净流入率{:.2f}% | 近3日净流入率{:.2f}% | 近5日净流入率{:.2f}% | 综合得分{:.3f}.format(row[hyMc],row[jlrLv],row[jsrJlrLv],row[jwrJlrLv],row[total_score]))returndf_sorted行业资金流向分析完之后我会计算行业动量因子。行业动量的逻辑是涨得多的行业接下来还会继续涨这就是动量效应。当然动量不是万能的也有反转的时候但在A股市场行业动量在大多数时候是有效的。我计算动量因子的时候会看近5日、近10日、近20日的行业涨跌幅然后给不同的权重。近期的涨跌幅权重高一些远期的权重低一些。最后把多个周期的动量合成一个综合动量因子。defcalc_industry_momentum(df_zjlx):ifdf_zjlxisNoneorlen(df_zjlx)0:returnNonedfdf_zjlx.copy()df[momentum_5d]df[jsrZdf]df[momentum_10d]df[jwrZdf]df[momentum_20d]df[jsrZdf2]df[momentum_score](df[momentum_5d].rank(pctTrue)*0.5df[momentum_10d].rank(pctTrue)*0.3df[momentum_20d].rank(pctTrue)*0.2)returndf[[hyDm,hyMc,momentum_5d,momentum_10d,momentum_20d,momentum_score]]有了资金流向得分和动量得分之后就可以合成最终的行业配置得分了。我的做法是资金流向得分占60%的权重动量得分占40%的权重。因为资金流向更能反映机构的真实意图而动量更多是价格趋势的滞后反映。defcalc_final_industry_score(df_capital,df_momentum):df_mergeddf_capital.merge(df_momentum[[hyDm,momentum_score]],onhyDm,howleft)df_merged[final_score](df_merged[total_score]*0.6df_merged[momentum_score]*0.4)df_sorteddf_merged.sort_values(final_score,ascendingFalse)returndf_sorted接下来就是具体的行业配置了。我一般会选综合得分排名前5的行业作为重点配置行业每个行业配置15%-25%的仓位剩下的仓位留作灵活配置或者现金。排名后5的行业坚决回避即使估值再低也不碰因为资金不认可的行业很难有行情。但光选行业还不够还要落实到具体的个股上。这时候就需要用到指数成分股数据了。每个行业都有对应的行业指数我会先找到排名靠前的行业对应的指数代码然后读取这个指数的成分股列表再从成分股里面选股。选股的时候我会在行业成分股里再用基本面因子选一遍比如选ROE高的、估值合理的、机构持仓比例高的。这样相当于做了两层筛选先选行业再选个股双重保险。defselect_stocks_from_industry(hy_dm_list,df_it,df_gp,df_fi_cache,top_per_industry5):selected_stocks[]forhy_dminhy_dm_list:df_stocksread_indextree(hy_dm)ifdf_stocksisNoneorlen(df_stocks)0:continuehy_infodf_it[df_it[dm]hy_dm]hy_mchy_info[mc].values[0]iflen(hy_info)0else未知df_mergeddf_stocks.merge(df_gp[[dm,mc,isSt]],on[dm,mc],howleft)df_mergeddf_merged[df_merged[isSt]0]stock_scores[]for_,rowindf_merged.iterrows():dmrow[dm]ifdmindf_fi_cacheanddf_fi_cache[dm]isnotNone:df_fidf_fi_cache[dm]roedf_fi[加权净资产收益率(%)].iloc[0]iflen(df_fi)0andpd.notna(df_fi[加权净资产收益率(%)].iloc[0])else0gross_margindf_fi[销售毛利率(%)].iloc[0]iflen(df_fi)0andpd.notna(df_fi[销售毛利率(%)].iloc[0])else0scoreroe*0.6gross_margin*0.4else:score0stock_scores.append({dm:dm,mc:row[mc],score:score,hyMc:hy_mc})df_scorespd.DataFrame(stock_scores)df_scoresdf_scores.sort_values(score,ascendingFalse)top_stocksdf_scores.head(top_per_industry)selected_stocks.extend(top_stocks.to_dict(records))df_resultpd.DataFrame(selected_stocks)print(行业配置选股结果)forhy_mc,groupindf_result.groupby(hyMc):print(\n【{}】.format(hy_mc))for_,rowingroup.iterrows():print( {}({}) - 基本面得分{:.2f}.format(row[mc],row[dm],row[score]))returndf_result最后一步就是回测验证了。任何策略如果不经过回测都只是纸上谈兵。我的回测方法是用历史的行业资金流向和动量数据每个月末进行一次行业调仓然后看下个月的行业收益表现。如果策略选出来的前5个行业的平均收益持续跑赢所有行业的平均收益那就说明策略是有效的。我还会看另一个指标行业换手率。也就是策略每次调仓的时候有多少个行业被换掉了。如果换手率太高说明策略不稳定今天选出来的行业下个月就全部换掉了这样交易成本也高。理想的换手率应该在30%-50%之间也就是说每次调仓只换掉1-2个行业。defbacktest_strategy(df_zjlx_hist,months12):ifdf_zjlx_histisNoneorlen(df_zjlx_hist)0:returnNoneresults[]foriinrange(months):df_currentdf_zjlx_hist.iloc[i*20:(i1)*20]if(i1)*20len(df_zjlx_hist)elsedf_zjlx_hist.iloc[i*20:]df_nextdf_zjlx_hist.iloc[(i1)*20:(i2)*20]if(i2)*20len(df_zjlx_hist)elsedf_zjlx_hist.iloc[(i1)*20:]iflen(df_current)0orlen(df_next)0:breakdf_current_aggdf_current.groupby(hyDm).agg({hyMc:first,jwrJlrLv:last,jwrZdf:last}).reset_index()df_current_agg[score](df_current_agg[jwrJlrLv].rank(pctTrue)*0.6df_current_agg[jwrZdf].rank(pctTrue)*0.4)df_sorteddf_current_agg.sort_values(score,ascendingFalse)top5df_sorted.head(5)bottom5df_sorted.tail(5)df_next_aggdf_next.groupby(hyDm)[jwrZdf].last().reset_index()top5_returndf_next_agg[df_next_agg[hyDm].isin(top5[hyDm])][jwrZdf].mean()bottom5_returndf_next_agg[df_next_agg[hyDm].isin(bottom5[hyDm])][jwrZdf].mean()all_returndf_next_agg[jwrZdf].mean()results.append({period:i1,top5_return:top5_return,bottom5_return:bottom5_return,all_return:all_return,excess_return:top5_return-all_return})df_resultspd.DataFrame(results)print(策略回测结果)print(前5行业平均收益{:.2f}%.format(df_results[top5_return].mean()))print(后5行业平均收益{:.2f}%.format(df_results[bottom5_return].mean()))print(全行业平均收益{:.2f}%.format(df_results[all_return].mean()))print(超额收益{:.2f}%.format(df_results[excess_return].mean()))returndf_results实际使用这套策略的过程中我也总结了一些经验教训。首先行业轮动策略适合中等周期1-3个月的配置不适合做超短线因为行业切换没那么快。其次不要过度依赖单一因子资金流向和动量结合起来看效果更好。第三要注意行业的估值水平如果一个行业已经涨了很多估值处于历史高位即使资金流向和动量都很好也要谨慎追高。还有一个很重要的点就是要关注政策面。A股是政策市很多行业的涨跌都和政策密切相关。比如2023年的AI行情和政策对数字经济的支持密不可分再比如医药行业的集采政策对整个板块的影响非常大。所以我在行业配置的时候会把政策面作为一个重要的参考因素政策支持的行业可以适当超配政策压制的行业要回避。总的来说动态行业配置策略让我的投资组合在不同市场环境下都能保持相对稳健的表现。在行业普涨的时候我能抓住涨幅最大的几个行业在行业分化的时候我能及时从弱势行业切换到强势行业在市场整体下跌的时候通过降低仓位和配置防御性行业也能减少亏损。我用的数据来源是ig50的本地数据接口行业资金流向数据每天15:30更新指数成分每周六凌晨3点更新数据质量和时效性都很不错。有需要的朋友可以自行了解。接口说明base/zscf - 指数成分本地路径数据存放目录/base/zscf主要字段指数代码(dm)、股票名称(mc)、交易所(jys)time/indextree/{指数代码} - 指数成分股本地路径数据存放目录/time/indextree/{指数、行业、概念代码}主要字段代码(dm)、名称(mc)、交易所(jys)base/it - 指数、行业、概念树本地路径数据存放目录/base/it主要字段名称(mc)、代码(dm)、一级分类(yjFl)、二级分类(ejFl)、层级(cj)、父节点代码(fjdm)、父节点名称(fjMc)、是否为叶子节点(isLeaf)all/zjlx/zjhhy - 证监会行业资金流向本地路径数据存放目录/all/zjlx/zjhhy主要字段服务器更新时间(gxSj)、行业名称(hyMc)、行业代码(hyDm)、均价(jj)、涨跌幅(zdf)、流入资金(lrZj)、流出资金(lcZj)、净流入(jlr)、净流入率(jlrLv)、领涨股名称(lzgMc)、领涨股代码(lzgDm)、领涨股净流入率(lzgJlrLv)all/zjlx/zjhhyzjlx - 证监会行业资金路线图本地路径数据存放目录/all/zjlx/zjhhyzjlx主要字段服务器更新时间(gxSj)、行业名(hyMc)、行业代码(hyDm)、近三日涨跌幅(jsrZdf)、近三日净流入(jsrJlr)、近三日净流入率(jsrJlrLv)、近五日涨跌幅(jwrZdf)、近五日净流入(jwrJlr)、近五日净流入率(jwrJlrLv)、近十日涨跌幅(jsrZdf2)、近十日净流入(jsrJlr2)、近十日净流入率(jsrJlrLv2)gitee开源地址github开源地址