碧沼

V1

2022/11/26阅读:25主题:橙心

研报复现系列3

研报复现系列(3)指数高阶矩择时策略

前言

本文参考广发证券2015年5月20日的研报《指数高阶矩择时策略——交易性择时策略研究之八》对其研究过程和结论进行复现学习。复现金工研报是学习量化策略,训练编程能力的很好途径。本文基本成功复现该研报的结果。

如果您对我的内容感兴趣,欢迎后台私信进行交流。

1.研报概述

收益率具有波动聚集、尖峰厚尾的性质。在市场大跌遭遇危机之时,资产价格迅速下降,震幅明显上升,波动率迅速升高,资产价格会是非平稳的高斯分布,这样仅仅用均值和方差来刻画资产价格的时间序列就会是不恰当的。此时高阶矩会异常发散,迅速增大,我们不可以忽略高阶矩的存在以及影响。

实际上基于这种思想,以GARCH模型为代表的二阶矩的研究在金融时间序列领域在1980年后就备受关注。

本研报的核心思想就是在下跌的过程中,高阶矩的变化要领先于走势的变化,当我们观测到高阶矩的增大或减小,就可以为根据其变化给出调仓信号。

图1 文章背景
图1 文章背景

本文使用的是收益率的k阶原点矩。计算公式如下:

模型构建方法为:

  • 计算每天日收益率的5阶矩,长度为20个交易日
  • 在T日收盘后,计算出T日之前的5阶矩
  • 对5阶矩进行指数移动平均处理,windows为移动平均窗口长度,调整可以得到不同的择时效果。
  • 滚动窗口外推,每隔90个交易日,利用T日前90个交易的窗口期进行参数确定,确定未来90个交易日指数移动平均的参数
  • 根据切线法,如果T日5阶矩的EMA大于T-1日的EMA那么T+1日为看多,以T日收盘价建仓。否则为看空。
  • 计算过程中设定10%止损线。

2.数据来源

本文的数据来源是通过Tushare获取的指数日线级别数据。

引入本文所需的第三方库。

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tushare as ts
from tqdm import tqdm 
plt.rcParams['font.family'] = ['sans-serif']
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

获取指数的日线数据。

data = pro.index_daily(ts_code='000300.SH')
data.sort_values('trade_date',inplace=True)
data.pct_chg = data.pct_chg*0.01
data.reset_index(inplace=True,drop=True)
data.set_index('trade_date',inplace=True)
data.index = pd.DatetimeIndex(data.index)

3.高阶矩择时模型复现

计算矩函数:

def cal_moment(arr,n=5):
    '''
    arr:数据序列
    n:阶数
    '''

    arr = np.array(arr)
    return np.mean(arr**n)

计算EMA函数:

def ema(arr,alpha):
    series = pd.Series(arr)
    return series.ewm(alpha=alpha,adjust=False).mean().iloc[-1]

图1展示了收益率1-9阶矩的表现与沪深300的走势图。可以比较明显的看出,奇数阶矩中,在下跌过程出现了量级迅速增大,而且为负值,而且至少都提前于下跌的最低点,有一定的领先作用。

图2 1-9阶矩表现
图2 1-9阶矩表现

接下来构建一个简单的信号框架,不考虑交易成本,考虑不同的止损阈值,单向做多。函数如下:

'''
简单的策略信号框架,不考虑交易成本
'''

