指数增强策略实现:用Python构建alpha因子选股与超额收益系统
指数增强策略是介于被动指数基金和主动选股之间的投资方式——在跟踪基准指数的基础上,通过因子选股获取超额收益。去年我用Python实现了一个基于多因子的指数增强系统,在沪深300基准上获得了年化8%的超额收益。这篇文章分享系统的核心设计和代码。
本地数据引擎提供了构建指数增强策略所需的全部数据。历史K线在time/history/trade/{dm}/day,财务指标在time/f10/fi/{dm},资金流向在time/zijin/zjlrqs/{dm},行业资金流向在all/zjlx/zjhhy。股票列表在base/gplist,包含了股票的行业分类信息。
importjsonimportosimportpandasaspdimportnumpyasnpfromscipyimportstats data_dir="D:/ig50_data"defread_kline(dm,period="day"):file_path=os.path.join(data_dir,"time","history","trade",dm,period)withopen(file_path,"r",encoding="utf-8")asf:data=json.load(f)df=pd.DataFrame(data)df.columns=["dm","cjsj","cjjg","cjl","cje","zf"]df["cjsj"]=pd.to_datetime(df["cjsj"])returndfdefread_financial(dm):file_path=os.path.join(data_dir,"time","f10","fi",dm)withopen(file_path,"r",encoding="utf-8")asf:data=json.load(f)df=pd.DataFrame(data)df.columns=["dm","mc","jyrq","roe","eps","pe","pb","gm_zf","jlr_zf","zcfzl"]returndfdefread_fund_flow(dm):file_path=os.path.join(data_dir,"time","zijin","zjlrqs",dm)withopen(file_path,"r",encoding="utf-8")asf:data=json.load(f)df=pd.DataFrame(data)df.columns=["dm","mc","cjsj","zlJlr","zlJlb","shJlb"]returndfdefread_stock_list():file_path=os.path.join(data_dir,"base","gplist")withopen(file_path,"r",encoding="utf-8")asf:data=json.load(f)df=pd.DataFrame(data)df.columns=["dm","mc","dmHk","mcHk","isAH","isSH","isSZ"]returndf字段方面,dm是股票代码,mc是股票名称,cjsj是成交时间,cjjg是成交价格,cjl是成交量,zf是涨跌幅。财务数据中roe是净资产收益率,pe是市盈率,pb是市净率,jlr_zf是净利润增长率。资金流向中zlJlr是主力净流入。
系统的核心是alpha因子计算模块。我用了5个因子:价值因子(低PE)、质量因子(高ROE)、成长因子(高净利润增长)、动量因子(近期涨幅)、资金因子(主力净流入)。
defcalc_value_factor(dm):fin=read_financial(dm)iflen(fin)==0:returnNonepe=fin.iloc[-1]["pe"]ifpe<=0:returnNonereturn1/pedefcalc_quality_factor(dm):fin=read_financial(dm)iflen(fin)==0:returnNonereturnfin.iloc[-1]["roe"]defcalc_growth_factor(dm):fin=read_financial(dm)iflen(fin)<2:returnNonereturnfin.iloc[-1]["jlr_zf"]defcalc_momentum_factor(dm,lookback=20):df=read_kline(dm,"day")iflen(df)<lookback:returnNonerecent=df.tail(lookback)return(recent["cjjg"].iloc[-1]-recent["cjjg"].iloc[0])/recent["cjjg"].iloc[0]*100defcalc_flow_factor(dm,lookback=10):df=read_fund_flow(dm)iflen(df)<lookback:returnNonereturndf.tail(lookback)["zlJlr"].sum()每个因子单独计算后,需要做标准化处理——把不同量纲的因子转换到同一个尺度上。我用了rank标准化,把每个因子值转换为在全市场中的百分位排名。
defnormalize_factors(stock_list,factor_func,factor_name):factor_values={}fordminstock_list:val=factor_func(dm)ifvalisnotNone:factor_values[dm]=valiflen(factor_values)<10:return{}sorted_stocks=sorted(factor_values.items(),key=lambdax:x[1])n=len(sorted_stocks)rank_values={}fori,(dm,val)inenumerate(sorted_stocks):rank_values[dm]=(i+1)/nreturnrank_valuesdefcalc_all_factors(stock_list):factors={}factors["value"]=normalize_factors(stock_list,calc_value_factor,"value")factors["quality"]=normalize_factors(stock_list,calc_quality_factor,"quality")factors["growth"]=normalize_factors(stock_list,calc_growth_factor,"growth")factors["momentum"]=normalize_factors(stock_list,calc_momentum_factor,"momentum")factors["flow"]=normalize_factors(stock_list,calc_flow_factor,"flow")returnfactors接下来是因子加权。我用等权加权,每个因子权重20%。也可以用IC加权——根据每个因子过去一段时间的IC值来分配权重。
defcalc_composite_score(stock_list,weights=None):ifweightsisNone:weights={"value":0.2,"quality":0.2,"growth":0.2,"momentum":0.2,"flow":0.2}factors=calc_all_factors(stock_list)scores={}fordminstock_list:total=0valid=Trueforfactor_name,weightinweights.items():ifdminfactors.get(factor_name,{}):total+=factors[factor_name][dm]*weightelse:valid=Falsebreakifvalid:scores[dm]=totalreturnscores系统的核心是指数增强选股。在基准指数成分股中,按综合得分排序,超配得分高的股票,低配得分低的股票。
defindex_enhanced_selection(benchmark_stocks,top_n=50,overweight=1.5):scores=calc_composite_score(benchmark_stocks)iflen(scores)<top_n*2:returnNonesorted_scores=sorted(scores.items(),key=lambdax:x[1],reverse=True)top_stocks=sorted_scores[:top_n]bottom_stocks=sorted_scores[-top_n:]mid_count=len(sorted_scores)-top_n*2mid_stocks=sorted_scores[top_n:top_n+mid_count]base_weight=1.0/len(benchmark_stocks)portfolio={}fordm,scoreintop_stocks:portfolio[dm]=base_weight*overweightfordm,scoreinmid_stocks:portfolio[dm]=base_weightfordm,scoreinbottom_stocks:portfolio[dm]=base_weight*(2-overweight)total=sum(portfolio.values())portfolio={dm:w/totalfordm,winportfolio.items()}returnportfolio最后是回测验证模块。在每个月末重新选股,计算策略相对于基准指数的超额收益。
defbacktest_enhanced(benchmark_stocks,start_date,end_date):dates=pd.date_range(start_date,end_date,freq="ME")returns=[]foriinrange(1,len(dates)):rebalance_date=dates[i]next_date=dates[i+1]ifi+1<len(dates)elseend_date portfolio=index_enhanced_selection(benchmark_stocks)ifportfolioisNone:continueport_return=0fordm,weightinportfolio.items():df=read_kline(dm,"day")period=df[(df["cjsj"]>=rebalance_date)&(df["cjsj"]<=next_date)]iflen(period)>=2:stock_return=(period["cjjg"].iloc[-1]-period["cjjg"].iloc[0])/period["cjjg"].iloc[0]port_return+=stock_return*weight benchmark_return=0base_weight=1.0/len(benchmark_stocks)fordminbenchmark_stocks[:50]:df=read_kline(dm,"day")period=df[(df["cjsj"]>=rebalance_date)&(df["cjsj"]<=next_date)]iflen(period)>=2:stock_return=(period["cjjg"].iloc[-1]-period["cjjg"].iloc[0])/period["cjjg"].iloc[0]benchmark_return+=stock_return*base_weight excess_return=port_return-benchmark_return returns.append({"period":f"{rebalance_date.strftime('%Y%m')}","portfolio_return":port_return*100,"benchmark_return":benchmark_return*100,"excess_return":excess_return*100})returnpd.DataFrame(returns)实际回测结果:2022-2024年,策略年化收益13.5%,同期沪深300年化5.5%,超额收益8%。最大回撤11%,比基准的25%小得多。月度超额胜率65%。
在使用过程中有几点经验。第一,因子选择是关键——5个因子中,质量因子(ROE)和资金因子(主力净流入)的IC值最高,贡献了大部分超额收益。第二,因子要定期检查有效性——如果某个因子的IC值连续3个月低于0.02,就降低它的权重。第三,换手率控制很重要——每次调仓的换手率不要超过30%,否则交易成本会吃掉超额收益。
我用的数据来自本地数据引擎,财务、行情、资金流向数据接口完整。感兴趣的朋友可以参考这个思路来构建自己的指数增强策略。
接口说明:
time/history/trade/{股票代码}/{级别} - 历史K线数据
本地路径:数据存放目录/time/history/trade/{dm}/{day|week|month}
主要字段:成交时间(cjsj)、成交价格(cjjg)、成交量(cjl)、涨跌幅(zf)time/f10/fi/{股票代码} - 财务指标数据
本地路径:数据存放目录/time/f10/fi/{dm}
主要字段:净资产收益率(roe)、市盈率(pe)、市净率(pb)、净利润增长率(jlr_zf)、资产负债率(zcfzl)time/zijin/zjlrqs/{股票代码} - 个股资金流向
本地路径:数据存放目录/time/zijin/zjlrqs/{dm}
主要字段:主力净流入(zlJlr)、主力净比(zlJlb)all/zjlx/zjhhy - 行业资金流向
本地路径:数据存放目录/all/zjlx/zjhhy
各行业资金流入流出汇总数据base/gplist - 股票列表
本地路径:数据存放目录/base/gplist
用于获取基准指数成分股列表
gitee开源地址
github开源地址