研报复现系列(六)【国泰君安】基于CCK模型的股票市场羊群效应研究

前言

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

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

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

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

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

本文作者:

徐鹏宇 山东大学(威海)数学与统计学院 pengyu_xu0607@163.com

1.研报概述

研究目的

参考国泰君安证券研报《20181128-基于CCK模型的股票市场羊群效应研究》,对市场上存在的个别股票的涨跌引起相关股票收益率联动的现象进行分析探究。根据研报构建CCK模型,并进行改良,寻找更多联动信号,并正确分析市场趋势。

研究思路

1、计算市场回报率并进行改良

2、确定羊群效应显著性水平的衡量标准

3、选取研报中若干数据进行回测

2.研究环境

JointQuant

Tushare

因为数据接口不同,两大平台在获取一些数据各有优势。本人第一次进行研报复现,混用两种数据接口,择优使用

from numpy.core.fromnumeric import product
from numpy.core.numeric import NaN
from jqdatasdk import *
import jqdatasdk as jq
import tushare as ts
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from datetime import *
from pandas.io.pytables import performance_doc
import statsmodels.api as sm
from sklearn.preprocessing import scale
import warnings
warnings.filterwarnings('ignore')
plt.rcParams['font.sans-serif'] = ['SimHei']  # 用来正常显示中文标签
plt.rcParams['axes.unicode_minus'] = False  # 用来正常显示负号

3.研报复现

3.1数据获取

本文数据来源于聚宽JoinQuant以及Tushare。用到的接口名和功能如下表所示。具体参数请查阅官方API文档。

聚宽接口功能
get_price获取数据、是否停牌、涨跌停价
get_trade_days获取一段时间内的交易日列表
get_extras查询特定股票是否是ST股
get_index_stocks获取指数成分股
get_all_securities获取某日所有股票
Tushare接口功能
daily获取股票单日数据
trade_cal获取交易日列表

3.2构建市场回报率

对于接下来要使用的A股综合市场回报率,研报中使用的为国泰安数据库,我们考虑用免费获得的数据进行代替。

流通市值加权平均

首先,我们想到的是使用流通市值加权平均的方法计算A股市场总收益率:

上证A股指数收益率上证A股总流通市值占比
+
深证A股指数收益率
深证A股总流通市值占比

总流通市值为当日指数成分股流通市值之和

def get_CriValue(exchange_code, start_date, end_date, count=3000):
    '''
    获取流通市值
    参数:
    exchange_code 指数代码,
    start_date 开始日期
    end_date 结束日期
    count 最多返回值个数,是聚宽规定的一次获取最多个数
    返回值:dataframe类型,每日流通市值
    '''
    CriValue = jq.finance.run_query(
            query(finance.STK_EXCHANGE_TRADE_INFO.circulating_market_cap,
                finance.STK_EXCHANGE_TRADE_INFO.date).filter(
                jq.finance.STK_EXCHANGE_TRADE_INFO.exchange_code==exchange_code,
                jq.finance.STK_EXCHANGE_TRADE_INFO.date<=end_date,
                jq.finance.STK_EXCHANGE_TRADE_INFO.date>=start_date).limit(count))
    CriValue.set_index('date', inplace=True)
    return CriValue
# 计算2017年一年的Rm
SH_CriValue = get_CriValue('322001', '2017-01-01', '2017-12-31')
SZ_CriValue = get_CriValue('322004', '2017-01-01', '2017-12-31')
###运用流通市值加权平均法计算Rm###
sz = pd.DataFrame(SZ_CriValue.circulating_market_cap)
sh = pd.DataFrame(SH_CriValue.circulating_market_cap)
real_market = pd.concat([sz,sh],axis=1)
real_market.columns = ['深证','上证']
real_market.index = SH_CriValue.index
real_market['all'] = real_market['深证'] + real_market['上证']
# print(real_market.tail())
SH_market_return = jq.get_price('000001.XSHG',start_date='2017-01-01',end_date='2017-12-31',fields='close'
                             ).pct_change(1).fillna(0)
SZ_market_return = jq.get_price('399106.XSHE',start_date='2017-01-01',end_date='2017-12-31',fields='close'
                             ).pct_change(1).fillna(0)
real_market['R深证'],real_market['R上证'] = SZ_market_return,SH_market_return
Rm = (real_market['深证']/real_market['all'] * real_market['R深证']
                               )+(
      real_market['上证']/real_market['all'] * real_market['R上证'])

由于该方法构建的市场收益率占用内存过高,需要很多次查询,新手账号暂且没有这么多积分,所以寻求更简单的方法。

简化的综合市场回报率

去掉加权平均,计算公式=(上A指数收益率+深A指数收益率)/2

def get_Rm_simple_cont(start_date, end_date):
    '''
    功能:计算沪深A股收益率均值作为市场均值
    连续时间
    参数:start_date, end_date
    返回值:list类型,A股收益率
    '''
    SH_market_return = jq.get_price('000002.XSHG',start_date=start_date,end_date=end_date,fields=['close', 'pre_close']
                                ).fillna(0)
    SH_market_return = (SH_market_return['close']-SH_market_return['pre_close']) / SH_market_return['pre_close']
    SZ_market_return = jq.get_price('399107.XSHE',start_date=start_date,end_date=end_date,fields=['close', 'pre_close']
                                ).fillna(0)
    SZ_market_return = (SZ_market_return['close']-SZ_market_return['pre_close']) / SZ_market_return['pre_close']
    Rm_simple_cont = (SH_market_return+SZ_market_return)/2
    return list(Rm_simple_cont)

该方法构建的综合市场回报率与国泰君安的数据库基本一致,因此下文使用该方法。此外,我们给这个函数稍作变化:
增加了一个新的参数time_interval,适用于计算若干天内市场回报率的指数平均。

增加了参数code,计算其他指数的收益率

def get_Rm_simple2(start_date,end_date,time_interval=None,code = None):
    '''
    功能:计算沪深A股收益率均值作为市场均值,也可以计算其他指数的收益率
    参数:start_date,end_date,
        time_interval默认为None,计算每日收益率,当为int型数字时,
        计算前time_interval天的指数平均收益率
    返回值:list类型,A股收益率
    '''
    if not code:
        return0 = jq.get_price(['000002.XSHG','399107.XSHE'],start_date=start_date,end_date=end_date,fields=['close', 'pre_close'],
                                ).fillna(0)
