研报复现系列(四):【华泰证券】波动率与换手率构造牛熊指标

前言

我们是国内普通高校的在校学生,同时也是量化投资的初学者。我们的学校不是清北复交,也没有金融工程实验室,同时地处三线小城,因此我们在校期间较难获得量化实习机会,但我们期待与业界进行沟通、交流。

蔡金航同学是我们其中的一员。其在寻找暑期量化实习时,收到了几家私募和券商金工组的笔试邀请,笔试内容皆为在给定时间内复现出一篇金工研报。蔡同学受到启发,发觉复现金工研报是我们学习量化策略、锻炼程序设计能力同时也是与业界交流的很好的途径。

在蔡同学的建议下,我们开启研报复现系列的创作,记录我们的学习过程,并将我们的创作内容分享出来,与读者们一起交流、学习、进步。

我们的水平有限,创作的内容难免会有错误或不严谨的内容,我们欢迎读者的批评指正。

文章同步发布于<HI投量化俱乐部>。

如果您对我们的内容感兴趣,请联系我们:cai_jinhang@foxmail.com

本文作者:何百圣 哈尔滨工业大学威海校区 经济管理学院 数量金融方向

(本篇文章首发于何百圣同学的个人博客

1.研报概述

本文是券商金工研报复现系列的第四篇,文本复现了【华泰证券】的【波动率与换手率构造牛熊指标】。

波动率和换手率是常见的市场监测指标。利用波动率和换手率能够构造出与市场长期走势明显负相关的指标,而且指标趋势性较好,可以用来判断股市牛熊。借助波动率与换手率构造出的牛熊指标与市场走势负相关性明显,借助牛熊指标开发的择时策略普遍优于直接对指数本身的择时策略。

对于股票市场来说,下跌时的波动率往往比上涨时的波动率更高。换手率的上升与下降往往与股票市场本身有强相关性。借助于波动率与换手率能够很好的将市场进行分类,牛市与熊市都有与之对应的明确的组合特征。

我们借助于波动率与换手率两个维度来对市场进行观察,类似美林投资时钟,可以将市场分为四种状态,这四种状态对应的市场走势有较强的规律性:
1.波动率换手率同时上行。这时市场是典型的牛市特征,市场快速上涨使得波动率上行,投资者的交易热情高涨使得换手率上行。
2.波动率换手率同时下行。此状态往往发生在震荡市中,这个阶段市场的方向性不好判断,可能震荡下跌也可能震荡上涨。
3.波动率上行换手率下行。这时市场是典型的熊市特征,市场的下跌一方面会使得波动率上行,另一方面会使成交量萎缩,造成换手率下行
4.波动率下行换手率上行。这时市场表现也往往比较好,这个阶段经常是牛市的初期或者熊市之后的反弹;波动率和换手率同时下行。

研报分别采用双均线策略和布林带策略对牛熊指标和指数本身分别进行择时。结果表明,对于牛熊指标来说,采用双均线进行择时效果更好,对于指数本身来说,采用布林带进行择时效果更好。进一步比较,采用双均线的牛熊指标择时要好于采用布林带的指数本身择时。牛熊指标择时的结果较为稳健,在不同指数上的差异也比较小,捕捉到的上涨机会也都比较类似,而直接对指数择时相对来说会更受指数本身的特性影响,针对不同的指数应该采用不同的参数或者策略。因为牛熊指标本身的平滑性,对牛熊指标进行择时不容易出现假信号,择时策略交易次数相对较少,胜率较高。

2.研究环境

数据来源:Tushare

import tushare as ts
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False

3.指标构建

本研报通过波动率与换手率观察市场,利用波动率与换手率构造牛熊指标。并利用牛熊指标对市场进行判断。我们将先从单一的指标开始,研究波动率与换手率本身的变化特征,然后利用波动率与换手率对市场状态进行描述并分类,继而构造牛熊指标,并利用牛熊指标搭建择时策略。

3.1 数据获取

波动率指标的计算主要基于股票指数的收益率,是过去历史n个交易日收益率的标准差。采用历史波动率的特点是波动率的变化特征会受到参数n的影响。n越小,对短期的变化越敏感,其变化速度就会越快,n越大,波动率的变化就会越平滑,更能反映长期的趋势,这个特征与移动平均(MA)的特点是一致的。

日换手率指标为当天成交量占流通股本的比例,基于不同流通股本的算法会出现两种不同的换手率,一种是基于流通总股本计算的换手率,另一种是基于自由流通股本计算的换手率。本研报采用基于自由流通股本所计算的换手率。

3.1.1 数据获取的细节问题

(1)笔者使用范围内的量化平台有Tushare,Joinquant,Uqer。Joinquant上的finance.run_query()函数可以获得指数基于自由流通股本计算的换手率,Uqer上的DataAPI.MktIdxdEvalGet()函数可以获得指数基于流通股本的换手率。由于研报中的指标计算二者均有涉及,故选用Tushare作为数据源。

(2)Tushare调取换手率的数据要求用户400积分,数据从2004.01月开始,故本文缩小了研报的回测范围。Tushare的交易日数据为int类型且从大到小降序排列,并且Tushare平台有单次调取3000条数据的限制,这些在代码中均作了相应调整。

策略源代码

def get_data(security,begin_date,end_date):
    df_ts1 = pro.index_dailybasic(ts_code = security,
                                 start_date = begin_date,
                                 end_date = '20100101',
                                  #以2010-01-01作为中断点
                                 fields='trade_date,turnover_rate,turnover_rate_f')
    
    df_ts2 = pro.index_dailybasic(ts_code = security,
                                 start_date = '20100102',
                                 end_date = end_date,
                                 fields='trade_date,turnover_rate,turnover_rate_f')
    df_ts = df_ts2.append(df_ts1)
    
    df_ts = df_ts.set_index(['trade_date'])
    df_ts = df_ts.sort_index(ascending = True)
    
    df_a = pro.index_daily(ts_code = security,
                                 start_date = begin_date,
                                 end_date = '20100101',
                                 fields ='close,pct_chg')
    df_a.index = df_ts1.trade_date
    
    df_b = pro.index_daily(ts_code = security,
                                 start_date = '20100102',
                                 end_date = end_date,
                                 fields ='close,pct_chg')
    df_b.index = df_ts2.trade_date
    
    df_A = df_b.append(df_a)
    df_A = df_A.sort_index(ascending = True)
    print(df_A.index)
    
    df = pd.DataFrame(index = df_ts.index)
    df['close'] = df_A['close']
    df['pct_change'] = df_A['pct_chg']
    df['turnover_rate'] = df_ts['turnover_rate']
    df['turnover_rate_f'] = df_ts['turnover_rate_f']
    return df

3.2波动率指标历史特征

原研报中波动率指标采用移动平均的概念,分为60日波动率、120波动率、200波动率和250日波动率。我们重点关注长期限的历史波动率,因为我们希望指标能够描述一段时间的市场特征,而且最好是能够持续一段时间、反映市场涨跌结构的特征。
在这里插入图片描述

代码

df1 = df.copy()
plt.figure(figsize = (10,4),dpi = 100)#dpi 是像素设置
xticks = np.arange(0,df1.shape[0],int(df1.shape[0]/12))
xticklabel = pd.Series(df1.index[xticks])
col_list = ['60_vol','120_vol','200_vol','250_vol']
for i in col_list:
plt.plot(range(df1.shape[0]),df1[i],label = i)
plt.xlabel('Time')
plt.ylabel('Volitility')
plt.xticks(xticks,xticklabel,rotation = 30)
plt.title('上证综指不同参数下的历史波动率对比')
plt.legend(loc = 'best')
plt.show()

实际上,对于A股来说,波动率的上升并非单单来源于市场的下跌,经常出现的快速上涨也会使得波动率整体走高,2007年和2015年就是典型的市场上涨带动波动率上升的例子。但是市场上涨带来的波动率上升不会因为上涨的停止而停止,上涨之后的下跌将使得波动率进一步上升。也就是说,在一轮完整的上涨和下跌里面,下跌的波动大概率依然是高于上涨的情形的。

2011年至2014年的震荡市场似乎是一个例外,这是因为我们采用长期波动率的原因,这段时间的上涨下跌周期较多,长周期波动率并不能较好的捕捉短周期的涨跌情形。

代码

fig = plt.figure(figsize = (10,4),dpi = 100)
ax1 = fig.add_subplot(1,1,1)
ax1.plot(range(df1.shape[0]),df1['close'],color = 'r',label = 'Close')
ax1.legend(loc = 'upper left')
plt.xticks(xticks,xticklabel,rotation = 30)
ax2 = ax1.twinx()
ax2.plot(range(df1.shape[0]),df1['250_vol'],color = 'b',label = '250_vol')
ax2.legend(loc = 'best')
plt.show()

从2000年以后来看,市场波动率与市场走势的相关性是存在轮动规律的。2001年7月至2002年9月,波动率随着市场下跌逐步上升,2002年之后波动率开始回落,这时波动率与上证综指的相关性也开始一路走高。2004年5月之后,随着市场的下跌波动率再次走高,两者的相关性一路走低,从高度正相关变为高度负相关。2005年11月在市场逐步转暖后波动率也开始回落。2006年底,市场进入大牛市,波动率开始走高,在2007年底市场转为下跌后波动率继续走高,一直到2009年初市场开始反弹,波动率才开始下降。这次下降期十分漫长,一共持续了6年左右,2014年底市场重新进入大牛市,波动率再次开始上升。在2007年市场大牛市期间,波动率与市场指数的相关性先从负相关变为正相关,再从正相关变为了负相关。2015年的牛市期间,同样出现了这个现象。
在这里插入图片描述

代码

fig = plt.figure(figsize = (10,4),dpi = 100)
ax1 = fig.add_subplot(1,1,1)
ax1.plot(range(df1.shape[0]),df1.close,color = 'r',linewidth = 2,label = 'Close')
ax1.legend(loc = 'upper left')
plt.ylim(0,7000)
plt.xticks(xticks,xticklabel,rotation = 30)
ax2 = ax1.twinx()
ax2.plot(range(df1.shape[0]),corr1,color = 'b',linewidth = 2,label = '250日波动率与收盘价滚动1年相关性')
plt.ylim(-1,1.25)
ax2.legend(loc = 'upper right')
plt.title('上证综指与波动率的滚动相关系数')
plt.show()

3.3换手率指标历史特征

换手率是一定时间内市场中股票转手买卖的频率,是反映股票流动性强弱和交易活跃度的指标之一。成交量和换手率都是反映股票交易规模的指标,但前者是一个绝对数值概念,随着市场总股本的增加自然增长;后者是相对数值型指标,以百分比形式呈现出来,能够更好地在不同时间跨度上进行比较。

股票每日换手率为当天成交量占流通股本的比例,这里基于流通股本的算法就会出现两种不同的换手率,一种是基于流通总股本计算的换手率,另一种是基于自由流通股本计算的换手率。股票指数的换手率与单只股票的换手率类似,区别在于分子变成了成分股成交量的加总,分母变成了成分股流通股本的加总。同样根据流通股本选用流通总股本和自由流通股本的不同会出现两种换手率。

对比两种换手率可以发现,基于自由流通股本的换手率数值整体高于基于总流通股本的换手率,这个容易理解,因为计算换手率的时候两者分子相同,但是基于总流通股本的换手率无疑分母更大。从局部上看,两者的走势其实是一致的,都能够反映市场局部的流动性、活跃度的变化。但是从整体上来看,基于总流通股本的换手率的平均水平是在下降的,特别是2015年的牛市换手率水平不到2007年牛市换手率的一半。这一点并不能真实的反映市场情况,我们不认为2015年市场的活跃程度会低于2007年。相反,采用基于自由流通股本计算的换手率显示的交易活跃度并没有长期下降的趋势,并且2015年换手率的峰值与2007年相仿。这说明采用总流通股本计算的换手率对市场的交易活跃程度有所低估,特别是在2010年之后非自由流通股本占比越来越高的情况下。因此,从逻辑的顺畅性和指标前后的一致性上考虑,原研报倾向于采用自由流通股本计算的换手率进行后续研究。本文除特殊说明外,之后的换手率都指代的是采用自由流通股本计算的换手率。实际上,在后续划分市场状态和构建牛性指标开发择时策略的研究中,两种换手率的差异很小。
在这里插入图片描述

代码

fig = plt.figure(figsize = (14,4),dpi = 100)
# 画上图时不能使用sns.set(),否则画面呈现会出问题
ax1 = fig.add_subplot(1,2,1)
ax1.bar(range(df1.shape[0]),df1['turnover_rate'],color = 'r',label = '基于总流通股本的换手率')
ax1.legend(loc = 'upper center')
plt.xticks(xticks,xticklabel,rotation = 30)
ax2 = fig.add_subplot(1,2,2)
ax2.bar(range(df1.shape[0]),df1['turnover_rate_f'],color = 'r',label = '基于自由流通股本的换手率')
ax2.legend(loc = 'upper center')
plt.xticks(xticks,xticklabel,rotation = 30)
plt.show()

在本研报中,我们不关心换手率每日的数值,关心的是换手率整体的趋势,因此在处理中,我们借鉴移动平均的方法,来考察一段时间换手率趋势性的变化。采用了均线的处理之后,均线的参数的选取对换手率指标就会产生影响。同波动率类似,均线参数越小,对小波动的识别性越好,均线参数越大,对长趋势的识别越稳定。
在这里插入图片描述

代码

df_turnover = df.copy()
#60、120、200换手率以后用不到,故单独计算
df_turnover['60_turnover'] = pd.Series.rolling(df['turnover_rate_f'],window = 60,min_periods = 1).mean()
df_turnover['120_turnover'] = pd.Series.rolling(df['turnover_rate_f'],window = 120,min_periods = 1).mean()
df_turnover['200_turnover'] = pd.Series.rolling(df['turnover_rate_f'],window = 200,min_periods = 1).mean()
df_turnover['250_turnover'] = pd.Series.rolling(df['turnover_rate_f'],window = 250,min_periods = 1).mean()
#去掉除顶部框和右侧框
plt.rcParams['axes.spines.top'] = False
plt.rcParams['axes.spines.right'] = False
plt.figure(figsize = (11,4),dpi = 100)
xticks = np.arange(0,df1.shape[0],int(df1.shape[0]/12))
xticklabel = pd.Series(df1.index[xticks])
col_list = ['60_turnover','120_turnover','200_turnover','250_turnover']
for i in col_list:
    plt.plot(range(df1.shape[0]),df_turnover[i],label = i)
plt.xticks(xticks,xticklabel,rotation = 30)
plt.title('上证综指不同参数下的日均换手率')
plt.legend(loc = 'best')
plt.show()

换手率与指数走势呈现明显的正相关性,为了与波动率统一,我们依然观察250日均换手率与上证指数的关系。换手率与指数的走势整体上呈现出明显的正相关性,在技术分析中,价量是常被一起提及的指标,换手率本质上属于量的概念,在市场明显的上涨行情中,量也是明显上升的,这也可以直观的理解为牛市都是需要成交量来推动的。当然,由于我们采用了均线的方法,因此在一些关键拐点处确实会出现滞后的特征。
在这里插入图片描述

代码

plt.rcParams['axes.spines.right'] = True
df1['250_turnover'] = pd.Series.rolling(df['turnover_rate_f'],window = 250,min_periods = 1).mean()
fig = plt.figure(figsize = (10,4),dpi = 120)
ax1 = fig.add_subplot(1,1,1)
ax1.plot(range(df1.shape[0]),df1['close'],color = 'r',label = 'Close')
ax1.legend(loc = 'upper left')
plt.xticks(xticks,xticklabel,rotation = 30)
ax2 = ax1.twinx()
ax2.plot(range(df1.shape[0]),df1['250_turnover'],color = 'b',label = '250_vol')
ax2.legend(loc = 'best')
plt.title('上证综指与 250 日换手率')
plt.show()

3.4构建牛熊指标

3.4.1利用波动率与换手率对上证综指走势状态的划分

波动率和换手率同时上升期是典型的牛市特征,是市场交易热情和波动的同时放大;波动率上升、换手率下降是典型的熊市特征,此时市场交易热情下降同时波动放大;波动率下降、换手率上升,市场往往表现也较强,成交热情的上升伴随着市场波动的下降,往往出现在牛市前期或者反弹期;波动率和换手率同时下降,市场大体表现为震荡,但是方向性不明确。如此,我们以波动率和换手率构建投资时钟,或者叫市场状态坐标系,利用波动率与换手率就可以大致确定市场状态。

在这里插入图片描述在这里插入图片描述

代码

fig = plt.figure(figsize = (10,4),dpi = 100)
ax1 = fig.add_subplot(1,1,1)
ax1.plot(range(df1.shape[0]),df1.close,label = 'Close',color = 'purple')
plt.ylim(0,7000)
ax1.legend(loc = 'upper left')
plt.xticks(xticks,xticklabel,rotation = 30)
ax2 = ax1.twinx()
ax2.plot(range(df1.shape[0]),df1['250_vol']*2,label = '250_vol*2',color = 'b')
ax2.plot(range(df1.shape[0]),df1['250_turnover'],label ='turnover_rate_f_250',color = 'r')
plt.ylim(0,7)
ax2.legend(loc = 'best')
plt.title('波动率与换手率对上证综指走势状态的划分')
plt.show()

3.4.2量化牛熊指标

构建牛熊指标的方式非常简单:牛熊指标 = 250日波动率/250日换手率

如此构造出来的牛熊指标与指数本身的走势呈现明显的负相关。以上证综指为例,两者相关系数为-0.66(原研报为-0.67)。当牛熊指标上升时,市场往往处于下跌状态,牛熊指标下降时,市场往往处于上涨状态。

在这里插入图片描述

代码

#构建牛熊指标
df1['kernel'] = df1['250_vol']/df1['250_turnover']
fig = plt.figure(figsize = (10,4),dpi = 100)
ax1 = fig.add_subplot(111)
ax1.plot(range(df1.shape[0]),df1.close, label='close',color = 'r',linewidth = 2)
plt.legend(loc='upper left')
plt.ylim(0,7000)
plt.xticks(xticks,xticklabel,rotation = 30)
ax2 = ax1.twinx() 
ax2.plot(range(df1.shape[0]),df1.kernel, label='牛熊指标',color = 'b',linewidth = 2)
plt.ylim(0,2)
plt.legend(loc='best')
ax1.set_title("上证综指收盘价与其对应的牛熊指标")
plt.show()
corr = df.close.corr(df.kernel)
print('收盘价与牛熊指标相关系数为:',corr)
收盘价与牛熊指标相关系数为: -0.6613201866235457

牛市的波动率上行都是换手率上升所带来的结果,市场的波动放大是交易热情高涨所形成的。震荡市特征(波动率下行、换手率下行)在牛熊指标上的表现则不尽相同,部分出现上涨部分出现下跌,此时牛熊指标刚好弥补了震荡市无法判断市场方向的问题。当波动率和换手率都出现下行时,就需要比较两者的下行速度,若波动率的下行速度大于换手率的下行速度,说明市场趋于稳定的速度要大于交易热情衰退的速度,这时候牛熊指标下行,市场表现为上涨,典型如2017年的上涨。当波动率的下行速度小于换手率的下行速度,说明市场交易热情衰减的速度更快,牛熊指标表现为上升,市场发生下跌,典型如2011年、2012年的震荡下跌。因此牛熊指标很好的弥补了单一使用波动率、换手率对震荡市涨跌方向不好判断的情况,能够对市场择时起到关键的作用。

在构建牛熊指标时,我们选取的参数都相对较长,因此得到的牛熊指标对中长期趋势的识别效果较好。同时牛熊指标的平稳性较好,不会因为市场短期的变化就随之发生改变,不管是主动投资者还是运用量化方法的投资者,都可以借助于牛熊指标对市场进行择时判断。因为牛熊指标对中长期趋势的把握较好,指标更适合中长线资金,特别是做资产配置的投资者。

4.策略构建与回测可视化

借助于牛熊指标,我们可以设计相关的择时策略。在本研报中,为了显示牛熊指标的特性,我们不做过多的优化,仅仅比较两个常用的择时方法在牛熊指标和原始指数上的效果差异, 来显示牛熊指标的优势。

回测设计:针对上证综指、上证50、沪深300、中证500、万得全A五个代表性指数,分别计算其牛熊指标,分别采用双均线策略、布林带策略判断牛熊指标趋势,若牛熊指标趋势向上则对指数看空,若牛熊指标向下则对指数看多,同时采用两个策略对原始指数直接进行择时,比较择时结果。回测中,仅对指数进行做多操作,看空时空仓。不考虑交易费率。

1、双均线策略:计算标的的 20 日均线和 60 日均线,若 20 日均线自下而上穿过 60 日均 线,则对标的看多,若 20 日均线自上而下穿过 60 日均线,则对标的看空。

2、布林带策略:计算标的的 20 日均线和标的价格过去 20 日的标准差,以均线加上两个 标准差作为布林带上轨,以均线减去两个标准差作为布林带下轨。当标的突破上轨时,对 标的看多,当标的突破下轨时,对标的看空。

回测结论:利用牛熊指标构建量化择时策略普遍优于直接对指数择时

回测区间长度:Tushare的换手率数据只提供2004.01之后的,且各指数上市时间不同,故开始日期略有差异,但是截止时间均为 2019 年 9 月 5 日。上证综指、万得全 A 的回测起始时间为 2004 年 1 月 5 日;上证 50 的回测起始时间为 2005 年 1 月 12 日;沪深 300 的回测起始时间为 2006 年 4 月 19 日;中证 500 的回测起始时间为 2008 年 1 月 22 日。

4.1 双均线策略

双均线策略是技术指标中常用的一种策略,属于经典策略之一,策略运用短均线和长均线的相对位置对市场进行判断。应用双均线择时对牛熊指标和指数收盘价分别择时的结果显示,在牛熊指标上操作的效果要明显好于直接对指数择时,采用牛熊指标择时的收益率和胜率均明显提升,交易次数显著下降。

4.1.1上证综指双均线策略

在这里插入图片描述

策略源代码

#双均线牛熊指标
def Calc_netv(df):
    df1 = df.copy()
    #为方便使用loc
    df1 = df1.reset_index()
    
    df1['kernel_20'] = pd.Series.rolling(df1.kernel,window = 20,min_periods = 1).mean()
    df1['kernel_60'] = pd.Series.rolling(df1.kernel,window = 60,min_periods = 1).mean()
    df1['position'] = 0
    df1['flag'] = 0
    position1 = 0
    
    for i in range(df.shape[0]-1):
        if df1.loc[i,'kernel_20'] <  df1.loc[i,'kernel_60'] and position1 == 0:
            df1.loc[i,'flag'] = 1
            df1.loc[i+1,'position'] = 1
            position1 = 1
        elif df1.loc[i,'kernel_20'] >  df1.loc[i,'kernel_60'] and position1 == 1:
            df1.loc[i+1,'position'] = 0
            df1.loc[i,'flag'] = -1
            position1 = 0
        else:
            df1.loc[i+1,'position'] = df1.loc[i,'position']
    df1['net_value'] = (1+(df1['pct_change']/100)*df1.position).cumprod()
        
    return df1

指数本身择时代码

def clac_itself(df):
    #指数本身择时所有回测都需要,单独提出
    df_itself = df.copy()
    df_itself = df_itself.reset_index()
    df_itself['ma_20'] = pd.Series.rolling(df_itself['close'],window = 20,min_periods = 1).mean()
    df_itself['ma_60'] = pd.Series.rolling(df_itself['close'],window = 60,min_periods = 1).mean()
    df_itself['position'] = 0
    df_itself['flag'] = 0
    position2 = 0
    for i in range(df_itself.shape[0]-1):
        if df_itself.loc[i,'ma_20'] >  df_itself.loc[i,'ma_60'] and position2 == 0:
            df_itself.loc[i,'flag'] = 1
            df_itself.loc[i+1,'position'] = 1
            position2 = 1
        elif df_itself.loc[i,'ma_20'] <  df_itself.loc[i,'ma_60'] and position2 == 1:
            df_itself.loc[i+1,'position'] = 0
            df_itself.loc[i,'flag'] = -1
            position2 = 0
        else:
            df_itself.loc[i+1,'position'] = df_itself.loc[i,'position']
    df_itself['net_value'] = (1+(df_itself['pct_change']/100)*df_itself.position).cumprod()
    return df_itself

画图代码

def plot_net(df_signal,df_itself):
    fig = plt.figure(figsize = (10,4),dpi = 100)
    x = range(df_signal.shape[0])
    
    plt.plot(x,df_signal['net_value'],label = '牛熊指标择时',color = 'r')
    plt.plot(x,df_itself['net_value'],label = '指数本身择时',color = 'm')
    plt.plot(x,df_signal['close']/df_itself.close[0],label = '指数',color = 'c')
    
    xlab = range(0,df_itself.shape[0],int(df_itself.shape[0]/12))
    plt.xticks(xlab,df_itself.loc[xlab,'trade_date'],rotation = 30)
    plt.legend(loc = 'best')
    plt.show()

在上证综指上来看,对牛熊指标使用双均线择时之后在指数上进行操作的择时收益率比较可观,年化收益15.41%,夏普比率0.9190,明显好于直接对指数进行择时,也明显好于指数表现。使用双均线系统对牛熊指标进行择时相比于对指数直接择时,胜率有明显的提高。特别是在2011年至2013年、2016年至2017年这种震荡市,采用牛熊指标进行判断的优势很大。
在这里插入图片描述

注:策略绩效指标中,做多胜率计算的是做多交易中赚钱的比例,没有考虑空仓情况下错 过的上涨行情,交易次数中统计的是单边交易次数。

策略源代码

统计指标

def calc_statistic(df,index = False):
    
    total_return = df.loc[df.shape[0]-1,'net_value']-df.net_value[0]
    annual_return = (total_return+1)**(1/(df.shape[0]/250))-1
    annual_return_str = format(annual_return*100,'.2f')+'%'
    
    annual_vol = (df['net_value'].pct_change(1).fillna(0)).std()*250**(0.5)
    annual_vol_str = format(annual_vol*100,'.2f') + '%'
    
    #原研报中直接采用年化收益率闭上年化波动率,未减去无风险利率
    sharp_ratio = annual_return/annual_vol
    sharp_ratio = format(sharp_ratio,'4f')
    
    df['high_level'] = pd.Series.rolling(df.net_value,window = df.shape[0],min_periods=1).max()
    max_drawdown = ((df.net_value - df['high_level'] )/df['high_level'] ).min()
    #输出为百分比
    max_drawdown_str = format(max_drawdown*100,'.2f') + '%'
    
    sell_trade = np.array(df[df['flag'] < 0].net_value)
    buy_trade = np.array(df[df['flag'] > 0].net_value)
    
    if df[df['flag']!=0]['flag'].iloc[0] < 0:
        buy_trade = np.insert(buy_trade,0,df['net_value'].iloc[0])
    if df[df['flag']!=0]['flag'].iloc[-1] == 1:
        sell_trade = np.append(sell_trade,df['net_value'].iloc[-1])
        
    trade_pct = (sell_trade - buy_trade)/buy_trade
    
    #做多胜率
    trade_winrate = len(trade_pct[trade_pct>0])/len(trade_pct)
    trade_winrate_str = format(trade_winrate*100,'.2f') + '%'
    #盈亏比
    if trade_winrate == 1:
        profit_loss_ratio = 'all_win'
    else:
        profit_loss_ratio = trade_winrate/(1-trade_winrate)
        profit_loss_ratio = format(profit_loss_ratio,'.4f')
    
    #交易次数
    trade_count = len(buy_trade)*2
    #交易频率
    trade_frequency = df.shape[0]/trade_count
    trade_frequency = np.round(trade_frequency)
    
    statistic_dict = {'年化收益':annual_return_str,'年化波动率':annual_vol_str,'夏普比率':sharp_ratio,'最大回撤':max_drawdown_str,
                     '做多胜率':trade_winrate_str,'交易盈亏比':profit_loss_ratio,'交易次数':trade_count,'平均交易频率':trade_frequency}
    
    df_statistic = pd.DataFrame([statistic_dict])
    
    if index:
        df_statistic.index = [index]
    
    return df_statistic
    

指数本身指标

def calc_index(df,index = False):
    #计算指数本身指标(非择时)
    total_return_index = df['close'].iloc[-1]/df['close'].iloc[0] - 1
    annual_return_index = (total_return_index+1)**(1/(df.shape[0]/250))-1
    annual_return__index_str = format(annual_return_index*100,'.2f')+'%'
    annual_vol_index = (df['close'].pct_change(1).fillna(0)).std()*250**(0.5)
    annual_vol_index_str = format(annual_vol_index*100,'.2f') + '%'
    sharp_ratio_index = annual_return_index/annual_vol_index
    sharp_ratio_index_str = format(sharp_ratio_index,'4f')
    df['high_level'] = pd.Series.rolling(df.close,window = df.shape[0],min_periods=1).max()
    max_drawdown_index = ((df.close - df['high_level'] )/df['high_level'] ).min()
    max_drawdown_index_str = format(max_drawdown_index*100,'.2f') + '%'
    statistic_dict = {'年化收益':annual_return__index_str,'年化波动率':annual_vol_index_str,'夏普比率':sharp_ratio_index_str,
                      '最大回撤':max_drawdown_index_str,'做多胜率':None,'交易盈亏比':None,'交易次数':None,'平均交易频率':None}
    
    if index:
        df_index = pd.DataFrame([statistic_dict],index = [index])        
    
    return df_index

4.1.2 上证50双均线策略

对于上证50指数的主要4个上升区间,采用牛熊指标择时对09年、15年、17年三次捕捉较好,对于07年的牛市,牛熊指标仅捕捉到了前半程。直接对指数进行择时则对07年、09年两次主要的上升区间把握较好,对于15年和17年则相对较弱。而在2011年至2014年的震荡市,牛熊指标构造的择时策略净值维持在了较高水平,没有发生持续性的下跌,而直接对指数择时在这段时间出现了持续性的下跌。从策略的绩效表现来看,采用牛熊指标进行择时胜率明显提升,交易次数明显下降,年化收益提升至15.31%,夏普比率也提升到了一个较为可观的水平。

在这里插入图片描述在这里插入图片描述

4.1.3 沪深300双均线策略

牛熊指标双均线择时策略在沪深 300 指数上的表现与上证 50 类似,对于 2007 年的牛市,仅捕捉到了前半程,但是对于后面的几次机会把握都比较好,在震荡市中的表现更为突出。牛熊指标双均线策略的胜率与盈亏比都高于指数自身双均线择时策略。年化收益与夏普比率上的表现都比较可观。
在这里插入图片描述在这里插入图片描述

4.1.4 中证500双均线策略

在中证 500 指数上,牛熊指标双均线择时策略相比于自身择时策略而言明显更优,胜率和盈亏比都有所提高,交易频率也大大下降。

在这里插入图片描述在这里插入图片描述

4.2 布林带策略

与双均线策略类似,我们采用布林带策略对每个指数对应的牛熊指标和其自身分别进行择时,对比两者的择时效果。布林带是技术指标择时中另一类策略的代表,是通道类策略中的经典策略。

采用布林带策略对牛熊指标进行择时和指数本身择时,在收益率上差异不大,甚至在沪深300上直接对指数进行择时的收益要好于对牛熊指标进行择时,但是从净值的走势来看,采用牛熊指标进行择时稳健性更好,其交易次数更低,在市场行情不好时,策略选择空仓,而不是频繁交易使净值出现下跌。对于投资者来说是一个更容易接受的策略。

4.2.1上证综指布林带策略

以上证综指为标的的两种择时策略收益端基本表现一致,净值走势也较为一致,只是直接对指数本身择时的策略交易次数更多,完全空仓的时间更短,净值的平稳性稍稍弱于对牛熊指标择时的策略。
在这里插入图片描述在这里插入图片描述

策略源代码

布林带-牛熊

def bollinger_netv(df):
    df1 = df.copy()
    df1 = df1.reset_index()
    
    df1['ma_20'] = pd.Series.rolling(df1.kernel,window = 20,min_periods = 1).mean()
    df1['vol_20'] = pd.Series.rolling(df1.kernel,window = 20, min_periods = 1).std()
    df1['bullin_up'] = df1['ma_20'] + df1['vol_20']*2
    df1['bullin_down'] = df1['ma_20'] - df1['vol_20']*2
    
    df1['position'] = 0
    df1['flag'] = 0
    position = 0
    
    for i in range(df1.shape[0]-1):
        if df1.loc[i,'kernel'] <  df1.loc[i,'bullin_down'] and position == 0:
            df1.loc[i,'flag'] = 1
            df1.loc[i+1,'position'] = 1
            position = 1
        elif df1.loc[i,'kernel'] >  df1.loc[i,'bullin_up'] and position == 1:
            df1.loc[i+1,'position'] = 0
            df1.loc[i,'flag'] = -1
            position = 0
        else:
            df1.loc[i+1,'position'] = df1.loc[i,'position']
    df1['net_value'] = (1+(df1['pct_change']/100)*df1.position).cumprod()
    
    return df1  

布林带-指数择时

def bollinger_itself(df):
    df1 = df.copy()
    df1 = df1.reset_index()
    
    df1['ma_20'] = pd.Series.rolling(df1.close,window = 20,min_periods = 1).mean()
    df1['vol_20'] = pd.Series.rolling(df1.close,window = 20, min_periods = 1).std()
    df1['bullin_up'] = df1['ma_20'] + df1['vol_20']*2
    df1['bullin_down'] = df1['ma_20'] - df1['vol_20']*2
    
    df1['position'] = 0
    df1['flag'] = 0
    position = 0
    
    for i in range(df1.shape[0]-1):
        if df1.loc[i,'close'] >  df1.loc[i,'bullin_up'] and position == 0:
            df1.loc[i,'flag'] = 1
            df1.loc[i+1,'position'] = 1
            position = 1
        elif df1.loc[i,'close'] <  df1.loc[i,'bullin_down'] and position == 1:
            df1.loc[i+1,'position'] = 0
            df1.loc[i,'flag'] = -1
            position = 0
        else:
            df1.loc[i+1,'position'] = df1.loc[i,'position']
    df1['net_value'] = (1+(df1['pct_change']/100)*df1.position).cumprod()
    return df1

画图

def plot_net(df_signal,df_itself,index = False):
    fig = plt.figure(figsize = (10,4),dpi = 100)
    x = range(df_signal.shape[0])
    
    plt.plot(x,df_signal['net_value'],label = '牛熊指标择时',color = 'r')
    plt.plot(x,df_itself['net_value'],label = '指数本身择时',color = 'm')
    plt.plot(x,df_signal['close']/df_itself.close[0],label = index,color = 'c')
    
    xlab = range(0,df_itself.shape[0],int(df_itself.shape[0]/12))
    plt.xticks(xlab,df_itself.loc[xlab,'trade_date'],rotation = 30)
    plt.legend(loc = 'best')
    
    if index:
        plt.title(index + '布林带策略净值对比')
    plt.show()             

4.2.2 上证50布林带策略

考察上证50指数上布林带两种策略的应用,直接对指数本身择时虽然收益率较高,但是其收益主要来源于2009年以前,之后接近六年的时间一直在回撤,这种策略在实际应用中是较难使用的,投资者需要忍受漫长的回撤期。对牛熊指标择时的策略虽然前几年收益没有特别高,但是整体表现更平稳,09年以后表现明显好于直接对指数择时的策略,而且胜率也更高。

在这里插入图片描述在这里插入图片描述

4.2.3 沪深300布林带策略

应用到沪深 300 上,对牛熊指标择时的策略收益要弱于直接对指数择时的策略。主要原因是 2007 年的牛市对牛熊指标择时的策略没有能够把握住。2007 年之后,实际上还是对牛熊指标择时的策略表现更为稳健。【有点嗯吹了属于是】
在这里插入图片描述在这里插入图片描述

4.2.4 中证500布林带策略

中证 500 上采用布林带对牛熊指标择时和直接对指数择时的效果差异不是很大,主要的机会都有所把握,但是准确率上还是牛熊指标择时效果更好一点,所以择时收益率更高。
在这里插入图片描述在这里插入图片描述

5.总结

本文基本复现了【华泰证券-波动率与换手率构造牛熊指标】的研究内容,同时得到了与原研报相近的结果。

1.因为软硬件条件的限制,压缩了原研报的样本空间。

2.没有考虑交易成本。原研报同时给出了在不同交易成本下各策略的结果,净值有略微的下降。

3.由于种种原因,没有对WIND全A指数进行回测

系列文章
https://blog.csdn.net/weixin_43915798/category_10971751.html

  • 11
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值