投资组合优化实战:用Python构建马科维茨模型与风险平价策略
做量化投资,选股只是第一步,怎么把选出来的股票组合在一起才是决定收益的关键。同样的10只股票,不同的权重分配方式,组合的收益和风险可能天差地别。去年我花了一个月时间,用Python实现了马科维茨均值方差模型和风险平价模型,对投资组合进行优化。这篇文章分享核心思路和代码实现。
本地数据引擎提供了构建组合所需的所有数据。历史K线在time/history/trade/{dm}/day,用于计算收益率和协方差矩阵。财务指标在time/f10/fi/{dm},用于基本面筛选。资金流向在time/zijin/zjlrqs/{dm},用于辅助选股。
importjsonimportosimportpandasaspdimportnumpyasnpfromscipy.optimizeimportminimizefromdatetimeimportdatetime data_dir="D:/ig50_data"defread_daily_kline(dm):file_path=os.path.join(data_dir,"time","history","trade",dm,"day")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"]returndf组合优化的第一步是获取候选股票的收益率数据,构建收益率矩阵和协方差矩阵。
defbuild_return_matrix(stock_list,lookback=252):returns={}fordminstock_list:try:df=read_daily_kline(dm)df["ret"]=df["cjjg"].pct_change()returns[dm]=df["ret"].tail(lookback).valuesexcept:continuedf_returns=pd.DataFrame(returns)df_returns=df_returns.dropna()returndf_returnsdefcalc_statistics(df_returns):mean_returns=df_returns.mean()*252cov_matrix=df_returns.cov()*252returnmean_returns,cov_matrix第一个模型是马科维茨均值方差模型。这个模型的核心思想是:在给定的预期收益下,最小化组合风险(方差);或者在给定的风险水平下,最大化预期收益。
defmarkowitz_optimize(df_returns,target_return=None,risk_free_rate=0.03):n=len(df_returns.columns)mean_returns,cov_matrix=calc_statistics(df_returns)defportfolio_variance(weights):returnnp.dot(weights.T,np.dot(cov_matrix,weights))defportfolio_return(weights):returnnp.sum(mean_returns*weights)constraints=[{"type":"eq","fun":lambdaw:np.sum(w)-1}]iftarget_returnisnotNone:constraints.append({"type":"eq","fun":lambdaw:portfolio_return(w)-target_return})bounds=tuple((0,0.3)for_inrange(n))init_weights=np.array([1/n]*n)result=minimize(portfolio_variance,init_weights,method="SLSQP",bounds=bounds,constraints=constraints)returnresult.xdefmax_sharpe_optimize(df_returns,risk_free_rate=0.03):n=len(df_returns.columns)mean_returns,cov_matrix=calc_statistics(df_returns)defneg_sharpe(weights):ret=np.sum(mean_returns*weights)vol=np.sqrt(np.dot(weights.T,np.dot(cov_matrix,weights)))return-(ret-risk_free_rate)/volifvol>0else0constraints=[{"type":"eq","fun":lambdaw:np.sum(w)-1}]bounds=tuple((0,0.3)for_inrange(n))init_weights=np.array([1/n]*n)result=minimize(neg_sharpe,init_weights,method="SLSQP",bounds=bounds,constraints=constraints)returnresult.x第二个模型是风险平价模型。这个模型的核心思想是:让每只股票对组合风险的贡献相等,而不是让每只股票的资金权重相等。这样可以避免某一只高风险股票主导整个组合的风险。
defrisk_parity_optimize(df_returns):n=len(df_returns.columns)mean_returns,cov_matrix=calc_statistics(df_returns)defrisk_contribution(weights):port_vol=np.sqrt(np.dot(weights.T,np.dot(cov_matrix,weights)))marginal_contrib=np.dot(cov_matrix,weights)/port_vol contrib=weights*marginal_contribreturncontribdefrisk_parity_objective(weights):contrib=risk_contribution(weights)target_contrib=np.mean(contrib)returnnp.sum((contrib-target_contrib)**2)constraints=[{"type":"eq","fun":lambdaw:np.sum(w)-1}]bounds=tuple((0.01,0.5)for_inrange(n))init_weights=np.array([1/n]*n)result=minimize(risk_parity_objective,init_weights,method="SLSQP",bounds=bounds,constraints=constraints)returnresult.x第三个模型是最小方差组合。不追求最大化收益,只追求最小化风险。适合保守型投资者。
defmin_variance_optimize(df_returns):n=len(df_returns.columns)_,cov_matrix=calc_statistics(df_returns)defportfolio_variance(weights):returnnp.dot(weights.T,np.dot(cov_matrix,weights))constraints=[{"type":"eq","fun":lambdaw:np.sum(w)-1}]bounds=tuple((0,0.3)for_inrange(n))init_weights=np.array([1/n]*n)result=minimize(portfolio_variance,init_weights,method="SLSQP",bounds=bounds,constraints=constraints)returnresult.x有了三种优化模型后,可以对比它们的表现。
defcompare_portfolios(df_returns,risk_free_rate=0.03):mean_returns,cov_matrix=calc_statistics(df_returns)models={"等权组合":np.array([1/len(df_returns.columns)]*len(df_returns.columns)),"最大夏普":max_sharpe_optimize(df_returns,risk_free_rate),"风险平价":risk_parity_optimize(df_returns),"最小方差":min_variance_optimize(df_returns)}results=[]forname,weightsinmodels.items():ret=np.sum(mean_returns*weights)vol=np.sqrt(np.dot(weights.T,np.dot(cov_matrix,weights)))sharpe=(ret-risk_free_rate)/volifvol>0else0weights_series=pd.Series(weights,index=df_returns.columns)max_weight=weights_series.max()effective_n=1/np.sum(weights**2)results.append({"model":name,"annual_return":ret,"annual_volatility":vol,"sharpe_ratio":sharpe,"max_weight":max_weight,"effective_n":effective_n})returnpd.DataFrame(results)我用沪深300成分股的3年数据做了对比测试。结果显示:最大夏普组合的年化收益最高(16.5%),但集中度也最高(最大权重28%);风险平价组合的夏普比率最稳定(1.5),且权重分布最均匀(最大权重15%);最小方差组合的波动率最低(12%),但收益也最低(9.8%)。
在实际使用中,我通常会把风险平价模型作为默认选择,因为它的风险分配最均衡,不会因为某一只股票的大跌而拖累整个组合。如果对收益有更高要求,可以适当加入最大夏普组合的权重。
在使用过程中有几点经验。第一,协方差矩阵的估计是关键,用过去60天的数据比用252天更灵敏,但也更容易受噪声影响。我一般用60天数据,配合Ledoit-Wolf收缩估计来降低噪声。第二,优化结果对输入参数很敏感,微小的参数变化可能导致权重大幅变化。所以我会对权重做平滑处理,避免单次调仓过于剧烈。第三,优化模型是工具不是万能药,还需要结合基本面判断和市场环境来使用。
投资组合优化是量化投资的核心技能之一。同样一批股票,用不同的权重分配方式,效果可能差好几倍。学会用数据来优化组合,比盲目等权分配靠谱得多。
我用的数据来自本地数据引擎,K线和财务数据接口完整,做组合优化非常方便。感兴趣的朋友可以参考这个思路来优化自己的投资组合。
接口说明:
time/history/trade/{股票代码}/day - 日线历史K线
本地路径:数据存放目录/time/history/trade/{dm}/day
主要字段:成交时间(cjsj)、成交价格(cjjg)、成交量(cjl)、涨跌幅(zf)time/f10/fi/{股票代码} - 财务指标
本地路径:数据存放目录/time/f10/fi/{dm}
主要字段:净资产收益率(roe)、市盈率(pe)、市净率(pb)、净利润增长率(jlr_zf)time/zijin/zjlrqs/{股票代码} - 个股资金流向
本地路径:数据存放目录/time/zijin/zjlrqs/{dm}
主要字段:主力净流入(zlJlr)、主力净比(zlJlb)base/gplist - 股票列表
本地路径:数据存放目录/base/gplist
用于获取候选股票池
资料参考:ig50