SH_market_return = return0.loc[return0['code']=='000002.XSHG'].reset_index(drop=True)
SZ_market_return = return0.loc[return0['code']=='399107.XSHE'].reset_index(drop=True)
if time_interval:
return1 = jq.get_price(['000002.XSHG','399107.XSHE'],count = time_interval,end_date=start_date,fields=['close', 'pre_close'],
).fillna(0)
SH_market_return1 = return1.loc[return1['code']=='000002.XSHG'].reset_index(drop=True)
SH_market_return1.drop([len(SH_market_return1)-1],inplace=True)
SH_market_return = SH_market_return1.append(SH_market_return).reset_index(drop = True)
SZ_market_return1 = return1.loc[return1['code']=='399107.XSHE'].reset_index(drop=True)
SZ_market_return1.drop([len(SZ_market_return1)-1],inplace=True)
SZ_market_return = SZ_market_return1.append(SZ_market_return).reset_index(drop = True)
SH_market_return = (SH_market_return['close']-SH_market_return['pre_close']) / SH_market_return['pre_close']
SZ_market_return = (SZ_market_return['close']-SZ_market_return['pre_close']) / SZ_market_return['pre_close']
Rm_simple = list((SH_market_return+SZ_market_return)/2)
else:
return0 = jq.get_price(code,start_date=start_date,end_date=end_date,fields=['close', 'pre_close'],
).fillna(0).reset_index(drop=True)
if time_interval:
return1 = jq.get_price(code,count = time_interval,end_date=start_date,fields=['close', 'pre_close'],
).fillna(0)
return1 = return1.reset_index(drop=True)
return1.drop([len(return1)-1],inplace=True)
return0 = return1.append(return0).reset_index(drop = True)
return0 = (return0['close']-return0['pre_close']) / return0['pre_close']
Rm_simple = list(return0)
if time_interval:
Rm_simple.append(0)
Rm_simple = [(np.prod(1+np.array(Rm_simple[i-time_interval:i])))**(1/time_interval)-1 for i in range(time_interval, len(Rm_simple))]
return Rm_simple

3.3识别羊群效应

Chang, Cheng & Khorana首先通过以下推导证明了理性情况下,由于个股对市场风险的敏感程度不同,市场收益率Rm剧烈波动时,组合收益率相对于Rm的离散程度会线性增加:

定义股票组合在t时刻的截面绝对离散度CSAD

C S A D t = 1 N ∑ 1 N ∣ R i , t − R m , t ∣ CSAD_{t} = \frac{1}{N}\sum_{1}^{N}{\left| R_{i,t}-R_{m,t} \right|} CSADt=N11NRi,tRm,t

根据CAPM,导出

E ( C S A D t ) = 1 N ∑ i = 1 N ∣ β i − β m ∣ E ( R m , t − γ 0 ) E(CSAD_{t})=\frac{1}{N}\sum_{i=1}^{N}{\left| \beta_{i}-\beta_{m} \right|}E(R_{m,t}-\gamma_{0}) E(CSADt)=N1i=1NβiβmE(Rm,tγ0)

可以看出无羊群效应时CSAD与Rm关系为线性正相关。

Chang, Cheng & Khorana构造了如下回归,根据回归中Rm,t系数b**是否显著为负判断是否存在羊群效应:b显著时,说明C S A D和Rm的关系为非线性;b**为负时,如下图所示,随Rm增大,离散度会减速上升或加速下降。

为了计算股票组合的截面绝对离散度CSAD,首先需要得到股票成分股。该函数原创于我们的研报复现第一篇。

样本空间:沪深300成分股,剔除 ST 股和上市未满 60 日的新股,以及涨停、停牌、跌停(若按照原研报为每日的正常交易股票,但数据量过多,受硬件限制本文缩小了样本范围,只在180天更新一次指数成分股)

def get_stocks(date,index=None):
'''
功能:
根据日期,获取该日满足交易要求的股票相关数据,即剔除ST股、上市未满60天、停牌、跌涨停股
参数:
date,日期
index,指数代码,在特定指数的成分股中选股。缺省时选股空间为全部A股
返回:
DataFrame类型,索引为股票代码,同时包含了价格数据,方便后续使用
'''
stocks = jq.get_all_securities(
types=['stock'],
date=date
)#该日正在上市的股票

if index:#特定成分股
stock_codes = jq.get_index_stocks(index,date=date)#成分股
stocks = stocks[stocks.index.isin(stock_codes)]

#上市日期大于60个自然日
# start_date 为 [datetime.date] 类型
stocks['start_date']=pd.to_datetime(stocks['start_date'])
date = datetime.strptime(date,'%Y-%m-%d').date()
date = pd.to_datetime(date)
stocks['datedelta'] = date - stocks['start_date']
stocks = stocks[stocks['datedelta'] > timedelta(days=60)]

#是否是ST股
stocks['is_st'] = jq.get_extras(
info='is_st',
security_list=list(stocks.index),
count=1,
end_date=date
).T

#涨停、跌停、停牌
stocks_info = jq.get_price(
security = list(stocks.index),
fields=['close','high','low','high_limit','low_limit','paused'],
count=1,
end_date=date,
panel=False
).set_index('code').drop('time',axis=1)

stocks['price'] = stocks_info['close']#顺便保存价格,方便后续运算
stocks['paused'] = stocks_info['paused'] == 1#是否停牌
stocks['high_stop'] = stocks_info['high'] >= stocks_info['high_limit']#涨停
stocks['low_stop'] = stocks_info['low'] <= stocks_info['low_limit']#跌停
stocks = stocks[~(stocks['is_st'] | stocks['paused'] | stocks['high_stop'] | stocks['low_stop'])]
return stocks

为了同时使用聚宽和Tushare数据,我们使用了两个格式转换的小函数

# ts日期转聚宽日期
def ts2jq_date(tsdate):
'''
tsdate必须是list
输出list
'''
date=['-'.join([i[0:4], i[4:6], i[6:]]) for i in tsdate]
return date
# 聚宽日期转ts日期只需要date.replace('-', '')删除'-'即可
# 聚宽股票代码转ts股票代码
def jq2ts_code(jqcode):
'''
输入聚宽的股票代码(list)
返回tushare股票代码(list)
'''
tscode = [i.replace('XSHE', 'SZ').replace('XSHG', 'SH') for i in jqcode]
return tscode

接下来可以根据公式计算CSAD了

