上上周发的那篇资产瞎配模型,事实证明,果然是瞎配,有大佬指出组合净值计算有一定的问题,所以这里对净值计算部分及进行改正,重新计算结果。
00
组合收益率计算说明
组合的净值计算是一件并不容易的问题,这里给出计算公式,并说明之前那一篇错在哪里,公式推导如下:
上一篇在计算组合收益率净值时候,不论是月度、半年度、年度调仓,我的做法都是在每一期末根据历史数据算出最优的权重,之后一直使用这一权重进行加权算收益率。
但根据刚才的推导可以看出。期初根据权重可以确定买入的份额,买入后份额是不变的,但随着价格的波动,单个资产和组合的价值都在变动,从而导致各资产权重也会发生变化。这也是每过一段时间就需要做再平衡的原因,但我一直用期初的权重计算组合的收益率,实际上是忽略了权重的变动,这在短期来看,影响会非常小,但随着时间推移,误差会越来越大,会有问题。本文我们用组合的价值计算组合的收益,可以与上一篇进行对比,看一看差别有多大。
此外,还有两个需要说明的点,一个是上篇提到的汇率的影响,如果是用价值计算就可以看出,汇率是不用考虑的,虽然标普500的1点和中证全指的1点有汇率差异价格不一样,但权重确定的情况下,汇率只影响能买到的份额,买到份额代表的总价值是固定不变的,当然如果考虑到汇率的变动,组合的总价值也会随汇率波动而波动,这个就忽略了。
另一方面,用组合价值计算时需要考虑到期末调仓时这一特殊时间点,调仓前和调仓后计算组合价值对应的资产权重是不一样的,所以组合价值之间可能会有很大差异,导致净值曲线出现一个价格缺口,有点类似不复权的价格出现了分红配股的情况,好在比分红配股的情况简单的多。因此需要计算调整市值,通过调整市值来计算收益,让净值曲线连续。(关于调整市值的说明,可以参见各个指数编制说明),当然也可以考虑用资金的方式计算组合收益,假设期初手里有1000元,根据权重确定每类资产的份额,计算每类资产带来的收益。两种方法逻辑上是差不多大的,但都会很麻烦,pct_change是用不了了,需要循环,而且速度会慢很多。
接下来对各个模型的代码和结果进行修改,代码可以留意下,再看看图就行了,文字可以忽略了,基本没变化。代码中变量沿用公式中的符号,每个函数中,weights是各资产的权重,N是各资产的份额,price是各资产的价格,V是组合的价值,但是份额数据这里只是代表一个比例,是相对数,不是绝对数,10:20跟1:2,0.1:0.2是一个意思。mv是组合的市值,mv_adj_last_day是前一天的调整市值。如果还有不正确的地方,欢迎指正!
01
理论模型
资产配置是根据投资者的收益风险偏好及不同资产特性,将资金配置于多种资产类别的一种投资策略,目的在于分散风险,是对组合收益和组合风险的权衡。
首先给出一些符号定义
接下来说明所用到的配置模型,资产配置从技术角度来说,只需要考虑三个问题:选择资产、横截面分配、时间序列分配。
选择资产相对主观,最重要的原则是风险不同源,从数据角度来说,相关性不能太高,相关性过高的资产,难以通过调整权重来规避风险。横截面分配与时间序列分配实质上就是确定各种资产的权重,各种模型也都是在选定资产后,在不同的假设下给出不同的权重表达式。接下来列出文中用到的所有资产配置模型。(公式较多,可能引起不适,可以直接看下一部分,不影响悦读)
几种简单粗暴的配置方式:
除了这些相对简单的配置方式外,还有很多理论上很完美的模型,基本都能看见马科维茨的影子。
风险平价跟等波动率相对比,出发点都是使每类资产的面临的风险相同,但不同之处在于,等波动率考虑的是让各个资产对应的风险值相同,风险平价考虑的是让权重变化引起风险的的比例相同。
当然除了这些,还有美林时钟、Black-Litterman等模型,也应用很广,美林时钟比较定性,BL模型是MVO的基础上引入了预期收益,客观 主观,但目前没搞懂实际应用时候观点矩阵该怎么定义,等搞懂了再尝试吧。
02
回测:资产选择
资产应选择相关性较低的资产,一般都是权益、债券、商品、黄金等资产中选择。本文选择资产类型如下
回测区间:2006年1月-2018年12月
数据来源:wind
将各资产起点标准化为1000点,各资产走势如下
不得不感叹,A股真的是十年一梦啊,各资产相关性如图,没有相关性很高的资产,所以做配置模型数据上没有什么问题。
03
回测:等权重
等权方式配置,起点时刻各资产买同样权重,一直持有,之前的写法忽略的权重的变动就非常简单,如果考虑权重变动影响,代码也变的比较复杂
代码语言:javascript复制def EqualWeight(datas,period):
ret = datas.pct_change(1).fillna(0)
data_norm = datas/datas.iloc[0,]*1000
result = data_norm.copy()
result['m'] = result.index
result['m'] = result.m.apply(lambda x:x.month)
weights = pd.DataFrame(columns = datas.columns,index = datas.index).fillna(0)
N = pd.DataFrame(columns = datas.columns,index = datas.index).fillna(0)
if period == 'month':
for i in range(result.shape[0]):
if i == 0:
weights.iloc[i,:] = 1/datas.shape[1]
price = datas.loc[datas.index[i],:]
n = weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
elif result.m[i] != result.m[i - 1]:
weights.iloc[i,:] = 1/datas.shape[1]
price = datas.loc[datas.index[i],:]
n = weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
else:
N.iloc[i,:] = N.iloc[i-1,:]
weights.iloc[i,:] = N.iloc[i,:]*datas.loc[datas.index[i],:]
elif period == '6month':
for i in range(result.shape[0]):
if i == 0:
weights.iloc[i,:] = 1/datas.shape[1]
price = datas.loc[datas.index[i],:]
n = weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
elif (result.m[i] != result.m[i - 1] and result.m[i]%6==0) :
weights.iloc[i,:] = 1/datas.shape[1]
price = datas.loc[datas.index[i],:]
n = weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
else:
N.iloc[i,:] = N.iloc[i-1,:]
weights.iloc[i,:] = N.iloc[i,:]*datas.loc[datas.index[i],:]
elif period == 'year':
for i in range(result.shape[0]):
if i == 0 :
weights.iloc[i,:] = 1/datas.shape[1]
price = datas.loc[datas.index[i],:]
n = weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
elif (result.m[i] != result.m[i - 1] and result.m[i]==0) :
weights.iloc[i,:] = 1/datas.shape[1]
price = datas.loc[datas.index[i],:]
n = weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
else:
N.iloc[i,:] = N.iloc[i-1,:]
weights.iloc[i,:] = N.iloc[i,:]*datas.loc[datas.index[i],:]
else:
return '请输入调仓周期'
result['mv'] = 0
result['mv_adj_last_day'] = 0
result['nav'] = 1
for i in range(result.shape[0]):
result.loc[result.index[i],'mv'] = (datas.iloc[i,:]*N.iloc[i,:]).sum()
if i == 0:
pass
elif all(weights.iloc[i,:] == weights.iloc[i-1,:]):
result.loc[result.index[i],'mv_adj_last_day'] = result.loc[result.index[i-1],'mv']
result.loc[result.index[i],'nav'] = result.nav[i-1]*result.mv[i]/result.mv_adj_last_day[i]
else:
result.loc[result.index[i],'mv_adj_last_day'] = (datas.iloc[i-1,:]*N.iloc[i,:]).sum()
result.loc[result.index[i],'nav'] = result.nav[i-1]*result.mv[i]/result.mv_adj_last_day[i]
result['nav'] = result.nav/result.nav[0]*1000
return weights,result
净值曲线
权重变化
之前做出来的权重是一马平川,现在看一下考虑到了价格波动对于组合价值影响后,各资产的权重变化:
月度
月度结果看起来跟之前差不多,略有波动。
年度
年度来看,随着时间推移,权重偏离初始设定越来越大,这时候就体现出再平衡的重要作用。
04
等资金
有了之前的推导可以看出,等资金的推导逻辑是错误的,权重并非是份额的占比,而是价值的占比,所以等资金就是等权重,这个就略过了。
05
等波动率
等波动率以及后面需要用到协方差的模型都需要考虑一个问题,如何估计波动率/协方差?这里图方便我们都使用历史波动率估计量,不考虑高端方法。有两种方式计算,一种是滚动计算,每次只用过去一段时间的数据计算,另一种研报里称为递归计算,用过去所有数据计算,这里两种方法都进行尝试,对结果进行对比。
递归计算代码如下,滚动代码类似。
代码语言:javascript复制def EqualVolWeight(datas,period ='month'):
ret = datas.pct_change(1).fillna(0)
data_norm = datas/datas.iloc[0,]*1000
result = data_norm.copy()
result['m'] = result.index
result['m'] = result.m.apply(lambda x:x.month)
weights = pd.DataFrame(columns = datas.columns,index = datas.index).fillna(0)
N = pd.DataFrame(columns = datas.columns,index = datas.index).fillna(0)
if period == 'month':
for i in range(result.shape[0]):
if i == 0:
pass
elif result.m[i] != result.m[i - 1]:
vol = ret.iloc[:i].std()
weights.iloc[i,:] = (1/vol)/((1/vol).sum())
price = datas.loc[datas.index[i],:]
V = (weights.iloc[i,:]*price).sum()
n = V*weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
else:
N.iloc[i,:] = N.iloc[i-1,:]
w = N.iloc[i,:]*datas.loc[datas.index[i],:]
weights.iloc[i,:] = w/w.sum()
elif period == '6month':
for i in range(result.shape[0]):
if i == 0:
pass
elif (result.m[i] != result.m[i - 1] and result.m[i]%6==0) :
vol = ret.iloc[:i].std()
weights.iloc[i,:] = (1/vol)/((1/vol).sum())
price = datas.loc[datas.index[i],:]
V = (weights.iloc[i,:]*price).sum()
n = V*weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n/n.sum()
else:
N.iloc[i,:] = N.iloc[i-1,:]
w = N.iloc[i,:]*datas.loc[datas.index[i],:]
weights.iloc[i,:] = w/w.sum()
elif period == 'year':
for i in range(result.shape[0]):
if i == 0:
pass
elif (result.m[i] != result.m[i - 1] and result.m[i]==0) :
vol = ret.iloc[:i].std()
weights.iloc[i,:] = (1/vol)/((1/vol).sum())
price = datas.loc[datas.index[i],:]
V = (weights.iloc[i,:]*price).sum()
n = V*weights.iloc[i,:].values/price.values
N.loc[result.index[i],:] = n
else:
N.iloc[i,:] = N.iloc[i-1,:]
w = N.iloc[i,:]*datas.loc[datas.index[i],:]
weights.iloc[i,:] = w/w.sum()
else:
return '请输入调仓周期'
result['mv'] = 0
result['mv_adj_last_day'] = 0
result['nav'] = 1
for i in range(result.shape[0]):
result.loc[result.index[i],'mv'] = (datas.iloc[i,:]*N.iloc[i,:]).sum()
if all(N.iloc[i,:]==0):
pass
elif all(N.iloc[i,:] == N.iloc[i-1,:] ):
result.loc[result.index[i],'mv_adj_last_day'] = result.loc[result.index[i-1],'mv']
result.loc[result.index[i],'nav'] = result.nav[i-1]*result.mv[i]/result.mv_adj_last_day[i]
else:
result.loc[result.index[i],'mv_adj_last_day'] = (datas.iloc[i-1,:]*N.iloc[i,:]).sum()
result.loc[result.index[i],'nav'] = result.nav[i-1]*result.mv[i]/result.mv_adj_last_day[i]
result['nav'] = result.nav/result.nav[0]*1000
return weights,result
滚动结果
递归结果
从结果可以明显看出,滚动窗口敏感性更高,一方面能够更贴近最新的趋势,但另一方面也可能对于噪声过度反应。 相比之下, 递归窗口稳定性好得多,但不够灵敏。两种方法各有优劣。但整体来看,两种方法对应的权重是类似的,说明还是比较稳健的。
等波动率的情况下,货币的波动率太小了,导致高配货币,零配A股。很稳健,但这种结果跟直接买货币也差不多了,没什么意义。
06
GMV
首先尝试直接套用模型解析解的表达式计算权重。只给出权重计算公式,其他部分都和前面是一样的。
代码语言:javascript复制sigma = ret.iloc[:i].cov()
weight = np.dot(np.mat(sigma).I,np.ones([sigma.shape[1],1]))
weights.iloc[i,:] = np.array(weight/(weight.sum())).T[0]
递归结果
权重有负值
滚动
权重有负值,A股、债券都不能做空,所以不符合常理,因此加上卖空限制后,重新求解。
07
GMO 卖空限制
有卖空限制后,模型没有解析解,只能通过最优化方法求数值解,我们使用python的scipy库中的minmum函数进行优化求解,funs为优化目标。
代码语言:javascript复制def funs(weight,sigma):
weight = np.array([weight]).T
result = np.dot(np.dot(weight.T,np.mat(sigma)),weight)[0,0]
return(result)
res = minimize(funs,weight, method='SLSQP',args = (sigma,),
bounds=bnds,constraints=cons,tol=1e-8)
weights.iloc[i,:] = res.x
滚动结果
递归结果
两种方法结果基本是差不多的,债券和货币依然占绝大比例。
将单个资产占比限定在50%以内,重新优化:
滚动结果
基本上没有什么变化。
08
Risk Parity
优化函数funsRP
代码语言:javascript复制def funsRP(weight,sigma):
weight = np.array([weight]).T
X = np.multiply(weight,np.dot(sigma.values,weight))
result = np.square(np.dot(X,np.ones([1,X.shape[0]])) - X.T).sum()
return(result)
滚动
递归
A股大概是怎么都不愿意配一点了,为了避免单个资产权重过高或者过低的问题,对资产权重加以限制。
09
Risk Parity w<=40%
滚动
递归
10
Risk Parity w>=10%
滚动
递归
11
所有结果净值对比
递归-月度
递归-半年度
递归-年度
滚动-月度
滚动-半年度
滚动-年度
12
结果评价
从净值上来看,等权重是最优的,我们计算不同组合下的年化收益,波动率,夏普比,对结果进行评价。
代码语言:javascript复制def performance(datas):
nav = (datas.nav[datas.shape[0]-1]/1000)**(1/12) - 1
vol = (datas.nav.pct_change(1)).std()*np.sqrt(250)
Sharp = nav/vol
return nav,vol,Sharp
剔除有问题的等资金,所有方法结果对比如下,出现做空的GMO也可以忽略
滚动
递归
两种参数估计方式下,结果基本是一致的:
- 等波动率优于RP优于GMO优于等权重。如果看收益和波动率,等波动率下的年化收益是所有方法里最低的,但波动率也是最小的,小一个数量级。因为资产中有货币这一基本没有波动的资产,导致等波动率情况下货币占了80%以上的比例。除过等波动率的情况看,风险平价要更优一些。
- 对于半年度和年度再平衡的策略,刚开始不满六个月/十二个月的时候,我所有的权重都设置的0,导致这段时间这些策略收益一直是0,其实至少可以全配货币或者按无风险利率累积。
总体来说,净值曲线有肉眼可见的差别,说明这样的纠正是非常有必要的!!!
13
参考文献
- Bodnar T, Schmid W. A test for the weights of the global minimum variance portfolio in an elliptical model[J]. Metrika, 2008, 67(2):127.
- 20160725-华泰证券-风险平价模型实证研究:风险平价模型在大类资产配置及行业配置中的应用
- 20180309-华宝证券-华宝证券金融工程专题报告:资产配置的流程、框架与运用
- 20180928-东北证券-东北证券大类资产配臵“全解析”专题研究之一::风险平价性质探究
- 20181114-爱建证券-爱建证券量化资产配置系列报告:从不同维度和角度探索风险平价资产配置方法的稳定性