前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >基于高阶矩的行业轮动

基于高阶矩的行业轮动

作者头像
量化小白
发布2019-12-31 15:01:33
1.2K0
发布2019-12-31 15:01:33
举报
文章被收录于专栏:量化小白上分记

本文主要参考了报告[1],报告数据代码获取后台回复“高阶矩行业轮动”。

1.背景

大量研究表明,A股行业有明显的轮动现象,并且与A股相反,行业指数通常呈现动量特征,即前期涨幅高的行业,会延续上涨的趋势,比前期涨幅低的行业有明显超额收益,这一现象之前的文章中也探究过,具体可以参考《研报复制(六):行业轮动的黄金律》

此外,也有大量研究表明,A股市场存在明显的低波动异象,即前期波动率更低的行业,相比于波动率高的行业,未来有明显超额收益。因为投资者不愿意承担过高的风险。这一现象在行业上也是显著存在的。本文参考报告[1]中对波动率因子的定义,对行业上的波动因子进行测试。

最后,考虑到动量是收益率的一阶矩,波动率是收益率的二阶矩,自然而然的想到,是否收益率的三阶矩、四阶矩是否也能对行业的轮动现象做出解释。即用偏度、峰度作为因子做行业轮动。这两个因子也有一些文献做过研究。偏度反映的是数据整体相较于均值的偏离的程度,正偏度越高,表明整体高于均值的程度更高,数据右拖尾。负偏度越高,表明数据整体低于均值的程度更高,数据左拖尾,总体来说,偏度的绝对值越大,表明数据出现极端值的情况越多。 峰度则反映数据整体的集中程度,集中程度越高, 峰度越高。

综上,本文通过因子的动量、波动率、偏度、峰度进行行业轮动测试。

2. 因子定义

  • 动量因子:行业过去20天收益率累计
  • 波动率因子:参考报告[1]的VOL定义,具体如下
  • 偏度因子:用上文高低价计算的rHL计算偏度作为偏度因子
  • 峰度因子:用今开昨收计算的收益率计算峰度作为峰度因子

具体行业轮动策略如下

  • 回测区间:2006.01-2019.06
  • 频率:月度
  • 标的:中信一级行业指数
  • 评价方法:因子IC,ICIR,分层测试曲线,FamaMacBeth回归,因子合成

3. 回测结果

因子累计IC曲线如下

其中,mom为动量因子,vol为波动率因子,sk为偏度因子,kur为峰度因子。IC均值、年化ICIR如下

可以看出,月度上偏度因子、波动率因子比较有效,稳定性较高,动量因子ic较高,但稳定性较差,峰度因子一般。

四个因子的分层测试曲线如下

可以看出,波动率因子、偏度因子的分层特性非常好,Top组明显优于其他组。

各因子的Spearman相关性矩阵如下

偏度和峰度的相关性较高,偏度和波动率的相关性较高。其他各因子之间的相关性都很低。

接下来用FamaMacBeth回归来看在动量因子和波动率因子的基础上,偏度、峰度因子是否能提供额外的信息。FamaMacBeth回归的原理和代码实现参考上一篇《Fama-Macbeth回归和Newey-West调整》

首先用动量和波动率因子做回归,结果如下,需要注意的位置已标红

再加入偏度因子回归

再用动量、波动率、峰度回归

最后用四个因子一起回归

结果如果做一个表格的话,会看的更清楚一些,这里我就直接用图解释了:

  • 首先动量因子、波动率因子都是5%显著的,只加入偏度的话,两个因子都在10%上显著,偏度在5%上显著,波动率的显著性降低较多,说明偏度包含的信息与波动率重复较多,但也有额外信息。此外模型的R2也有明显提升,说明加入偏度是有提升的,因子的方向也与前面IC方向一致。
  • 只加入峰度的话,两因子在10%上显著,峰度不显著,并且模型R2基本没有变,说明峰度没有额外信息。
  • 如果四个因子都包括的话,峰度不显著,波动率不显著,R2显著提升,这都可以用以上两点的结论来解释。

综上,偏度因子比较有效,峰度因子有效性不高,最后用动量、波动率、偏度因子做等权合成f1,f1的IC、ICIR、分层曲线如下

合成之后,因子稳定性显著提升,分层曲线如下

最后需要说明,量价类的因子在周度上比月度更为有效,周度上峰度、偏度都是有效的,并且相关性不高。限于篇幅,这里不给出周度的结果,有兴趣自己测试一下。

4.代码

代码语言:javascript
复制
import os
import pandas as pd
import numpy as np
import datetime
from linearmodels import FamaMacBeth
import statsmodels.api as sm