def cal_CSAD_cont(start_date, end_date, date_interval, index):
'''
为了减少运行时间,股票不能每日筛选
date_interval:股票池更新间隔,int
'''
CSAD = pd.DataFrame([])
start_date_ts = start_date.replace('-', '')
end_date_ts = end_date.replace('-', '')
trade_days = pro.trade_cal(start_date=start_date_ts, end_date=end_date_ts)
trade_days = trade_days[trade_days['is_open']==1]['cal_date']
trade_days = list(trade_days)
trade_days = ts2jq_date(trade_days)
dates_update = trade_days[::date_interval]
CSAD['cal_date'] = trade_days
csad = []
stocks_rm = get_Rm_simple2(start_date, end_date)
for i in range(len(trade_days)):
if trade_days[i] in dates_update:  #如果在dates_update就更新stocks
stocks = get_stocks(trade_days[i], index)
stocks = list(stocks.index)
stocks = jq2ts_code(stocks)
ts_date = trade_days[i].replace('-', '')
ri = pro.daily(trade_date=ts_date, fields='ts_code,pct_chg')
ri = ri[ri.ts_code.isin(stocks)].drop('ts_code',axis=1)
csadi = float(np.mean(abs(ri-stocks_rm[i])))
csad.append(csadi)
CSAD['CSAD'] = csad
CSAD['rm'] = stocks_rm
CSAD.set_index('cal_date')
return CSAD

对CSAD和综合市场收益率Rm进行OLS拟合,计算拟合的二次项系数beta2以及p值

def cal_beta(start_date, end_date, date_interval, index):
# 返回start_date22天后的beta
# 想要返回从start_date后的beta请使用
# real_date = get_trade_day_before(22,start_date)
# start_date = real_date
CSAD=cal_CSAD_cont(start_date, end_date, date_interval, index)
x1=[]
x2=[]
p1 = []
p2 = []
rm_mean = []
for i in range(len(CSAD))[22:]:
x=CSAD.iloc[i-22:i, 2] #pd.ix方法被1.0.0版本移除了
y=CSAD.iloc[i-22:i, 1]
X = np.column_stack((x**2, x))
X = sm.add_constant(X)
model = sm.OLS(y,X)
results = model.fit()
x1.append(results.params[1])
x2.append(results.params[2])
p1.append(results.pvalues[1])
p2.append(results.pvalues[2])
meanrm = np.mean(x)
rm_mean.append(meanrm)
beta = pd.DataFrame([])
beta['beta1']=x1
beta['pvalue1']=p1
beta['beta2']=x2
beta['pvalue2']=p2
beta['date']=list(CSAD.cal_date[22:])
beta['rm']=rm_mean
beta.set_index('date',inplace = True)#默认的drop=True失效了是怎么回事
return beta

以沪深300为例

为了方便取用,我们将cal_beta( )的返回值保存于本地的excel中。然后绘制beta2的折线走势图以及p值的折线走势图。

HS300_beta = cal_beta('2007-01-01','2019-01-01',180, '000300.XSHG')
HS300_beta.to_excel(excel_writer='beta.xlsx', sheet_name='sheet_1')
# 绘制beta2的p值
df1 = pd.read_excel('beta.xlsx')
print(df1)
fig,ax=plt.subplots(figsize=(13,8))
plt.plot(df1.date,df1.pvalue2,'b',linewidth = 1)
plt.grid(False)
plt.title('07年1月1日至19年1月β2的p值折线走势图',fontsize=20)
plt.hlines(y=0.05,xmin=df1.date[0],xmax=df1.date[len(df1)-1],color='red',linewidth = 1,alpha = 0.5)
first_legend = plt.legend(['pvlaue-β2'],fontsize=15)
ax1 = plt.gca().add_artist(first_legend)
red_patch = mpatches.Patch(color = 'red',label = '0.05分界线',linewidth = 1,alpha = 0.5)
plt.legend(handles = [red_patch],loc = 3,fontsize=15)
plt.xlabel('时间',fontsize=15)
plt.ylabel('pvalue',fontsize=15)
for label in ax.xaxis.get_ticklabels():
# label is a Text instance
label.set_color('red')
label.set_rotation(45)
label.set_fontsize(10)
for label in ax.get_xticklabels():
label.set_visible(False)
for label in ax.get_xticklabels()[::180]:
label.set_visible(True)
plt.tight_layout()
ax.tick_params(bottom=False,top=False,left=False,right=False)  #移除全部刻度线
plt.show()
fig,ax=plt.subplots(figsize=(13,8))
plt.plot(df1.date,df1.beta2,'r')
plt.grid(False)
plt.title('07年1月1日至19年1月β2的折线走势图',fontsize=20)
plt.legend(['β2'],fontsize=15)
plt.xlabel('时间',fontsize=15)
plt.ylabel('β2数值',fontsize=15)
for label in ax.xaxis.get_ticklabels():
# label is a Text instance
label.set_color('red')
label.set_rotation(45)
label.set_fontsize(10)
for label in ax.get_xticklabels():
label.set_visible(False)
for label in ax.get_xticklabels()[::180]: #每180个刻度显示一个
label.set_visible(True)
plt.tight_layout()
ax.tick_params(bottom=False,top=False,left=False,right=False)  #移除全部刻度线
plt.show()

在这里插入图片描述

方法一

通常的ols拟合,当p值小于0.05时,认为beta2显著。但是可以看出该方法给出的开仓信号过多。一个方法是将临界的p值减小,例如减小到0.001
在这里插入图片描述

# 筛选出显著羊群效应即β^2显著的,p值<0.001
df1 = pd.read_excel('beta.xlsx')
pvalues = df1.pvalue2
rm22_mean = df1.rm
signal_up = [rm22_mean[i]>0 and pvalues[i]<0.001 for i in range(len(pvalues))]
signal_down  = [rm22_mean[i]<0 and pvalues[i]<0.025 for i in range(len(pvalues))]
print('上涨时置信水平超过99.9%的信号出现了{}次,\n\
下跌时置信水平超过97.25%的信号出现了{}次,\n\
共出现信号{}次\n'.format(sum(signal_up),sum(signal_down),sum(signal_up)+sum(signal_down)))

# 可视化
start_date = df1.date[0]
end_date = list(df1['date'])[-1]
###信号坐标##
R_300 = jq.get_price('000300.XSHG',start_date=start_date,end_date=end_date,fields=['low','close'])
fig = plt.subplots(figsize=(13,8))
plt.plot(R_300.close,'b')
plt.grid(True)
plt.title('单侧置信区间下限0.1%时的β信号分布图',fontsize=20)
Y = R_300.close
for i in range(len(signal_up)):
if signal_up[i]:
loc = int(Y[i])
plt.vlines(Y.index[i], loc-400,loc+400, color='red',linewidth = 1,alpha = 0.5)
first_legend = plt.legend(['000300.XSHG'],fontsize=20)
ax = plt.gca().add_artist(first_legend)
blue_patch = mpatches.Patch(color = 'red',label = '买入信号',linewidth = 1,alpha = 0.5)
plt.legend(handles = [blue_patch],loc = 4,fontsize=20)
plt.xlabel('时间',fontsize=15)
plt.ylabel('沪深300指数收盘价',fontsize=15)
plt.show()

