我们知道,根据上市公司发布的季报表现来进行短线交易常常是得不到满意结果的,因为在季报发布前,相关的信息往往已经被市场充分响应完毕,公司股价往往已经提前反映了市场对季报表现的预期。
然而大量上市公司在发布季报的若干天之前会发布季报预告,我们猜测,季报预告可能是一种未被市场提前充分响应的信息来源,其重大利好消息很可能成为在季报公布前的这段时间内股价上攻的主要动力,为短线交易赚取价差的策略提供盈利契机。
为获取季报预告的相关数据,我们需要使用到tushare包中的forecast_data函数,输入参数year和q为年份和季度。
import tushare as tsdef get_forecast_data(season = '2013q1' ):year = int(season[:4]) q = int(season[-1]) df = ts.forecast_data(year, q)return df
调用函数,返回数据类型示例如下:
各列数据含义:
code:股票6位代码
name:股票名称
type:预告类型,包括‘预增’,‘预升’,‘预盈’,‘预平’,‘预减’,‘预降’,‘预亏’,‘减亏’,‘0’等。预告类型取决于预报EPS变动幅度range。
report_date:预告发布日期。
pre_eps:上一季报公布的EPS。
range:预告EPS较上一季EPS变动幅度,决定了预告类型。例如range达到50%以上的则为预增,达到-50%以下的则为预减。
基于forecast_data提供的数据,我们便有能力对季报预告的效果进行观察分析了。
首先我们尝试观察预报信号对个股股价的影响。我们不妨选取预告类型为‘预增’,预告range为1000%以上的重大利好消息的少数个股进行K线图观察,直观来看是否在预增预告发布后会有股价上涨的表现。
下面看两个个股观察的例子:
如下图所示为002256兆新股份的K线图,第一条竖线为2013年2季度季报预告日期,第二条竖线为季报发布日期。根据多均线指标,我们不难看出在季报预告后,季报发布前,股价总体呈现上升的趋势,显示了预增预告的信号作用。
另一方面,也存在表现非常不符合我们直观预期的个股。如下图为000809铁岭新城的K线图,在大幅预增的季报预告后,股价不但没有走高反而发生严重下跌。
以上两例可以看出,某家上市公司的预增预告对之后该个股股价的预测效果并不是绝对准确的。接下来我们想验证,某种预报类型的预测准确性是否能在整体尺度上表现显著。
我们任意选取某一个季度的预报数据和季报发布数据,进行以下几项统计:
按申万一级行业对股票进行行业划分,观察不同行业是否有明显不同的响应特性。
统计季报发布当天股价比预告日当天股价更高或更低的股票,求出他们占总预告股票数的比例,用来衡量某种预告对股价上涨或下跌的预测准确性。
统计季报预告后、季报发布前的时间内,股价出现过的最高价比预告当天价格高出一定比例的股票数占总股票数的比例,以及达到高位价格的相对涨幅,以确认是否预告信号在季报发布前已经被响应完毕达到高位。
计算最高价比预告日晚出现的平均天数来简单衡量这一响应时间。
一个典型的统计结果示例如下图,反应了不同预告类型中的股票数以及上述统计量(上涨占比,下跌占比,出现比预告日更高的股价占比,出现的最高价的平均涨幅,出现最高价当天相比预告日的平均延迟天数)以及该行业中的上述统计量最为最显著的预告类型汇总:
经过广泛统计,我们认为,尽管预增类型预告在某个行业中或某个特定季度中没有表现出预测准确概率上绝对的显著,甚至有一些行业的预增上涨股票数比例极其低,但预增信号总体平均来看确实具有一定的预测股价上升趋势的能力。因此,以下我们的交易策略构建主要基于预增信号。
经过以上的信号有效性统计,我们自然想到验证一个简单直观的交易想法:选取预告类型为“预增”的股票,在其季报预告日买入持有,公告发布日清仓(若非交易日或该股票停牌则交易日向后顺延),初始资金2亿,每日调仓对所持股票等权持有。此次回测标的为沪深300。
回测结果如下图所示:
实际策略中我们调试策略发现,加入EPS预告增长幅度为1000%以上以及上一季EPS>0的筛选条件,选出预增信息更加鲜明,利好信号更加强势的个股,相应地也可以得到更好的策略表现,得到的回测结果如下(此次回测的标的为中证全指):
基于以上的两个回测结果,我们可以看出该策略在2016年中之前是产生了明显的超额收益的,但在这之后则表现出了策略的失效。不论我们选择沪深300,中证全指或其他的指数标的,策略均会在2016年后明显的跑不赢市场。
随后,我们进行了一系列的策略改进工作,主要着眼于股票筛选条件的精细化,如加入一些基本面因子市值、ROE等,但都没有对策略的鲁棒性起到明显的改良——在2016年附近,策略总会失效,开始跑输市场。因此,我们转换思路,开始考虑市场有效性对该策略的影响。
我们认为该策略在2016年后的失效原因主要有两种可能:
一方面,随着金融市场的发展,市场有效性的增强,季报预告得到了更多的市场关注,市场对公开信息响应迅速,在发布季报预告后,该股连续涨停,而我们的策略待到开板买入时,股价已经处于高位,蓄势回调,造成策略收益率的损失;
另一方面由于可能的知情交易的存在,我们之前认为季报发布的信息会被市场提前响应完毕,那么季报预告也有可能会被市场提前响应而失去价值——即在季报预告发布前,内幕信息泄露,知情交易者买入股票,使得股价在预告发布日前已经上涨完毕,待到我们的策略买入时,预告信号已经被响应结束,股价缺乏上攻力量。
现在我们来尝试验证知情交易的想法,构建这样一个策略:选出“预增”类型,EPS增幅超过1000%,上一季EPS>0的股票,在季报预告的7天前买入,在季报预告发布当天卖出(如有非交易日或停牌则交易日顺延),每日调仓,股票等权持有。
我们得到的回测结果见文末下方,高企的收益曲线足以证实我们关于知情交易的猜测。当然,这样的回测包括了未来函数——即我们绝不可能提前获知7天后的季报预告的信息。此策略的回测目的只是帮我们佐证季报预报信号中知情交易的存在和显著性,了解季报预报和季报公布同样会被市场提前响应而失去信号价值的真实情况,而不能真正的用于现实交易。
综上所述,我们观察并统计发现了季报预告,尤其是强的预增信号,是对股价有一定的预测作用的。根据我们的回测结果,季报预增信号在2016年之前尚且可以有效地产生超额收益,而之后则失去了作用,开始跑输市场。最终,我们用含有未来函数的回测证实了在季报预告信号中知情交易的显著存在,帮助我们理解了如今的市场现实:季报预告信号,如同季报公布信号一样,会被市场提前响应而失去作用。
注:后附研究模块为回测用数据的调取和存入pickle,为回测提供必要的数据。详细内容见代码注释。
本文由JoinQuant量化课堂推出,版权归JoinQuant所有,商业转载请联系我们获得授权,非商业转载请注明出处。
import tushare as tsimport numpy as npimport pandas as pdimport mathimport picklefrom collections import defaultdictfrom jqdata import *
def nextSeason(season):if season[-1] == '4':nextSeason = str(int(season[:4]) + 1) + 'q1'else:nextSeason = season[:5] + str(int(season[-1]) + 1)return nextSeason
# 获取7天前的日期def Prev_7_Date(date):date_2 = datetime.datetime(int(date[:4]), int(date[5:7]), int(date[8:10]))date_3 = date_2 - datetime.timedelta(7)date_4 = date_3.date().isoformat()return date_4
# 获取季报预增字典和预报提前7天字典,格式 日期:(股票 ,季度标签)def SeasonForeDict(season, all_days):year = int(season[:4])month = int(season[-1])all_stock_list = get_all_securities().indexdf = ts.forecast_data(year, month)season_buy_dict = {}prev_season_buy_dict = {}for code in df.code:df_code = df[df.code == code]# 筛掉上季EPS为负的if list(df_code.pre_eps)[0] <= 0:continue# 选出预增的, 用'~'判断给出的是业绩预测值,而不是实际值if df_code.type.values[-1] == type_name and '~' in str(df_code.range.values[-1]):range_string = str(df_code.range.values[-1])first_sign_index = range_string.index('%')lower_lim = float(range_string[:first_sign_index])# 选出EPS涨幅超1000%的if lower_lim < 1000:continue# 将6位数字变为完整的股票代码stock = ''code_XSHG = code + '.XSHG'code_XSHE = code + '.XSHE'if code_XSHG in all_stock_list:stock = code_XSHGelif code_XSHE in all_stock_list:stock = code_XSHEelse:continue# 获取预报日date = df_code.report_date.values[-1]# 若预报日不交易,转为之后的第一个交易日if date not in all_days:all_days_after = [x for x in all_days if x >= date]date = all_days_after[0]# 整理放入季报预报字典if date not in season_buy_dict.keys():season_buy_dict[date] = [(stock, season)]elif (stock,season) not in season_buy_dict[date]:season_buy_dict[date].append((stock,season))# 季报预报提前7天交易字典prev_date = Prev_7_Date(date)if prev_date not in all_days:all_days_after = [x for x in all_days if x >= prev_date]prev_date = all_days_after[0]if prev_date not in prev_season_buy_dict.keys():prev_season_buy_dict[prev_date] = [(stock, season)]elif (stock, season) not in prev_season_buy_dict[prev_date]:prev_season_buy_dict[prev_date].append((stock, season))return season_buy_dict, prev_season_buy_dict
# 获取一定季度范围内的预报字典汇总和预报提前7天字典汇总def TotalDict(all_days, start_season = '2012q3', end_season = '2018q2'):total_in_dict = {}total_out_dict = {}season = start_seasonwhile season <= end_season:# 获取每季字典FD, PFD = SeasonForeDict(season, all_days)#PD = SeasonPubDict(season, all_days)# 汇总放入总字典for k,v in PFD.items():total_in_dict.setdefault(k, []).append(v)for k,v in FD.items():total_out_dict.setdefault(k, []).append(v)season = nextSeason(season)return total_in_dict, total_out_dict
# 获取某个日期开始回测时初始的股票池def InitTupList(all_days, start_season = '2012q3', end_season = '2012q4', start_date = '2013-01-01'):init_in_dict, init_out_dict = TotalDict(all_days, start_season, end_season)# 去除黑名单股票blacklist = ['300356.XSHE']init_tup_list = []# 将预报前7天在回测初始日之前,预报在回测初始日之后的股票加入初始股票池for date,s_list in init_out_dict.items():if date > start_date and Prev_7_Date(date) <= start_date:for tup in s_list[0]:if tup[0] not in blacklist:init_tup_list.append(tup)return init_tup_list
type_name = '预增'all_days = list(get_all_trade_days())all_days = [str(x) for x in all_days]
a, b = TotalDict(all_days)
[Getting data:]############################[Getting data:]#######################################################[Getting data:]##################[Getting data:]###################################[Getting data:]###########################[Getting data:]########################################################[Getting data:]#####################[Getting data:]##################################[Getting data:]#############################[Getting data:]############################################################[Getting data:]#######################[Getting data:]########################################[Getting data:]################################[Getting data:]###################################################################[Getting data:]#######################[Getting data:]#######################################[Getting data:]###################################[Getting data:]############################################################################[Getting data:]###########################[Getting data:]##############################################[Getting data:]########################################[Getting data:]#####################################################################################[Getting data:]###########################[Getting data:]##############################################
c = InitTupList(all_days)
[Getting data:]############################[Getting data:]#######################################################
# 【将结果写入Package并Pickle序列化】Package=[a, b, c]#使用pickle模块将数据对象保存到文件pkl_file = open('DateStockDict.pkl', 'wb')pickle.dump(Package, pkl_file, 0)pkl_file.close()
# 获取季报发布日期:股票列表 + 季度标签def SeasonPubDict(season, all_days):q = query(valuation.code, indicator.pubDate)df = get_fundamentals(q, statDate = season)season_pub_dict = {}for stock in df.code:date = df[df.code == stock].pubDate.values[0]# 若发布日不交易,转为之后的第一个交易日if date not in all_days:all_days_after = [x for x in all_days if x >= date]date = all_days_after[0]# 整理放入季报发布字典if date not in season_pub_dict.keys():season_pub_dict[date] = [(stock, season)]else:season_pub_dict[date].append((stock, season))return season_pub_dict
本社区仅针对特定人员开放
查看需注册登录并通过风险意识测评
5秒后跳转登录页面...
移动端课程