def getRet(price,freq ='d',if_shift = True):
    price = price.copy()
   
    if freq == 'w':
        price['weeks'] = price['tradedate'].apply(lambda x:x.isocalendar()[0]*100 + x.isocalendar()[1])
        ret = price.groupby(['weeks','classname']).last().reset_index()
        del ret['weeks']
    
    elif freq =='m':
        price['ym'] = price.tradedate.apply(lambda x:x.year*100 + x.month)
        ret = price.groupby(['ym','classname']).last().reset_index()
        del ret['ym']
    
    ret = ret[['tradedate','classname','s_dq_close']]
    if if_shift:
        ret = ret.groupby('classname').apply(lambda x:x.set_index('tradedate').s_dq_close.pct_change(1).shift(-1))
    else:
        ret = ret.groupby('classname').apply(lambda x:x.set_index('tradedate').s_dq_close.pct_change(1))
    
    ret = ret.T.stack(dropna = False).reset_index()
    ret = ret.rename(columns = {ret.columns[2]:'ret'})
    return ret


def getdate(x):
    if type(x) == str:
        return pd.Timestamp(x).date()
    else:
        return datetime.date(int(str(x)[:4]),int(str(x)[4:6]),int(str(x)[6:]))

# 隔夜收益率
def ret_after_days(x):
    x = x.set_index('tradedate')
    r = x.s_dq_open/x.s_dq_close.shift(1) - 1
    return r


def GroupTestAllFactors(factors,ret,groups):
    """
    一次性测试多个因子
    """
    fnames = factors.columns
    fall = pd.merge(factors,ret,left_on = ['classname','tradedate'],right_on = ['classname','tradedate'])
    Groupret = []
    Groupic = []
    for f in fnames: # f= fnames[2]
        if ((f != 'classname')&(f != 'tradedate')):
            fuse = fall[['classname','tradedate','ret',f]]
        
            fuse['groups'] = fuse[f].groupby(fuse.tradedate).apply(lambda x:np.ceil(x.rank()/(len(x)/groups)))
            result = fuse.groupby(['tradedate','groups']).apply(lambda x:x.ret.mean())
            result = result.unstack().reset_index()
            result.insert(0,'factor',f)
            Groupret.append(result)
            groupic = fuse.groupby(['tradedate','groups']).apply(lambda x:x.corr(method = 'spearman').values[0,1])
            groupic = pd.DataFrame(groupic,columns = [f])
            groupic = groupic.reset_index().pivot(index = 'tradedate',columns = 'groups',values = f)
            groupic.insert(0,'factor',f)
            Groupic.append(groupic)
# print(f)
    Groupret = pd.concat(Groupret,axis = 0).reset_index(drop = True)
    Groupret = Groupret.fillna(0)
    Groupnav = Groupret.iloc[:,2:].groupby(Groupret.factor).apply(lambda x:(1 + x).cumprod())
    Groupnav = pd.concat([Groupret[['tradedate','factor']],Groupnav],axis = 1)

    Groupic = pd.concat(Groupic,axis = 0)
    return Groupnav,Groupic



def getICSeries(factors,ret,method):
    # method = 'spearman';factors = fall.copy();

    icall = pd.DataFrame()
    fall = pd.merge(factors,ret,left_on = ['tradedate','classname'],right_on = ['tradedate','classname'])
    icall = fall.groupby('tradedate').apply(lambda x:x.corr(method = method)['ret']).reset_index()
    icall = icall.drop(['ret'],axis = 1).set_index('tradedate')

    return icall

 

'''

数据读入
'''

datas = pd.read_csv('中信一级行业指数日度行情序列.csv',encoding = 'gbk')
#datas = pd.read_csv('申万一级行业指数日度行情序列.csv',encoding = 'gbk')

'''
收益率计算
'''



# 当日和前一日收盘价算的收益率
ret = datas.groupby('classname').apply(lambda x:x.set_index('tradedate').s_dq_close.pct_change(1)).T.stack(dropna = False).reset_index()
ret = ret.rename(columns = {ret.columns[2]:'ret'})
ret['tradedate'] = ret.tradedate.apply(getdate)

# 开收盘价算的收益率
ret_oc = datas.groupby('classname').apply(lambda x:x.set_index('tradedate').s_dq_close/x.set_index('tradedate').s_dq_open - 1).T.stack(dropna = False).reset_index()
ret_oc = ret_oc.rename(columns = {ret_oc.columns[2]:'ret'})
ret_oc['tradedate'] = ret_oc.tradedate.apply(getdate)

# 高低价算的收益率
ret_hl = datas.groupby('classname').apply(lambda x:(x.set_index('tradedate').s_dq_high - x.set_index('tradedate').s_dq_low)/(x.set_index('tradedate').s_dq_high + x.set_index('tradedate').s_dq_low)/2).T.stack(dropna = False).reset_index()
ret_hl = ret_hl.rename(columns = {ret_hl.columns[2]:'ret'})
ret_hl['tradedate'] = ret_hl.tradedate.apply(getdate)

# 正常的逐日收益率计算
datas['tradedate'] = datas.tradedate.apply(getdate)
ret_m = getRet(datas,freq ='m',if_shift = True)

'''
因子计算
1. 动量因子:过去一个月/周的涨跌幅
2. 波动率因子:过去一个月/周的日收益率的波动率
收益率1 :开/收 - 1
收益率2:(高 - 低)/(高 + 低)/2
波动率因子 = 波动率因子1和2打分后加总
3. 偏度因子 :收益率的偏度
4. 峰度因子:波动率的峰度

'''