上涨时置信水平超过99.9%的信号出现了145次,
下跌时置信水平超过97.25%的信号出现了194次,

共出现信号339次

在这里插入图片描述

可以看出该方法给出的信号纯度并不好。在08年的峰顶给出了买入信号,在14-15年的熊市中多次尝试抄底,在震荡市中表现也不佳。信号纯度并不高。

方法二

使用某日前180日的所有ols拟合的β2作为总体的β2分布,用其上、下0.05分位数作为总体的置信限。

该方法得到的信号次数是几乎确定的。假设β2的分布是随时间平稳的,那么前180天中在置信区间中的应当有20.05180=18次,即约有1/10的时间有信号。不过在股市这个随时间有明显变化的时间序列中,可能有信号的次数与理论有很大差异。

为了获得更精确的市场状态,避免在震荡市中误判上涨行情,我们考虑原始均线策略。增加一个约束条件,当30日均线大于30个交易日前的均线。不过我认为30日均线显示上涨,则30日收益率也应该上涨。平均收益率代表几何平均,均线代表线性平均,两者不会有太大差异。

在平台给的接口中只有判断某日是否交易日的函数。为了配合若干交易日前的均线的计算,曲线救国定义了一个函数。该函数作用为获取给定日期若干天前的交易日。简单的做法是使用.rolling()方法,但是会填充一些NA值。

def get_trade_day_before(count, end_date):
'''
获取end_date前count天的交易日,从该日到end_date一共count天
'''
return1 = jq.get_price('000002.XSHG',count = count,end_date=end_date,fields='close').fillna(0)
trade_day_before = return1.index[0]
return str(trade_day_before)[:10]
# 增加一个均线策略
######################################################################
def get_ma(start_date,end_date,time_interval,code = None):
'''
功能:计算沪深A股time_interval天均值作为市场均值
参数:start_date,end_date,
time_interval为int型数字
code为可选参数,默认计算沪深A股均值。也可以选择其他指数
计算前time_interval天的平均值
返回值:list类型,time_interval天的平均值
'''
if not code:
# 将time_interval天的指数价格拼接在start_date前面,
# 然后rolling方法计算均值,最后丢掉前面的指数价格
return0 = jq.get_price(['000002.XSHG','399107.XSHE'],start_date=start_date,end_date=end_date,fields=['close','pre_close']
).fillna(0)
SH_market_return = return0.loc[return0['code']=='000002.XSHG'].reset_index(drop=True)
SZ_market_return = return0.loc[return0['code']=='399107.XSHE'].reset_index(drop=True)
return1 = jq.get_price(['000002.XSHG','399107.XSHE'],count = time_interval,end_date=start_date,fields=['close','pre_close']
).fillna(0)
SH_market_return1 = return1.loc[return1['code']=='000002.XSHG'].reset_index(drop=True)
SH_market_return1.drop([len(SH_market_return1)-1],inplace=True)
SH_market_return = SH_market_return1.append(SH_market_return).reset_index(drop = True)
SZ_market_return1 = return1.loc[return1['code']=='399107.XSHE'].reset_index(drop=True)
SZ_market_return1.drop([len(SZ_market_return1)-1],inplace=True)
SZ_market_return = SZ_market_return1.append(SZ_market_return).reset_index(drop = True)
SH_market_return = SH_market_return.close
SZ_market_return = SZ_market_return.close
ma_simple = (SH_market_return+SZ_market_return)/2
else:
return0 = jq.get_price(code,start_date=start_date,end_date=end_date,fields=['close','pre_close'],
).fillna(0)
return1 = jq.get_price(code,count = time_interval,end_date=start_date,fields=['close', 'pre_close'],
).fillna(0)
return1 = return1.reset_index(drop=True)
return1.drop([len(return1)-1],inplace=True)
return0 = return1.append(return0).reset_index(drop = True)
return0 = return0.close
ma_simple = return0
ma = ma_simple.rolling(time_interval,min_periods=time_interval).mean()
ma.drop(ma.index[:time_interval-1], axis=0, inplace = True)
return ma

分析ols拟合后的数据,计算beta,MA5,MA10,MA20,22天平均收益率,180天2.5%,5%,10%分位数。

# 获取标的指数的beta,MA5,MA10,MA20,22天平均收益率,180天2.5%,5%,10%的分位数
# 输入的数据为cal_beta()的返回dataframe以及指数代码
def get_analysis_frame(code, beta_frame):
'''
标的指数的beta,MA5,MA10,MA20,22天平均收益率,180天2.5%,5%,10%的分位数
'''
# 直接从beta_frame中读取日期
start_date = beta_frame.iloc[0,0]
end_date = beta_frame.iloc[len(beta_frame)-1,0]
beta = beta_frame[['date','beta2','pvalue2']]
R_MA22 = get_Rm_simple2(start_date, end_date, 22, code)
MA30 = get_ma(start_date, end_date, 30, code)
# 获取30个交易日前的日期
start_date_before = get_trade_day_before(30, start_date)
MA30_before = get_ma(start_date_before, end_date, 30, code)
# 30个交易日前的日期到结束日期前30日的均线
MA30_before = MA30_before[:-29]
MA5_before = get_ma(start_date_before, end_date, 5, code)
MA5_before = MA30_before[:-4]
MA10 = get_ma(start_date, end_date, 10, code)
MA5 = get_ma(start_date, end_date, 5, code)
beta['MA5'] = MA5
beta['MA5_before'] = MA5_before
beta['MA10']= MA10
beta['MA30']= MA30
beta['MA30_before'] = MA30_before
beta['R_MA22'] = R_MA22
beta['quantile-0.1'] = beta['beta2'].rolling(180,min_periods=180).quantile(0.1)
beta['quantile-0.05'] = beta['beta2'].rolling(180,min_periods=180).quantile(0.05)
beta['quantile-0.025'] = beta['beta2'].rolling(180,min_periods=180).quantile(0.025)

"""为了避免过拟合的情况,分位数的计算方式为向前滚动计算180天的分位数
180天即半年,是许多标的指数成份股的调整周期
"""

beta = beta.fillna(method = 'bfill')
return beta

有均线时:

df1 = pd.read_excel('beta.xlsx')
analysis = get_analysis_frame('000300.XSHG',df1)
ma30 = analysis.MA30
ma5 = analysis.MA5
ma30_before = analysis.MA30_before
ma5_before = analysis.MA5_before
rm22_mean = analysis.R_MA22
beta2 = analysis.beta2
q_0_05 = analysis['quantile-0.05']
q_01 = analysis['quantile-0.1']
# 增加条件:三十日均线大于三十日前的均线
# 线性平均和指数平均我觉得没啥大的区别
# MA30如果下降,rm30也应该下降
signal_up = [rm22_mean[i]>0 and beta2[i]<q_01[i] and ma30[i] > ma30_before[i] for i in range(len(beta2))]
signal_down  = [rm22_mean[i]<0 and beta2[i]<q_01[i] and ma5[i] < ma5_before[i] for i in range(len(beta2))]
df1['signal_up'] = signal_up
df1['signal_down'] = signal_down
print('上涨时超过单侧置信区间百分之5下限的beta信号出现了{}次,\n\
下跌时超过单侧置信区间百分之5下限的beta信号出现了{}次,\n\
共出现单侧置信区间下限为百分之10的beta信号{}次\n'.format(sum(signal_up),sum(signal_down),sum(signal_up)+sum(signal_down)))
start_date = df1.date[0]
end_date = list(df1['date'])[-1]
###信号坐标##
R_300 = jq.get_price('000300.XSHG',start_date=start_date,end_date=end_date,fields=['low','close'])
fig = plt.subplots(figsize=(13,8))
plt.plot(R_300.close,'b')
plt.grid(True)
plt.title('图七 改进后的多空信号图',fontsize=20)
Y = R_300.close
for i in range(len(signal_up)):
if signal_up[i]:
loc = int(Y[i])
plt.vlines(Y.index[i], loc-400,loc+400, color='red',linewidth = 1,alpha = 0.5)
if signal_down[i]:
loc = int(Y[i])
plt.vlines(Y.index[i], loc-400,loc+400, color='green',linewidth = 1,alpha = 0.5)
first_legend = plt.legend(['000300.XSHG'],fontsize=20)
ax = plt.gca().add_artist(first_legend)
blue_patch = mpatches.Patch(color = 'red',label = '做多信号',linewidth = 1,alpha = 0.5)
green_patch = mpatches.Patch(color = 'green',label = '做空信号',linewidth = 1,alpha = 0.5)
plt.legend(handles = [blue_patch,green_patch],loc = 4,fontsize=20)
plt.xlabel('时间',fontsize=15)
plt.ylabel('沪深300指数收盘价',fontsize=15)
plt.show()

上涨时超过单侧置信区间百分之5下限的beta信号出现了104次,
下跌时超过单侧置信区间百分之5下限的beta信号出现了113次,

共出现单侧置信区间下限为百分之10的beta信号217次
在这里插入图片描述

可以看出这种方法相当保守,在09-10年之间没有信号,11-12年的信号也踏空了。信号集中在大牛市和16-17年的慢牛。
多空策略
在加入均线策略控制信号噪声以后,基本上没有较大的误判,与原研报的结果已经很相近了。
在这里插入图片描述

3.4策略构建和回测

宽基指数

策略标的:上证综指、上证50、沪深300、中小板综合指数 股票组合:当日标的股票成分股,剔除ST股等非正常交易股票
回测时间:2007.01.01 – 2008.12.31 手续费用:忽略 建仓成本:次日均价 策略步骤:计算向前 22
日(包括当日)每天的成分股组合截面绝对离散度 CSAD,OLS 估计 CCK 模型中
Rm,t^2的系数β2,若β2显著为负则认为当日该组合存在羊群效应,根据 22
日内指数平均收益率的正负区分羊群效应发生时的市场趋势为上涨/下跌,买入/卖出标的指数并持仓22 交易日,持有期不重复开仓。

定义一个计算所有感兴趣信息的函数

def cal_rate(code, start_date, end_date):
    '''
    获取信号发生后的收益率,胜率,夏普比率等若干信息。分上涨和下跌两种
    参数:标的指数代码,回测开始和结束日期
    返回值:
    回测的数据dataframe类型,包含多空次数,胜率,最大回撤,夏普比率
    累计策略收益率,dataframe类型,分做空和做多
    每次做多/空的收益率
    '''
    try:
        df1 = pd.read_excel('{}_{}_{}.xlsx'.format(code,start_date,end_date))
    except:
        real_date = get_trade_day_before(22,start_date)
        df1 = cal_beta(real_date,end_date,180,code)
        df1.to_excel(excel_writer='{}_{}_{}.xlsx'.format(code,start_date,end_date), sheet_name='sheet_1')
        df1 = pd.read_excel('{}_{}_{}.xlsx'.format(code,start_date,end_date))
    analysis = get_analysis_frame(code,df1)
    ma30 = analysis.MA30
    ma5 = analysis.MA5
    ma30_before = analysis.MA30_before
    ma5_before = analysis.MA5_before
    rm22_mean = analysis.R_MA22
    beta2 = analysis.beta2
    q_0_05 = analysis['quantile-0.05']
    q_01 = analysis['quantile-0.1']
    # 增加条件:三十日均线大于三十日前的均线
    # 线性平均和指数平均我觉得没啥大的区别
    # MA30如果下降,rm30也应该下降
    signal_up = [rm22_mean[i]>0 and beta2[i]<q_01[i] and ma30[i] > ma30_before[i] for i in range(len(beta2))]
    signal_down  = [rm22_mean[i]<0 and beta2[i]<q_01[i] and ma5[i] < ma5_before[i] for i in range(len(beta2))]
    # 次日持仓情况
    # 是否开仓标志
    is_hold_open = [False]
    is_sell_open = [False]
    for i in range(1,len(signal_up)):
        if signal_up[i-1] and not any(signal_up[max(i-23,0):i-1]):
            is_hold_open.append(True)
        else:
            is_hold_open.append(False)
        if signal_down[i] and not any(signal_down[max(i-22,0):i]):
            is_sell_open.append(True)
        else:
            is_sell_open.append(False)
    df1['is_hold_open'] = is_hold_open
    df1['is_sell_open'] = is_sell_open
    # 次日是否持仓标记
    is_hold = [any(is_hold_open[max(0,i-22):i]) for i in range(len(is_hold_open))]
    is_sell = [any(is_sell_open[max(0,i-22):i]) for i in range(len(is_sell_open))]
    # 指数日收益率
    return0 = jq.get_price(code,start_date=start_date,end_date=end_date,fields=['close', 'pre_close'],
                                ).fillna(0)
    return0['rates'] = return0['close'] / return0['pre_close'] - 1
    # 当日收益率,空仓时按照1计算
    rates_up = [return0.iloc[i,2]+1 if is_hold[max(i-1,0)] else 1 for i in range(len(return0))]
    rates_down = [-return0.iloc[i,2]+1 if is_sell[max(i-1,0)] else 1 for i in range(len(return0))]
    rates_up = np.array(rates_up)
    rates_down = np.array(rates_down)
    df1['rates_up'] = rates_up
    df1['rates_down'] = rates_down
    # 开仓次数
    open_up_times = sum(is_hold_open)
    open_down_times = sum(is_sell_open)
    rates_up_cum = []
    rates_down_cum = []
    # 分日累计收益率
    rates_daily_up = []
    rates_daily_down = []
    for i in range(len(is_hold_open)):
        if is_hold_open[i]:
            rates_up_cum.append(product(rates_up[i+1:i+23]))
            rates_daily_up.append(rates_up[i+1:i+23])
        if is_sell_open[i]:
            rates_down_cum.append(product(rates_down[i+1:i+23]))
            rates_daily_down.append(rates_down[i+1:i+23])
    rates_up_cum=np.array(rates_up_cum)
    rates_down_cum=np.array(rates_down_cum)
    # 胜次数
    up_win_times = sum(rates_up_cum>1)
    down_win_times = sum(rates_down_cum>1)
    # 胜率
    if open_up_times:
        win_rate_up = up_win_times/open_up_times
    else: 
        win_rate_up = NaN
    if open_down_times:
        win_rate_down = down_win_times/open_down_times
    else: 
        win_rate_down = NaN
    ###计算最大回撤,只计算做多###
    re = []
    for k in range(len(rates_up)):
        retreat = max(rates_up[k]-rates_up[k:])
        re.append(retreat)
    max_retreat = max(re)
    ###计算夏普比率,只计算做多###
    ex_pct_close = rates_up - 1 - 0.04/252
    sharpe = (ex_pct_close.mean() * (252)**0.5)/ex_pct_close.std()
    analysis_datas = np.array([open_up_times,open_down_times,win_rate_up,win_rate_down,max_retreat,sharpe]).reshape(1,6)
    back_analysis_data = pd.DataFrame(analysis_datas,
                                        columns =['做多次数','做空次数','做多胜率','做空胜率','最大回撤','夏普比率'
                                        ],index = [code])
    rates_analysis = df1[['date','rates_up','rates_down']]
    rates_up_prod = [product(rates_up[:i]) for i in range(len(rates_up))]
    rates_down_prod = [product(rates_down[:i]) for i in range(len(rates_down))]
    rates_analysis['rates_up_prod'] = rates_up_prod
    rates_analysis['rates_down_prod'] = rates_down_prod
    return back_analysis_data,rates_analysis,rates_daily_up,rates_daily_down,rates_up_cum,rates_down_cum