def get_position(ret,cond,loss=0.1):
    '''
    ret:收益率
    cond:信号序列
    loss:止损阈值
    '''

    df = pd.concat([ret,cond],axis=1
    df.columns = ['ret','cond']
    position = []      
    loss_flag = 1
    df.reset_index(inplace=True)
    for i in range(len(df)-1):
        if position:
            # 开仓信号,大于止损线
            if df.loc[i,'cond'and loss_flag >=1-loss:
                position.append(1)
                loss_flag = loss_flag*(1+df.loc[i+1,'ret'])
                loss_flag = min(1,loss_flag)
            else:      
                position.append(0
                loss_flag = 1         
        else:           
            if df.loc[i,'cond'and loss_flag >=1-loss:
                position.append(1)
                loss_flag = loss_flag*(1+df.loc[i+1,'ret'])
                loss_flag = min(1,loss_flag)
            else:
                position.append(0)
                loss_flag = 1
    position.append(np.NAN)
    df['position'] = position
    
    df.set_index('trade_date',inplace=True)
    return df

原文里在基准模型中考虑了做多和做空的双向信号,因此我们再构建一个双向信号框架。代码如下:

def get_biposition(ret,cond,loss=0.1):
    '''
    ret:收益率
    cond:信号序列
    '''

    df = pd.concat([ret,cond],axis=1
    df.columns = ['ret','cond']
    position = []      
    loss_flag = 1
    df.reset_index(inplace=True)
    for i in range(len(df)-1):
        if position:
            # 开仓信号,大于止损线
            if position[-1] == 1:
                if df.loc[i,'cond'and loss_flag >=1-loss:
                    position.append(1)
                    loss_flag = loss_flag*(1+df.loc[i+1,'ret'])
                    loss_flag = min(1,loss_flag)
                else:      
                    position.append(-1
                    loss_flag = 1  
            elif position[-1] == -1:
                if df.loc[i,'cond']==False and loss_flag >=1-loss:
                    position.append(-1)
                    loss_flag = loss_flag*(1-df.loc[i+1,'ret'])
                    loss_flag = min(1,loss_flag)
                else:      
                    position.append(1
                    loss_flag = 1  

        else:           
            if df.loc[i,'cond'and loss_flag >1-loss:
                position.append(1)
                loss_flag = loss_flag*(1+df.loc[i+1,'ret'])
                loss_flag = min(1,loss_flag)
            else:
                position.append(-1)
                loss_flag = loss_flag*(1-df.loc[i+1,'ret'])
                loss_flag = min(1,loss_flag)
    position.append(np.NAN)
    df['position'] = position
    df.set_index('trade_date',inplace=True)
    return df

回测表现如下:

图3 单向看多表现(alpha=0.05)
图3 单向看多表现(alpha=0.05)
图4 双向多空表现(alpha=0.05)
图4 双向多空表现(alpha=0.05)

我们可以先不考虑滚动确定alpha值,用给定的alpha值、窗口期、10%止损线,先对两个策略的表现进行简单的观察。对于策略的评估函数暂不开源。

单向看多择时 双向多空择时
累积净值 11.35 15.44
年化收益 14.54% 16.52%
夏普比率 0.75 0.58
最大回撤 -58.78% -67.02%
最大回撤开始时间 2008-01-11 2008-06-30
最大回撤结束时间 2008-12-04 2008-12-04
年化收益/回撤比 0.25 0.25

可以看到无论是单向做多还是双向多空择时,在累计净值上取得了优异的表现,最大回撤主要是出现在了08年的股灾之中。

从图5可以看出,在08年的股灾中,出现了几次5阶矩的向上波动,是大跌过程中的短暂的几次反弹,导致了策略出现较大的回撤,这也提示我们这个策略对于大幅下跌过程中的反弹会出现一定的回撤,可以尝试增加其他的风控条件。

图5 5阶矩波动
图5 5阶矩波动

以上的测试都是基于后验的视角进行的参数设置,接下来根据原文的思路进行滚动设定移动平均参数


result_series = pd.DataFrame()
'''
    动态寻优调整alpha
'''

from tqdm import tqdm

windows = 90
ema_windows = 120
for i in tqdm(range(1,len(data)//windows )):
    if i <len(data)//windows  -2 :
        tem = []
        for alpha in np.arange(0.05,0.55,0.05):
            tem_1 = data.iloc[:(i+1)*windows,14].rolling(windows ).apply(ema,args=(alpha,))
            tem_1 = tem_1.iloc[-windows:,]
            ret = data.loc[tem_1.index].pct_chg 
            tem_2 = tem_1>tem_1.shift(1)
            result_return = get_position(ret, tem_2)
            result_return = result_return['ret'].shift(-1) * result_return['position']
            cum = (1 + result_return).cumprod().dropna()
            tem.append(cum[-1])

        alpha = (np.argmax(tem)+1)*0.05
        tem_3 = data.iloc[:(i+2)*windows,14].rolling(windows ).apply(ema,args=(alpha,))
        tem_3 = tem_3.iloc[-windows:]
        tem_4 = tem_3>tem_3.shift(1)          
图6 单向看多择时(动态alpha)
图6 单向看多择时(动态alpha)
图7 双向多空择时(动态alpha)
图7 双向多空择时(动态alpha)

从结果中可以看出,动态调整的 得到的净值曲线和原本基本一致,并且回撤得到了一定程度的控制,由于动态调整导致损失了一定的持仓时间,对单向看多择时来说并没有损失太多的收益。

单向看多择时 双向多空择时
累积净值 9.9 11.13
年化收益 14.73% 15.54%
夏普比率 0.77 0.53
最大回撤 -50.94% -54.38%
最大回撤开始时间 2008-01-11 00:00:00 2008-08-15 00:00:00
最大回撤结束时间 2008-11-10 00:00:00 2008-11-18 00:00:00
年化收益/回撤比 0.29 0.29

4.不同止损阈值的测试

尝试对于止损阈值进行修改。观测其表现,结果和代码如下所示:

'''
不同止损阈值
'''

ema_window = 120#原文给出窗口期
alpha = 0.05
result_series = data['5阶矩'].rolling(ema_window).apply(ema,args=(0.05,)) 
cond_series = result_series>result_series.shift(1)
ret = data.loc[:,'pct_chg']
for i in np.arange(0,0.11,0.02):
  result_return = get_biposition(ret, cond_series,loss=i)
  result_return = result_return['ret'].shift(-1) * result_return['position']
  cum = (1 + result_return).cumprod()
  benchmark = (1 + ret).cumprod()
  plt.plot(pd.DataFrame({'result_return':cum}))
  plt.legend(list(np.arange(0,0.11,0.02)))

图8 双向多空择时(不同阈值) 从图8可以看出,确实是10%的止损线最终取得的收益表现最好。

图9 各年份不同阈值收益率
图9 各年份不同阈值收益率

5.不同开仓阈值的测试

在前述的测试中,我们只要5阶矩出现了增加就发出开仓指令,原文中设定了(1+k)的阈值,k=0,0.01,0.02也就是说,当k变动超过一定幅度的时候才进行开仓。因此我们也对不同k值进行测试。

  for k in np.arange(0,0.06,0.01):
    result_series = pd.DataFrame(result_series)
    cond_series = result_series['5阶矩']>result_series['5阶矩'].shift(1)*(1+k)

    ret = data.loc[:,'pct_chg']
    tem = {}

    result_return = get_position(ret, cond_series,loss=0.1)
    result_return = result_return['ret'].shift(-1) * result_return['position']
    cum = (1 + result_return).cumprod()
    benchmark = (1 + ret).cumprod()
    tem[i] = cum 
    plt.plot(pd.DataFrame({'result_return':cum}))
    plt.legend(list(np.arange(0,0.06,0.01)))

  
图10 不同开仓阈值(单向做多)
图10 不同开仓阈值(单向做多)

可以看到k=0.01也就是说当收益率5阶矩向上变动大于1%时效果最好。

6.不同阶矩的测试

除了5阶矩我们也可以测试其他的奇数阶矩,有趣的是相比于原文得出的5阶矩的结论,从净值曲线来看,7年后的视角,7阶矩相比于5阶矩表现稍好一些。

图11 单向看多择时
图11 单向看多择时
图12 双向多空择时
图12 双向多空择时

7.考虑交易成本的ETF测试

以上的测试都没有考虑交易成本,本文将交易信号在choice组合管理中进行测试,交易标的为沪深300ETF,时间从12年底至今。回测曲线如下图所示:

图13 ETF回测表现
图13 ETF回测表现

从ETF回测表现中可以看出,考虑了交易成本后,这一策略表现仍然优异,12年底至今收益率达到300%,相比于沪深300有大幅超额,表现优异。

8.其他指数的测试

除了沪深300,本文还进行了其他指数的测试,大部分均取得了不错的表现,受限于篇幅。本文只展示中证500的择时净值曲线。如下图所示:

图14 单向看多择时(中证500)
图14 单向看多择时(中证500)

欢迎在后台进行交流 以上代码部分用于XJTU课程作业。

分类:

后端

标签:

后端

作者介绍

碧沼
V1