N = 20
# 1月动量
mom_m = ret.groupby('classname').apply(lambda x:x.set_index('tradedate').rolling(15).sum()).fillna(0)
mom_m = mom_m.reset_index()
mom_m = mom_m.rename(columns = {mom_m.columns[2]:'mom'})
 


# 波动率因子
vol_m_1 = ret_oc.groupby('classname').apply(lambda x:x.set_index('tradedate').rolling(N).std()).fillna(0)

vol_m_1 = vol_m_1.reset_index()
vol_m_1 = vol_m_1.rename(columns = {vol_m_1.columns[2]:'vol_m_1'})


vol_m_2 = ret_hl.groupby('classname').apply(lambda x:x.set_index('tradedate').rolling(N).std()).fillna(0)
vol_m_2 = vol_m_2.reset_index()
vol_m_2 = vol_m_2.rename(columns = {vol_m_2.columns[2]:'vol_m_2'})
 
# 偏度因子
sk = ret_hl.groupby('classname').apply(lambda x:x.set_index('tradedate').rolling(N).skew()).fillna(0)
sk = sk.reset_index()
sk = sk.rename(columns = {sk.columns[2]:'sk'})
 
# 峰度因子
kur = ret.groupby('classname').apply(lambda x:x.set_index('tradedate').rolling(N).kurt()).fillna(0)
kur = kur.reset_index()
kur = kur.rename(columns = {kur.columns[2]:'kur'})
 

# 峰度因子
"""
因子合并
"""


factors_m = pd.merge(mom_m,vol_m_1,left_on = ['tradedate','classname'],right_on = ['tradedate','classname'])
factors_m = pd.merge(factors_m,vol_m_2,left_on = ['tradedate','classname'],right_on = ['tradedate','classname'])
factors_m['vol'] = factors_m.vol_m_1.groupby(factors_m.tradedate).rank(ascending = True) + factors_m.vol_m_2.groupby(factors_m.tradedate).rank(ascending = False)
factors_m = pd.merge(factors_m,sk,left_on = ['tradedate','classname'],right_on = ['tradedate','classname'])
factors_m = pd.merge(factors_m,kur,left_on = ['tradedate','classname'],right_on = ['tradedate','classname'])

factors_m = factors_m.drop(['vol_m_1','vol_m_2'],axis = 1)
# factors_m['r1'] = factors_m.vol_m_1.groupby(factors_m.tradedate).rank(ascending = False)


"""
因子测试
"""



groups = 5


startdate = datetime.date(2006,1,1)
enddate = datetime.date(2019,6,30)

fm = factors_m.loc[(factors_m.tradedate>= startdate) &(factors_m.tradedate <= enddate)].reset_index(drop = True).copy()

fm['f1'] = fm['vol'] + fm['mom']
fm['f2'] = fm['vol'] + fm['mom'] - fm['sk'] -fm['kur']


# IC
ic_m = getICSeries(fm,ret_m,'spearman')

ic_m[['vol','mom','sk','kur']].cumsum().plot(figsize = (8,4))
ic_m.mean()
ic_m.mean()/ic_m.std()*np.sqrt(12)

fm.iloc[:,2:].corr(method = 'spearman')

nav_m,ic_m = GroupTestAllFactors(fm,ret_m,groups)


nav_m.loc[nav_m.factor == 'mom'].set_index('tradedate').plot(figsize = (10,5),title = 'mom')
nav_m.loc[nav_m.factor == 'vol'].set_index('tradedate').plot(figsize = (10,5),title = 'vol')
nav_m.loc[nav_m.factor == 'sk'].set_index('tradedate').plot(figsize = (10,5),title = 'sk')
nav_m.loc[nav_m.factor == 'kur'].set_index('tradedate').plot(figsize = (10,5),title = 'kur')
nav_m.loc[nav_m.factor == 'f1'].set_index('tradedate').plot(figsize = (10,5),title = 'f1')
nav_m.loc[nav_m.factor == 'f2'].set_index('tradedate').plot(figsize = (10,5),title = 'f2')


# FamaMacbeth

fall = pd.merge(fm,ret_m,left_on = ['tradedate','classname'],right_on = ['tradedate','classname'])
fall = fall.set_index(['classname','tradedate']).fillna(0)

fm = FamaMacBeth(dependent = fall['ret'],exog = sm.add_constant(fall[['vol','mom']]))
fm.fit()
 
fm = FamaMacBeth(dependent = fall['ret'],exog = sm.add_constant(fall[['vol','mom','sk']]))
fm.fit()
 
 
fm = FamaMacBeth(dependent = fall['ret'],exog = sm.add_constant(fall[['vol','mom','kur']]))
fm.fit()



fm = FamaMacBeth(dependent = fall['ret'],exog = sm.add_constant(fall[['vol','mom','sk','kur']]))
fm.fit()

参考文献

[1]20170409-海通证券-海通证券金融工程专题报告:动量策略及收益率高阶矩在行业轮动中的应用-388742

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2019-12-28,如有侵权请联系 cloudcommunity@tencent.com 删除

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

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

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

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