配合下面的函数使用,获得分年度的策略表现。

def get_annual_performance(time_interval,code):
    # 对于某一指数的若干年的数据
    # 返回一个dataframe
    # 包含每年做多次数,胜率,做空次数,胜率,最大回撤,夏普比率
    performance = pd.DataFrame(columns =['做多次数','做多胜率','最大回撤-多','夏普比率-多','做空次数','做空胜率','最大回撤-空','夏普比率-空'])
    for i in range(len(time_interval)-1):
        start_date = time_interval[i]
        end_date = time_interval[i+1]
        back_analysis_data=cal_rate(code, start_date, end_date)[0].reset_index(drop=True)
        performance = performance.append(back_analysis_data, ignore_index=True)
    time_index = [i[0:4] for i in time_interval[:-1]]
    performance['年份'] = time_index
    performance.set_index('年份',inplace=True)
    return performance

展示沪深300指数14-15年两年

以14-15年沪深300指数为例
将分年度的表现组合起来,就得到了若干年的回测收益率。包含做多和做空。

def get_everyyear_rate(time_interval,code):
    # 对于某一指数的若干年的数据
    # 返回一个dataframe
    # 包含当日收益率和从开始到结束日期的累计收益率,分为做多和做空
    rate = pd.DataFrame(columns =['date','rates_up','rates_down','rates_up_prod','rates_down_prod'])
    for i in range(len(time_interval)-1):
        start_date = time_interval[i]
        end_date = time_interval[i+1]
        back_analysis_data=cal_rate(code, start_date, end_date)[1]
        rate = rate.append(back_analysis_data, ignore_index=True)
    
    rate_cum_up = [product(rate.iloc[:i,1]) for i in range(len(rate))]
    rate['rate_prod_up'] =  rate_cum_up
    rate_cum_down = [product(rate.iloc[:i,2]) for i in range(len(rate))]
    rate['rate_prod_down'] =  rate_cum_down
    rate.set_index('date',inplace=True)
    return rate

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

可以看出,做多的效果尚可。但是做空的收益率很低(几乎忽略不计),原因可能是我国对做空的限制导致大量做空的羊群效应不显著。研报中提到做空的羊群效应持续时间很短,可以考虑对做空时间缩短。代码如下。

code = '000300.XSHG'# 上证300
start_date = '2014-01-01'
end_date = '2016-01-01'

back_analysis_data,rates_analysis,rates_daily_up,rates_daily_down,rates_up_cum,rates_down_cum = cal_rate(code, start_date, end_date)
print(back_analysis_data)
rates_daily_down0 = rates_daily_down[0]
# print(rates_daily_down0)
rates_daily_down_cum = [product(rates_daily_down0[:i]) for i in range(len(rates_daily_down0))]
# print(rates_daily_down_cum)
fig = plt.subplots(figsize=(13,8))
plt.plot(rates_daily_down_cum,'green')
plt.grid(True)
plt.title('图八,做空开仓后22日的累计收益率',fontsize=20)
plt.xlabel('时间',fontsize=15)
plt.ylabel('沪深300某次做空的收益率',fontsize=15)
plt.show()

rates_daily_up0 = rates_daily_up[0]
# print(rates_daily_down0)
rates_daily_up_cum = [product(rates_daily_up0[:i]) for i in range(len(rates_daily_up0))]
fig = plt.subplots(figsize=(13,8))
plt.plot(rates_daily_up_cum,'green')
plt.grid(True)
plt.title('图八,做多开仓后22日的累计收益率',fontsize=20)
plt.xlabel('时间',fontsize=15)
plt.ylabel('沪深300某次做多的收益率',fontsize=15)
plt.show()

下面以上证50为例,对收益率可视化

'''
上证50
'''
code = '000016.XSHG'# 上证50
# 时间取07-17年
time_interval = ['2007-01-01','2008-01-01','2009-01-01']
time_interval.extend(['20{}-01-01'.format(i) for i in range(10,17+2)])
performance = get_annual_performance(time_interval,code)
performance.to_excel(excel_writer='07-17年上证50表现.xlsx', sheet_name='sheet_1')
start_date = '2007-01-01'
end_date = '2008-01-01'
back_analysis_data,rates_analysis,rates_daily_up,rates_daily_down,rates_buy_each_time,rates_sell_each_time = cal_rate(code, start_date, end_date)

