投资组合优化实战:用Python构建马科维茨模型与风险平价策略 投资组合优化实战用Python构建马科维茨模型与风险平价策略做量化投资选股只是第一步怎么把选出来的股票组合在一起才是决定收益的关键。同样的10只股票不同的权重分配方式组合的收益和风险可能天差地别。去年我花了一个月时间用Python实现了马科维茨均值方差模型和风险平价模型对投资组合进行优化。这篇文章分享核心思路和代码实现。本地数据引擎提供了构建组合所需的所有数据。历史K线在time/history/trade/{dm}/day用于计算收益率和协方差矩阵。财务指标在time/f10/fi/{dm}用于基本面筛选。资金流向在time/zijin/zjlrqs/{dm}用于辅助选股。importjsonimportosimportpandasaspdimportnumpyasnpfromscipy.optimizeimportminimizefromdatetimeimportdatetime data_dirD:/ig50_datadefread_daily_kline(dm):file_pathos.path.join(data_dir,time,history,trade,dm,day)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[dm,cjsj,cjjg,cjl,cje,zf]df[cjsj]pd.to_datetime(df[cjsj])returndfdefread_financial(dm):file_pathos.path.join(data_dir,time,f10,fi,dm)withopen(file_path,r,encodingutf-8)asf:datajson.load(f)dfpd.DataFrame(data)df.columns[dm,mc,jyrq,roe,eps,pe,pb,gm_zf,jlr_zf,zcfzl]returndf组合优化的第一步是获取候选股票的收益率数据构建收益率矩阵和协方差矩阵。defbuild_return_matrix(stock_list,lookback252):returns{}fordminstock_list:try:dfread_daily_kline(dm)df[ret]df[cjjg].pct_change()returns[dm]df[ret].tail(lookback).valuesexcept:continuedf_returnspd.DataFrame(returns)df_returnsdf_returns.dropna()returndf_returnsdefcalc_statistics(df_returns):mean_returnsdf_returns.mean()*252cov_matrixdf_returns.cov()*252returnmean_returns,cov_matrix第一个模型是马科维茨均值方差模型。这个模型的核心思想是在给定的预期收益下最小化组合风险方差或者在给定的风险水平下最大化预期收益。defmarkowitz_optimize(df_returns,target_returnNone,risk_free_rate0.03):nlen(df_returns.columns)mean_returns,cov_matrixcalc_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})boundstuple((0,0.3)for_inrange(n))init_weightsnp.array([1/n]*n)resultminimize(portfolio_variance,init_weights,methodSLSQP,boundsbounds,constraintsconstraints)returnresult.xdefmax_sharpe_optimize(df_returns,risk_free_rate0.03):nlen(df_returns.columns)mean_returns,cov_matrixcalc_statistics(df_returns)defneg_sharpe(weights):retnp.sum(mean_returns*weights)volnp.sqrt(np.dot(weights.T,np.dot(cov_matrix,weights)))return-(ret-risk_free_rate)/volifvol0else0constraints[{type:eq,fun:lambdaw:np.sum(w)-1}]boundstuple((0,0.3)for_inrange(n))init_weightsnp.array([1/n]*n)resultminimize(neg_sharpe,init_weights,methodSLSQP,boundsbounds,constraintsconstraints)returnresult.x第二个模型是风险平价模型。这个模型的核心思想是让每只股票对组合风险的贡献相等而不是让每只股票的资金权重相等。这样可以避免某一只高风险股票主导整个组合的风险。defrisk_parity_optimize(df_returns):nlen(df_returns.columns)mean_returns,cov_matrixcalc_statistics(df_returns)defrisk_contribution(weights):port_volnp.sqrt(np.dot(weights.T,np.dot(cov_matrix,weights)))marginal_contribnp.dot(cov_matrix,weights)/port_vol contribweights*marginal_contribreturncontribdefrisk_parity_objective(weights):contribrisk_contribution(weights)target_contribnp.mean(contrib)returnnp.sum((contrib-target_contrib)**2)constraints[{type:eq,fun:lambdaw:np.sum(w)-1}]boundstuple((0.01,0.5)for_inrange(n))init_weightsnp.array([1/n]*n)resultminimize(risk_parity_objective,init_weights,methodSLSQP,boundsbounds,constraintsconstraints)returnresult.x第三个模型是最小方差组合。不追求最大化收益只追求最小化风险。适合保守型投资者。defmin_variance_optimize(df_returns):nlen(df_returns.columns)_,cov_matrixcalc_statistics(df_returns)defportfolio_variance(weights):returnnp.dot(weights.T,np.dot(cov_matrix,weights))constraints[{type:eq,fun:lambdaw:np.sum(w)-1}]boundstuple((0,0.3)for_inrange(n))init_weightsnp.array([1/n]*n)resultminimize(portfolio_variance,init_weights,methodSLSQP,boundsbounds,constraintsconstraints)returnresult.x有了三种优化模型后可以对比它们的表现。defcompare_portfolios(df_returns,risk_free_rate0.03):mean_returns,cov_matrixcalc_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():retnp.sum(mean_returns*weights)volnp.sqrt(np.dot(weights.T,np.dot(cov_matrix,weights)))sharpe(ret-risk_free_rate)/volifvol0else0weights_seriespd.Series(weights,indexdf_returns.columns)max_weightweights_series.max()effective_n1/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