0 简介
昨天实习摸鱼的时候开始看《Quantitative Equity Portfolio Management:Modern Techniques and Applications》,第一章讲到了经典的几个组合优化模型,于是乎干脆顺便在聚宽的策略环境里实现了一下,并选取了一个多因子选股进行了对比测试,初步证明了经典组合优化的一定有效性。个人觉得在实际交易场景中使用优化模型的主要难点在于如何较好的估计不同资产间的协方差矩阵以及各自的期望收益率。实证分析显示带有依赖资产预期收益率约束的优化问题,强行使用过去某段时间区间内的收益率替代预期收益率会产生严重的估计偏误从而严重影响优化结果。我觉得在实际使用这类模型以下两个问题需要被考虑:
1 如何有效的估计约束依赖的指标,如资产的预期收益率向量,资产的 向量,资产的协方差矩阵等等。关于这一块应该也已经有不少的工作,我了解比较少,但所知道的比如BL模型通过将投资者观点和先验收益率结合产生后验收益率输入均值方差优化模型,或者通过Compression Matrix的方法优化协方差矩阵都能较好的改进通过历史波动率来估计协方差矩阵而带来的种种弊端:比如,随着资产数目二次增加的估计时间复杂度,优化结果对权重的初始化高度敏感等等,这里不再展开。
2 如何考虑资产之间的序列相关性,通过简单的实证分析,对于大类资产间的配比如权益国债黄金,资产之间的相关性较低,我们更希望协方差是一个对角矩阵,通过一些诸如masking或者shrinkage的trick可以较好的实现,而如果是纯股票组合,有时候考虑序列相关性的优化结果更好(我未做深入研究)。回到正题,我大致实现了以下几种常见优化。首先设置我们的原始股票列表,以及定义一个函数返回该组资产过去 天的日收盘价序列。
import numpy as np
import pandas as pd
import scipy.optimize as sco
universe = get_index_stocks('000300.XSHG')
stock_list = universe[0:5]#为了方便演示这里只选取五只股票构建投资组合
'''给定股票列表得到过去245个交易日的收盘价面板'''
def get_closedata(stock_list):
close_dict = {}
for i in range(len(stock_list)):
code_name = stock_list[i]
close_seq = attribute_history(stock_list[i],245,'1d', ('close'))
close_seq = list(close_seq['close'])
close_dict[code_name] = close_seq
#transofrm to dataframe
df_stock = pd.DataFrame(close_dict,columns = list(close_dict.keys()))
return df_stock #255*N的DataFrame,每列对应一只股票过去245个交易日股价
1 以最大化期望收益率建立优化问题
优化问题:
代码:
'''全局最大化期望收益率组合'''
def cal_expret(weights:list,df_stock):
risk_free_rate = 0.03
if len(weights) != df_stock.shape[1]:
raise Exception("the weights should be same size as stock list")
weights = np.array(weights/np.sum(weights))
returns_daily = np.log(df_stock/df_stock.shift(1)).iloc[1:,:]
expected_return = np.sum(returns_daily.mean()*weights)*244-risk_free_rate
return expected_return
def max_expret(weight0:list,df_stock):
stock_num = df_stock.shape[1]
w_min = 1/(np.power(stock_num,1.5))
w_max = 0.6
df = df_stock
def maxexpret(weights):
return cal_expret(weights,df)*(-1) #minimize
cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1})
bnds = tuple((w_min,w_max) for x in range(stock_num))
optimize = sco.minimize(maxexpret, weight0 , method = 'SLSQP', bounds = bnds,constraints = cons)
optim_weight = list(optimize['x'].round(4))
return optim_weight
注意我的代码里未使用让权重[0,1]的约束,而是让权重约束在:
其中为组合的资产数目,在本例中
。
2 全局最小方差
优化问题:
代码:
'''全局最小方差组合'''
def cal_variance(weights:list,df_stock):
if len(weights) != df_stock.shape[1]:
raise Exception("the weights should be same size as stock list")
weights = np.array(weights/np.sum(weights)) #normalizing
#derive the portfolio volatility
returns_daily = (np.log(df_stock/df_stock.shift(1))).iloc[1:,:]
Sigma = returns_daily.cov()*244
Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights)))
return Portfolio_vol
def min_variance(weight0:list,df_stock):
stock_num = df_stock.shape[1]
w_min = 1/(np.power(stock_num,1.5))
w_max = 0.6
df = df_stock
def minvariance(weights):
return cal_variance(weights,df) #minimize
cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
bnds = tuple((w_min,w_max) for x in range(stock_num)) #w_i的上下限约束
optimize = sco.minimize(minvariance,weight0,method = 'SLSQP',bounds = bnds, constraints = cons)
optim_weight = optimize['x'].round(4)
return optim_weight
3 最大化夏普率
优化问题:
代码:
'''最大化夏普率组合'''
def cal_sharpe(weights:list,df_stock):
risk_free_rate = 0.03
if len(weights) != df_stock.shape[1]:
raise Exception("the weights should be same size as stock list")
weights = np.array(weights)/np.sum(weights)
returns_daily = np.log(df_stock/df_stock.shift(1)).iloc[1:,:]
expected_return = np.sum(returns_daily.mean()*weights)*244-risk_free_rate #annualized excess return
Sigma = returns_daily.cov()*244
Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights)))
Sharpe_ratio = expected_return/Portfolio_vol
return Sharpe_ratio
def max_sharpe(weight0:list,df_stock):
stock_num = df_stock.shape[1]
w_min = 1/(np.power(stock_num,1.5))
w_max = 0.5
df = df_stock
def maxsharpe(weights):
return cal_sharpe(weights,df)*(-1)
cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
bnds = tuple((w_min,w_max) for x in range(stock_num)) #w_i的上下限约束
optimize = sco.minimize(maxsharpe,weight0,method = 'SLSQP',bounds = bnds, constraints = cons)
optim_weight = optimize['x'].round(4)
return optim_weight
4 最大化效用函数(Utility Function)
效用函数期望尽可能的在最大化期望收益率和最小组合波动率之间取得一个平衡,带有一个penalty
代码:
'''效用函数最优化: 同时考虑期望收益率和组合风险'''
def cal_Utility(weights:list,df_stock,lambda_): #lambda 表示风险厌恶系数
if len(weights) != df_stock.shape[1]:
raise Exception("the weights should be same size as stock list")
num = len(weights)
weights = np.array(weights/np.sum(weights))
#first part
returns_daily = np.log(df_stock/df_stock.shift(1)).iloc[1:,:]
first_part = np.sum(returns_daily.mean()*weights)*244
#second part
Sigma = returns_daily.cov()*244
Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights)))
second_part = 0.5*lambda_*Portfolio_vol
return first_part-second_part #w.T*return - 0.5*lambda*sigma
def max_utility(weight0:list,df_stock,lambda_):
stock_num = df_stock.shape[1]
w_min = 1/(np.power(stock_num,1.5))
w_max = 1
df = df_stock
def maxutility(weights):
return cal_Utility(weights,df,lambda_)*(-1)
cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
bnds = tuple((w_min,w_max) for x in range(stock_num)) #w_i的上下限约束
optimize = sco.minimize(maxutility,weight0,method = 'SLSQP',bounds = bnds, constraints = cons)
optim_weight = optimize['x'].round(4)
return optim_weight
5 风险平价
考虑股票的收益率序列相关性,使每只股票的资产的风险贡献度相同。有不考虑序列相关性的版本读者可自己完成。使用向量求导的法则将组合风险对求导得到边际贡献度向量,再和权重向量做point-wise product即可得到风险贡献向量:
我们希望每一个元素尽可能相等,我借鉴了MSE损失函数的构造逻辑自己改进了损失函数:
代码:
'''风险平价:考虑不同个股之间的序列相关性'''
def RiskParityLoss(weights:list,df_stock):
if len(weights) != df_stock.shape[1]:
raise Exception("the weights should be same size as stock list")
num = len(weights)
weights = np.array(weights/np.sum(weights)) #normalizing
#derive covariance matrix
returns_daily = (np.log(df_stock/df_stock.shift(1))).iloc[1:,:]
Sigma = returns_daily.cov()*244
Portfolio_vol = np.sqrt(np.dot(weights.T,np.dot(Sigma,weights))) #组合波动率
MRC = np.dot(Sigma,weights)/Portfolio_vol #边际风险贡献向量
RC = MRC*weights #风险贡献
RPLoss = 0
for i in range(1,num):
for j in range(i):
RPLoss += (RC[i]-RC[j])**2
return RPLoss
def Riskparity(weight0:list,df_stock):
stock_num = df_stock.shape[1]
df = df_stock
def max_riskparity(weights):
return RiskParityLoss(weights,df) #minimize
cons = ({'type':'eq', 'fun':lambda x: np.sum(x)-1}) #weights的求和=1
bnds = tuple((0,1) for x in range(stock_num)) #风险平价只做(0,1)约束
optimize = sco.minimize(max_riskparity, weight0 , method = 'SLSQP', bounds = bnds,constraints = cons)
optim_weight = list(optimize['x'].round(4))
return optim_weight
对于海外的多空对冲基金,一般还会在约束条件里加上 neutral 和 dollar neutral,感兴趣的读者可以基于以上代码自己实现。对于
的估计可以简单的使用CAPM进行回归估计。我自己的一些想法: 如果在市场下行的时候,组合的Beta能尽可能接近0,则组合将可能不受市场趋势影响,因此一个可能的Idea是考虑优化:
其中第一项组合的
范数表示相对0的绝对偏移量,而第二项旨在最小化组合的波动率。
6 简单实证:以全局最小方差组合为例
这里我仅提供一个案例来说明这些经典优化的一定有效性。考虑构造一个rank-based多因子选股模型:经过单因子测试,我们得到一组有效因子,我们进行因子合成得到单因子:
换仓周期为30天,不剔除停牌的股票,选取换仓日该因子排名最高的30只股票,进行全局最小方差优化,和无优化组合对比。回测周期2011到2021全年。
无优化组合:
全局最小方差组合:
在风险控制方面,原始组合的波动率为0.254,优化组合的波动率为0.21,下降了近20%;原始组合的最大回撤为48.3%,优化组合的最大回撤为38.5%,下降近20%,原始组合的超额收益率为217%,而优化组合的超额收益为321%,高出近50%。
进一步的分析显示,适当增加选股数目,全局最小方差组合在市场长期下行风格中抗跌性能良好,最小回撤较小。更多的结论可以由读者进行多种市场风格切换下不同组合优化模型的表现得到。欢迎同行私戳我进行idea交流(去读master前还能摸会鱼)。