rates_daily_up0 = rates_daily_up[0]
# print(rates_daily_down0)
rates_daily_up_cum = [product(rates_daily_up0[:i]) for i in range(len(rates_daily_up0))]
fig = plt.subplots(figsize=(13,8))
plt.plot(rates_daily_up_cum,'green')
plt.grid(True)
plt.title('图八,做多开仓后22日的累计收益率',fontsize=20)
plt.xlabel('时间',fontsize=15)
plt.ylabel('07-08年上证50某次做多的收益率',fontsize=15)
plt.show()

fig,ax=plt.subplots(figsize=(13,8))
plt.plot(rates_analysis.date,rates_analysis.rates_up_prod,'b')
plt.grid(False)
plt.title('图九,07-08年上证50做多收益率',fontsize=20)
plt.legend(['收益率'],fontsize=15)
plt.xlabel('时间',fontsize=15)
plt.ylabel('收益率',fontsize=15)
for label in ax.xaxis.get_ticklabels():
    # label is a Text instance
    label.set_color('red')
    label.set_rotation(45)
    label.set_fontsize(10)
for i,label in enumerate(ax.get_xticklabels()):
    if i%30==0:
        label.set_visible(True)
    else:
        label.set_visible(False)

plt.tight_layout()
ax.tick_params(bottom=False,top=False,left=False,right=False)  #移除全部刻度线
plt.show()

在这里插入图片描述

最后,实现对07-08年上证综指、上证50、沪深300、中小板综合指数的回测。

'''
宽基指数做多收益率
'''
# 不知道什么原因,中证500和创业板指获取不了成分股
index_code = ['000001.XSHG','000016.XSHG','000300.XSHG'
              ,'399101.XSHE']
colors = ['red','orange','yellow','green']
# 时间取07-17年
time_interval = ['2007-01-01','2008-01-01','2009-01-01']
# time_interval.extend(['20{}-01-01'.format(i) for i in range(10,17+2)])

fig,ax=plt.subplots(figsize=(13,8))
for i,code in enumerate(index_code):
    every_rate = get_everyyear_rate(time_interval, code)
    plt.plot(every_rate.index,every_rate.rate_prod_up,color = colors[i],label = code)
plt.grid(False)
plt.title('07-08年宽基指数做多收益率',fontsize=20)
plt.legend()
plt.xlabel('时间',fontsize=15)
plt.ylabel('收益率',fontsize=15)
for label in ax.xaxis.get_ticklabels():
    # label is a Text instance
    label.set_color('red')
    label.set_rotation(45)
    label.set_fontsize(10)
for i,label in enumerate(ax.get_xticklabels()):
    if i%90==0:
        label.set_visible(True)
    else:
        label.set_visible(False)

plt.tight_layout()
ax.tick_params(bottom=False,top=False,left=False,right=False)  #移除全部刻度线
plt.show()


'''
上涨时分日累计收益率
'''
index_code = ['000001.XSHG','000016.XSHG','000300.XSHG'
              ,'399101.XSHE']
colors = ['red','orange','yellow','green']
# 时间取07-17年
time_interval = ['2007-01-01','2008-01-01','2009-01-01']
# time_interval.extend(['20{}-01-01'.format(i) for i in range(10,17+2)])

fig,ax=plt.subplots(figsize=(13,8))

for i,code in enumerate(index_code):
    rates_daily_up = []
    for j in range(len(time_interval)-1):
        rates_daily_up.extend(cal_rate(code, time_interval[j], time_interval[j+1])[2])
    rates_daily_up0 = rates_daily_up[1]
    rates_daily_up_cum = [product(rates_daily_up0[:i]) for i in range(len(rates_daily_up0))]
    plt.plot(rates_daily_up_cum,color = colors[i],label = code)
plt.grid(False)
plt.title('07-08年上涨时分日累计收益率',fontsize=20)
plt.legend()
plt.xlabel('时间',fontsize=15)
plt.ylabel('收益率',fontsize=15)

plt.tight_layout()
plt.show()


'''
下跌时分日累计收益率
'''
index_code = ['000001.XSHG','000016.XSHG','000300.XSHG'
              ,'399101.XSHE']
colors = ['red','orange','yellow','green']
# 时间取07-17年
time_interval = ['2007-01-01','2008-01-01','2009-01-01']
# time_interval.extend(['20{}-01-01'.format(i) for i in range(10,17+2)])

fig,ax=plt.subplots(figsize=(13,8))

for i,code in enumerate(index_code):
    rates_daily_up = []
    for j in range(len(time_interval)-1):
        rates_daily_up.extend(cal_rate(code, time_interval[j], time_interval[j+1])[3])
    rates_daily_up0 = rates_daily_up[1]
    rates_daily_up_cum = [product(rates_daily_up0[:i]) for i in range(18)]
    plt.plot(rates_daily_up_cum,color = colors[i],label = code)
plt.grid(False)
plt.title('07-08年下跌时分日累计收益率',fontsize=20)
plt.legend()
plt.xlabel('时间',fontsize=15)
plt.ylabel('收益率',fontsize=15)

plt.tight_layout()
plt.show()

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
中小板指做空效果更好,可能低市值指数更适合做空。

行业指数

申万一级行业指数,并分成4大类,金融型,成长型,消费型,周期型
策略标的:以上4大类行业
股票组合:当日标的指数的成分股,剔除ST股等非正常交易状态股票
回测时间:2014年3月1日至2019年9月1日
手续费用:在此回测暂时忽略

策略步骤: (做多)
向前22日计算每天(包括当天)的成份股组合截面绝对离散度CSAD,OLS估计CCK模型中Rm平方的系数β2。
向前计算180天内的样本单侧置信区间10%下限。
向前计算22日内标的指数平均收益率的正负区间,运用正负判断市场趋势
向前计算MA30
开仓:当β2小于单侧置信区间下限,标的指数平均收益率为正,当日MA30大于30天前的MA30时,做多标的指数。
清仓:持有22天后卖出。
(做空)
所需计算的指标同上文做多策略。
开仓:当β2小于单侧置信区间下限,标的指数平均收益率为负,当日MA30小于5天前的MA30时,做空标的指数。
清仓:持有22天后卖出。 为了方便对于行业指数和宽基指数同时使用以上函数,我们重载了get_price,聚宽的get_price仅针对宽基指数。相比于聚宽的函数,它仅能输入fields参数为[‘close’,
‘pre_close’]。

