打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
事件驱动分析基本流程
摘要
事件驱动是一类传统的投资策略,其将离散化的事件时间对齐,来看其是否具有的超额收益。这里所说的事件可以是公告,如业绩预告,定增公告,高转送等,也可以是技术指标,如CCI小于-200,DEA穿0等等。本文旨在完成一个简易的事件驱动分析基本流程,以及由事件驱动分析转化为投资策略的一些思考。
xxxxxxxxxx9
1
import pandas as pd
2
import numpy as np
3
import datetime
4
from dateutil.parser import parse
5
import scipy.stats as ss
6
import matplotlib.pyplot as plt
7
from CAL.PyCAL import *
8
cal = Calendar('China.SSE')
9
font.set_size(15)
一、事件列表获取
首先我们要获取事件数据,如下调用业绩预告API,对于事件分析来说,一定要知道的是股票代码和事件发生的时间字段,保留forecastType字段,方便以后对不同业绩预告做分组分析。
xxxxxxxxxx8
1
def get_events(begin_date,end_date,universe):
2
events = DataAPI.FdmtEfGet(secID=universe,reportType=u"",forecastType=u"",publishDateBegin=begin_date,
3
publishDateEnd=end_date,field=u"secID,actPubtime,forecastType",pandas="1")
4
events['actPubtime'] = events['actPubtime'].map(lambda x : x[0:10] if cal.isBizDay(x) else cal.advanceDate(x[0:10], '-1B',BizDayConvention.Unadjusted).strftime('%Y-%m-%d'))
5
events = events[events['actPubtime'] < parse(end_date).strftime('%Y-%m-%d')]
6
events['secID_actPubtime'] = events['secID'] +'_'+ events['actPubtime']
7
events = events.drop_duplicates()
8
return events
xxxxxxxxxx1
1
events = get_events('20080101','20160520',set_universe('A'))
二、行情数据获取
事件分析需要用到一个窗口内的行情数据,但通过离散的时间点反推拿数据调用API太多,并且也是会冗余的。故我们先将所有可能用到的行情数据全部拿出,生产一张日期-股票的二维表。
xxxxxxxxxx10
1
def get_quotation(start_date,end_date,universe):
2
df_eq = pd.DataFrame()
3
for i in range(30,len(universe),30):
4
df_tmp = DataAPI.MktEqudAdjGet(secID=universe[i-30:i],beginDate=start_date,endDate=end_date,
5
field=u"secID,tradeDate,openPrice",isOpen=1,pandas="1")
6
df_eq = pd.concat([df_eq,df_tmp],axis=0)
7
df_tmp = DataAPI.MktEqudAdjGet(secID=universe[i:],beginDate=start_date,endDate=end_date,
8
field=u"secID,tradeDate,openPrice",isOpen=1,pandas="1")
9
df_eq = pd.concat([df_eq,df_tmp],axis=0)
10
return df_eq
xxxxxxxxxx1
1
df_eq = get_quotation('20071101','20160920',set_universe('A'))
xxxxxxxxxx1
1
df_eq_unstack = df_eq.set_index(['tradeDate','secID']).unstack()
业绩预告公布通常是收盘后发生,此处我们计算每日收益采用的更接近实际的开盘至开盘的收益。
xxxxxxxxxx1
1
open2open_yield = df_eq_unstack['openPrice'].pct_change().dropna(how='all')
xxxxxxxxxx1
1
open2open_yield.head()
secID 000001.XSHE 000002.XSHE 000004.XSHE 000005.XSHE 000006.XSHE 000007.XSHE 000008.XSHE 000009.XSHE 000010.XSHE 000011.XSHE ... 603969.XSHG 603979.XSHG 603986.XSHG 603988.XSHG 603989.XSHG 603993.XSHG 603996.XSHG 603997.XSHG 603998.XSHG 603999.XSHG
tradeDate
2007-11-02 -0.019308 -0.007703 -0.073746 -0.016667 -0.033029 NaN -0.003281 -0.017812 NaN 0.049726 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2007-11-05 -0.026032 0.002554 0.029724 -0.011299 -0.000332 NaN -0.042798 0.034046 NaN -0.046039 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2007-11-06 -0.042206 -0.092762 0.010309 0.002857 -0.052247 NaN 0.079106 -0.010920 NaN -0.006396 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2007-11-07 0.019781 0.032651 -0.021429 0.007123 0.009450 NaN 0.016733 -0.017564 NaN 0.008661 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
2007-11-08 0.006754 0.000000 0.027112 -0.032532 -0.012309 NaN -0.014890 0.029116 NaN 0.040149 ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
5 rows × 2920 columns
三、基准收益选取
我们分析结果是要看最后事件是否具有超额收益,故要设置一个基准收益,看处于事件的股票的收益是否比基准收益更高。此处我们将股票同行业的平均收益做为基准收益,更细致的分析,可以将市值因素考虑进来,出于运算时间的考虑此处没有加上。
xxxxxxxxxx4
1
def get_industry(into_date,universe):
2
df_industry = DataAPI.EquIndustryGet(industryVersionCD=u"010303",secID=universe,
3
intoDate=into_date,field=u"secID,industryName1",pandas="1")
4
return df_industry
xxxxxxxxxx1
1
df_industry = get_industry('20161101',set_universe('A'))
xxxxxxxxxx6
1
ind_ret_list = []
2
for ind in df_industry.industryName1.drop_duplicates().tolist():
3
ind_ret = open2open_yield.ix[:,df_industry[df_industry['industryName1']==ind]['secID'].tolist()].mean(axis=1)
4
ind_ret.name = ind
5
ind_ret_list.append(ind_ret)
6
ind_ret_df = pd.concat(ind_ret_list,axis=1)
xxxxxxxxxx3
1
def get_benchmark(x):
2
ind = df_industry[df_industry['secID']==x]['industryName1'].values[0]
3
return ind_ret_df[ind]
xxxxxxxxxx1
1
benchmarkYield = open2open_yield.apply(lambda x: get_benchmark(x.name))
xxxxxxxxxx1
1
benchmarkYield.head()
secID 000001.XSHE 000002.XSHE 000004.XSHE 000005.XSHE 000006.XSHE 000007.XSHE 000008.XSHE 000009.XSHE 000010.XSHE 000011.XSHE ... 603969.XSHG 603979.XSHG 603986.XSHG 603988.XSHG 603989.XSHG 603993.XSHG 603996.XSHG 603997.XSHG 603998.XSHG 603999.XSHG
tradeDate
2007-11-02 -0.017547 -1.672790e-02 -0.052213 -1.672790e-02 -0.031043 -0.039814 -0.052747 -0.047344 -0.050576 -1.672790e-02 ... -0.046136 -0.031043 -0.050595 -0.052133 -0.050595 -0.052747 -0.055699 -0.053349 -0.052213 -0.049244
2007-11-05 -0.018039 -1.526467e-02 -0.017383 -1.526467e-02 -0.029716 -0.017486 -0.031141 -0.008446 -0.009109 -1.526467e-02 ... -0.013924 -0.029716 -0.003476 -0.013211 -0.003476 -0.031141 -0.004119 -0.016371 -0.017383 -0.011453
2007-11-06 -0.017563 2.556436e-07 0.024740 2.556436e-07 -0.010207 0.029414 0.007403 0.014627 0.013087 2.556436e-07 ... 0.025786 -0.010207 0.033150 0.018414 0.033150 0.007403 0.033471 0.025504 0.024740 0.025682
2007-11-07 0.010099 8.917162e-03 0.011993 8.917162e-03 0.026226 0.017989 0.006096 0.009628 0.005059 8.917162e-03 ... 0.003906 0.026226 0.008568 0.006212 0.008568 0.006096 0.007304 0.009932 0.011993 0.011487
2007-11-08 0.006309 -5.831206e-03 -0.014400 -5.831206e-03 -0.001473 -0.006418 -0.007260 -0.000318 -0.009066 -5.831206e-03 ... -0.015505 -0.001473 -0.017592 -0.010173 -0.017592 -0.007260 -0.006330 -0.008882 -0.014400 -0.009428
5 rows × 2920 columns
四、时间窗口分析
事件驱动分析将时间发生点设置为T+0,将所有事件发生对齐到T+0点,并设置T-n到T+n为事件影响期,看事件的超额收益情况。
xxxxxxxxxx18
1
def get_window_yield(unstack_yield,events,window):
2
#获取事件窗口收益
3
events = events.reset_index()
4
def get_time_list(window,events):
5
# 得到每个事件的时间窗口
6
minus_period = '-' + str(window) + 'B'
7
period = str(window) + 'B'
8
time_list = [[cal.advanceDate(events['actPubtime'][i],minus_period,BizDayConvention.Unadjusted),cal.advanceDate(events['actPubtime'][i],period,BizDayConvention.Unadjusted)] for i in range(len(events['actPubtime']))]
9
return time_list
10
11
time_list = get_time_list(window,events)
12
offset = range(-1*window,window+1,1)
13
event_equity_list = []
14
for i in range(len(events)):
15
df_tmp = pd.DataFrame(unstack_yield.ix[time_list[i][0].strftime('%Y-%m-%d'):time_list[i][1].strftime('%Y-%m-%d'),events.iloc[i]['secID']].values,columns=[events.iloc[i]['secID_actPubtime']],index = offset)
16
event_equity_list.append(df_tmp)
17
event_equity_window = pd.concat(event_equity_list,axis=1)
18
return event_equity_window
xxxxxxxxxx10
1
def get_mean_outperform_yield(event_equity_window_yield,event_benchmark_window_yield):
2
#获取窗口内平均超额收益
3
cols = list(event_equity_window_yield[(event_equity_window_yield > 0.4) | (event_equity_window_yield < -0.3)].dropna(how='all',axis=1).columns) #删除异常值,一天内的open2open收益最高也就是30%多点,存在借壳上市的公司会有很高的收益,删除这样的样本点
4
for col in cols:
5
del event_equity_window_yield[col]
6
del event_benchmark_window_yield[col]
7
event_equity_window_cumyield = (1 + event_equity_window_yield).cumprod(axis=0) / (1 + event_equity_window_yield).cumprod(axis=0).ix[0] # 累计收益,对齐T+0点(这里之前的版本把event_equity_window_yield写成了event_benchmark_window_yield,导致了后面的分析结果不对)
8
event_benchmark_window_cumyield = (1 + event_benchmark_window_yield).cumprod(axis=0) / (1 + event_benchmark_window_yield).cumprod(axis=0).ix[0]
9
event_outperform_window_yield = (event_equity_window_cumyield - event_benchmark_window_cumyield).mean(axis=1) #超额收益累计平均值
10
return event_outperform_window_yield
接下来我们对几类常见的业绩预告进行时间窗口分析,首先看下有哪些分类,以及其样本数量。
xxxxxxxxxx1
1
events['forecastType'].value_counts()
22 1713024 780011 770251 493021 383023 302441 275213 148412 29931 19732 11014 4729 1Name: forecastType, dtype: int64
可以看出这个API将业绩预告分为13类,我们将样本点过少的状态剔除,分析样本点大于2000的业绩预告事件。它们分别是,22——业绩预增;11——预计亏损;24——盈利下降;51——经营预期及其他(该条描述不明也不予分析);21——预计盈利;23——盈利减缓;41——基本持平
xxxxxxxxxx13
1
def event_window_plot(open2open_yield,benchmarkYield,events,forecastType,window,legend):
2
## 画出各事件时间窗口收益
3
event_equity_window_yield = get_window_yield(open2open_yield,events[events['forecastType']==forecastType],window)
4
event_benchmark_window_yield = get_window_yield(benchmarkYield,events[events['forecastType']==forecastType],window)
5
event_outperform_window_yield = get_mean_outperform_yield(event_equity_window_yield,event_benchmark_window_yield)
6
fig = plt.figure(figsize=(10,6))
7
ax = fig.add_subplot(111)
8
ax.plot(event_outperform_window_yield.index,event_outperform_window_yield.values,'dodgerblue',linewidth=2.5)
9
plt.xlim(-25,25)
10
plt.ylim(event_outperform_window_yield.min(),event_outperform_window_yield.max())
11
ax.plot([0,0],[event_outperform_window_yield.min(),event_outperform_window_yield.max()],'crimson',linewidth=1.5,linestyle ='--')
12
ax.plot([1,1],[event_outperform_window_yield.min(),event_outperform_window_yield.max()],'crimson',linewidth=1.5,linestyle ='--')
13
ax.set_title(legend, fontproperties=font, fontsize=18)
xxxxxxxxxx1
1
event_window_plot(open2open_yield,benchmarkYield,events,22,25,u"盈利预增事件时间窗口分析")
从上图我们可以看出,第一,在业绩预增公告发布时有一个明显的开盘至开盘的超额收益(两条红虚线之间),但这个收益在公开信息的情况下是没法抓到的;第二,业绩预增公告发布之前,股票存在明显的超额收益,表明确实存在公告发布之前就有很大的涨幅;第三,业绩预增公告发布之后,第五天超额收益达到最大,随后超额收益开始下降。
xxxxxxxxxx1
1
event_window_plot(open2open_yield,benchmarkYield,events,11,25,u"预计亏损事件时间窗口分析")
从上图可以看出,预计亏损事件在公告发布之前就有明显的负的超额收益,并在预计亏损事件公布之后快速下跌,不过最后最后下跌基本止住。
xxxxxxxxxx1
1
event_window_plot(open2open_yield,benchmarkYield,events,24,25,u"盈利下降事件时间窗口分析")
从上图可以看出,盈利下降事件在公告发布之前就有明显的负的超额收益,并在盈利下降事件公布之后快速下跌,并在25个时间窗口内持续下跌。
xxxxxxxxxx1
1
event_window_plot(open2open_yield,benchmarkYield,events,21,25,u"预计盈利事件时间窗口分析")
从上图中可以看出,预计盈利事件表现出在公告发布之前有一定的超额收益,在公告发布时有一个显著的超额收益;但在随后表现平稳,并没有超额收益,也没有出现下跌。
xxxxxxxxxx1
1
event_window_plot(open2open_yield,benchmarkYield,events,23,25,u"盈利减缓事件时间窗口分析")
从上图可知,盈利减缓事件,呈现出公告发布前几乎没有超额收益,公告发布当日有明显的超额收益,不过在随后25个交易日内出现大幅下跌。
xxxxxxxxxx1
1
event_window_plot(open2open_yield,benchmarkYield,events,41,25,u"基本持平事件时间窗口分析")
从上图可知,基本持平事件,呈现出公告发布前几乎没有超额收益,公告发布的两日呈现出负的超额收益,随后超额收益继续小幅下跌。
五、事件驱动分析转化为投资策略
上面的事件分析给了我们另一个角度来看股票的超额的收益,然而事件的发生实际上是离散的,上述分析无法给出我们一个投资策略。本文提供一个简易的思路来将事件驱动分析转化为投资策略。
我们知道我们上面的分析是将每一个处于事件的股票累计收益的平均,故一个直接的想法便是将所有处于事件的股票等权买入,但因为事件是离散的,我们在一个持有期会面临着新的处于事件的股票需要买入,而导致实际上我们对每一个股票并不都是同一权重的。此处给出的解决办法是,首先计算出处于事件窗口期的最大股票个数,然后以这个最大股票个数的倒数做为每一只处于事件的股票的权重,这样就能保证所有处于事件的股票以同一权重跑完整个窗口期。
xxxxxxxxxx16
1
def get_max_event_stock(events):
2
##获得窗口期有交叉的最大股票个数
3
events['minute'] = events['actPubtime'].map(lambda x:x[11:20])
4
events['actpubdate'] = events['actPubtime'].map(lambda x: datetime.datetime.strptime(x[0:10],'%Y-%m-%d'))
5
events['ii'] = range(len(events))
6
events['buytime'] = events['ii'].map(lambda x: cal.advanceDate(events.iloc[x]['actpubdate'],'1B') if events.iloc[x]['minute'] != '00:00:00' else Date.fromDateTime(events.iloc[x]['actpubdate']))
7
calendar_date = DataAPI.TradeCalGet(exchangeCD=u"XSHG",beginDate=u"20071227",endDate=u"",field=u"",pandas="1")['calendarDate']
8
calendar_date = pd.DataFrame(calendar_date)
9
calendar_date['calendarDate'] = calendar_date['calendarDate'].map(lambda x:Date.parseISO(x))
10
11
def get_event_stock(trade_date):
12
event_stock = []
13
begin_date = cal.advanceDate(trade_date,'-5B')
14
return events[(events['buytime'] < trade_date) & (events['buytime'] >= begin_date)]['secID'].tolist()
15
calendar_date['stk_list'] = calendar_date['calendarDate'].map(lambda x: get_event_stock(x))
16
return max([len(item) for item in calendar_date['stk_list'].values])
xxxxxxxxxx1
1
test = events[events['forecastType']==22]
xxxxxxxxxx1
1
get_max_event_stock(test)
355
如上我们得到了盈利预增事件的5天窗口期有交叉的最大股票个数,355;故我们编写如下策略。
xxxxxxxxxx45
1
import numpy as np
2
import pandas as pd
3
from CAL.PyCAL import *
4
start = '2007-12-27' # 回测起始时间
5
end = '2016-11-03' # 回测结束时间
6
benchmark = 'HS300' # 策略参考标准
7
universe = set_universe('A') #证券池,支持股票和基金
8
capital_base = 100000000 # 起始资金
9
freq = 'd' # 策略类型,'d'表示日间策略使用日线回测,'m'表示日内策略使用分钟线回测
10
refresh_rate = 1 # 调仓频率,表示执行handle_data的时间间隔,若freq = 'd'时间间隔的单位为交易日,若freq = 'm'时间间隔为分钟
11
cal = Calendar('China.SSE')
12
period = '-1B'
13
14
def initialize(account): # 初始化虚拟账户状态
15
account.day_count ={}
16
for stk in universe:
17
account.day_count[stk] = 0
18
19
def handle_data(account): # 每个交易日的买入卖出指令
20
21
yestoday = cal.advanceDate(account.current_date, period, BizDayConvention.Unadjusted)
22
begin_date = cal.advanceDate(account.current_date, '-1Y', BizDayConvention.Unadjusted)
23
profit_up = DataAPI.FdmtEfGet(secID=account.universe,publishDateBegin=begin_date.strftime('%Y%m%d'),publishDateEnd=account.current_date.strftime('%Y%m%d'),
24
forecastType="22",field=['secID','actPubtime',],pandas="1")
25
26
fil_profit = profit_up[profit_up['actPubtime'].map(lambda x: x[0:10]) == yestoday.strftime('%Y-%m-%d')]
27
28
buy_list = fil_profit['secID'].tolist()
29
30
total_money = account.reference_portfolio_value
31
prices = account.reference_price
32
33
for stk in account.avail_security_position:
34
if account.day_count[stk] == 5:
35
order_to(stk,0)
36
account.day_count[stk] = 0
37
38
for stk in buy_list:
39
if stk not in account.security_position:
40
if np.isnan(prices[stk]) or prices[stk] == 0:
41
continue
42
order_pct_to(stk,1.0/355)
43
44
for stk in account.security_position:
45
account.day_count[stk] = account.day_count[stk] + 1
年化收益率2.5%
基准年化收益率-5.1%
阿尔法-1.0%
贝塔0.09
夏普比率-0.34
收益波动率5.6%
信息比率0.13
最大回撤9.2%
换手率43.08
Created with Highstock 1.3.10累计收益率策略基准2008200920102011201220132014201520160.00%-75.00%-50.00%-25.00%25.00%50.00%2010-08-09策略: -3.95%基准: -44.57%
查看回测详情,以及回测曲线可以看出,盈利预增事件自带择时属性;牛市时,该事件相对较多,而在熊市时,该事件相对较少。
六、总结
本文以业绩预告为例,给出了一个事件驱动的基本分析过程,并给出最后的demo策略。通过上面的分析过程,我们可从另一个角度来看股票的超额收益,将离散的事件规整到同一分析维度,来看这类事件是否是存在超额收益的。但最后我们做策略时,我们还是要面对离散的事件,如果各事件窗口不存在交叉的话,我们可以很简单使用凯利公式来得到我们对每个事件的最优仓位。然而大部分时候,事件的窗口都存在很多的交叉,如上策略我们做了一个最大股票个数平均仓位的处理,但是这样资金利用率非常低,而且存在事件个数在各年份不一定稳定的问题,所以在真实情况下,我们对未来该事件发生的次数做一个预测或许是一个更好的方式。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
使用notebook打造日内监控市场数据的利器V0.5
数据预处理(上)之离群值处理、标准化 | RiceQuant米筐量化社区 交易策略论坛
【止损】 止损/止盈 方案目录 必读 | RiceQuant米筐量化社区 交易策略论坛
量化交易策略——小市值策略(四)
次新股开板投资攻略
博士云操作复盘和量化方向
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服