前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >资产瞎配模型(二):对瞎配(一)中净值计算错误的纠正

资产瞎配模型(二):对瞎配(一)中净值计算错误的纠正

作者头像
量化小白
发布2019-01-22 15:35:29
1.5K0
发布2019-01-22 15:35:29
举报

上上周发的那篇资产瞎配模型,事实证明,果然是瞎配,有大佬指出组合净值计算有一定的问题,所以这里对净值计算部分及进行改正,重新计算结果。

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]%12==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]%12==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

参考文献

  1. 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.
  2. 20160725-华泰证券-风险平价模型实证研究:风险平价模型在大类资产配置及行业配置中的应用
  3. 20180309-华宝证券-华宝证券金融工程专题报告:资产配置的流程、框架与运用
  4. 20180928-东北证券-东北证券大类资产配臵“全解析”专题研究之一::风险平价性质探究
  5. 20181114-爱建证券-爱建证券量化资产配置系列报告:从不同维度和角度探索风险平价资产配置方法的稳定性
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 量化小白躺平记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档