def get_price(code,start_date=None,end_date=None,fields='close',count=None):
    '''
    重新定义函数get_price
    整合对于行业和指数的价格获取要求
    行业指数的返回值与jq.get_price返回值相同
    参数:
    code,宽基指数或者行业指数代码,或以代码为元素的list
    start_date与count二选一
    fields,可选'close'或'pre_close'或['close','pre_close']
    '''
    if isinstance(code,list) and len(code[0])==6:
        price_df = pd.DataFrame(columns=['time','code','close','pre_close'])
        for i in code:
            df_temp = get_price(i, start_date=start_date, end_date=end_date, fields=fields, count=count)
            df2_temp = pd.DataFrame()
            df2_temp['time']=df_temp.index
            df2_temp['code']=[i]*len(df_temp)
            df_temp.reset_index(inplace=True,drop=True)
            df3 = pd.concat([df2_temp,df_temp],join="outer",axis=1)
            price_df = price_df.append(df3,ignore_index = True)
        return(price_df)
    elif len(code)==6:
        if count:
            return1 = jq.get_price('000002.XSHG',count = count,end_date=end_date,fields='close').fillna(0)
            start_date = return1.index[0]
        q = jq.finance.run_query(query(jq.finance.SW1_DAILY_PRICE
                                        ).filter(jq.finance.SW1_DAILY_PRICE.code==code,
                                                jq.finance.SW1_DAILY_PRICE.date >= start_date,
                                                jq.finance.SW1_DAILY_PRICE.date <= end_date))
        industry = q[['date','close']]
        if 'pre_close' in fields:
            pre_close = list(q.close)
            # print(pre_close[0]/(1+q.iloc[0, -1]/100))
            pre_close.insert(0, pre_close[0]/(1+q.iloc[0, -1]/100))
            pre_close.pop()
            industry['pre_close']=pre_close
        industry.set_index('date', inplace=True)
        return industry
    else:
        if count:
            return jq.get_price(code,count = count,end_date=end_date,fields=fields)
        else:
            return jq.get_price(code,start_date=start_date,end_date=end_date,fields=fields)

同时对获取成分股函数稍作更改,增加对行业成分股的支持。

def get_stocks(date,index=None):
    '''
    功能:
        根据日期,获取该日满足交易要求的股票相关数据,即剔除ST股、上市未满60天、停牌、跌涨停股
    参数:
        date,日期
        index,指数代码,也可以是行业代码。在特定指数的成分股中选股。缺省时选股空间为全部A股
    返回:
        DataFrame类型,索引为股票代码,同时包含了价格数据,方便后续使用
    '''
    stocks = jq.get_all_securities(
        types=['stock'],
        date=date
    )#该日正在上市的股票
    
    if index:#特定成分股
        # 是否宽基指数
        if len(index)==11:
            stock_codes = jq.get_index_stocks(index,date=date)#成分股
        # 行业指数
        elif len(index)==6:
            stock_codes = jq.get_industry_stocks(index,date=date)
        stocks = stocks[stocks.index.isin(stock_codes)] 

    #上市日期大于60个自然日
    # start_date 为 [datetime.date] 类型
    stocks['start_date']=pd.to_datetime(stocks['start_date'])
    date = datetime.strptime(date,'%Y-%m-%d').date()
    date = pd.to_datetime(date)
    stocks['datedelta'] = date - stocks['start_date']
    stocks = stocks[stocks['datedelta'] > timedelta(days=60)]
    
    #是否是ST股
    stocks['is_st'] = jq.get_extras(
        info='is_st',
        security_list=list(stocks.index),
        count=1, 
        end_date=date
    ).T
    
    #涨停、跌停、停牌
    stocks_info = jq.get_price(
        security = list(stocks.index),
        fields=['close','high','low','high_limit','low_limit','paused'],
        count=1,
        end_date=date,
        panel=False
    ).set_index('code').drop('time',axis=1)
    
    stocks['price'] = stocks_info['close']#顺便保存价格,方便后续运算
    stocks['paused'] = stocks_info['paused'] == 1#是否停牌
    stocks['high_stop'] = stocks_info['high'] >= stocks_info['high_limit']#涨停
    stocks['low_stop'] = stocks_info['low'] <= stocks_info['low_limit']#跌停
    stocks = stocks[~(stocks['is_st'] | stocks['paused'] | stocks['high_stop'] | stocks['low_stop'])]   
    return stocks

最后将所有函数的jq.get_price改成我们重载的get_price即可。
以下是消费行业指数为例,在07-08年的回测

# 行业指数
Cycle = ['801020','801030','801040','801050','801710',
         '801720','801730','801890','801170','801180','801160']

Consumption = ['801180','801110','801130','801200',
               '801120','801150','801210','801140','801010']
Growth = ['801080','801770','801760','801750','801740']

Finance = ['801780','801790']

times  = ['2014-03-01','2015-01-01','2016-01-01','2017-01-01'
          ,'2018-01-01','2019-01-01','2019-09-01']


'''
行业指数做多收益率
'''
# 消费行业指数为例
index_code = Consumption
colors = ['red','orange','yellow','green','magenta','lime','darkorchid','darkorange','blue']
# 时间取07-17年
time_interval = ['2007-01-01','2008-01-01','2009-01-01']
# time_interval.extend(['20{}-01-01'.format(i) for i in range(10,17+2)])

fig,ax=plt.subplots(figsize=(13,8))
for i,code in enumerate(index_code):
    every_rate = get_everyyear_rate(time_interval, code)
    plt.plot(every_rate.index,every_rate.rate_prod_up,color = colors[i],label = code)
plt.grid(False)
plt.title('07-08年消费行业指数做多收益率',fontsize=20)
plt.legend()
plt.xlabel('时间',fontsize=15)
plt.ylabel('收益率',fontsize=15)
for label in ax.xaxis.get_ticklabels():
    # label is a Text instance
    label.set_color('red')
    label.set_rotation(45)
    label.set_fontsize(10)
for i,label in enumerate(ax.get_xticklabels()):
    if i%90==0:
        label.set_visible(True)
    else:
        label.set_visible(False)

plt.tight_layout()
ax.tick_params(bottom=False,top=False,left=False,right=False)  #移除全部刻度线
plt.show()

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

在这里插入图片描述
消费行业指数做多效果尚可,但是做空收益率没有明显优势,与原研报的结论相仿。

总结

作为本人第一个独立完成的研报复现,该研报参考了聚宽精英任务获奖作品——作者:Eden666的思路。受限于账号等级和硬件水平,本人仅对研报完成了复现,在结论方面,由于没有足够的数据,没有给出更深入的结论。根据我们的研究得到了以下结论:
1、收益率与市值相关,高市值对于做多的羊群效应收益可能更高
2、羊群效应明显不足,收益率不高,大部分时间为空仓状态,尤其是做空收益很差,可能造成很大损失。

  • 14
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值