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

2019-01-22 15:35:29 浏览数 (1)

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

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

参考文献

  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-爱建证券-爱建证券量化资产配置系列报告:从不同维度和角度探索风险平价资产配置方法的稳定性

0 人点赞