[量化学堂-策略开发]事件驱动策略(基于业绩快报)

事件驱动
业绩快报
标签: #<Tag:0x00007f5c01955a38> #<Tag:0x00007f5c01955880>

(iQuant) #1

事件驱动(Event Driven)属于量化投资之中的一个重要类别,涵盖投资机会广泛。广义上说,市场上任何发生的有可能与股票市场相关的新闻、事件、公告均有可能成为事件驱动的投资机会。 目前我国业界事件驱动策略中包括的常用重大事件有:业绩预告、业绩快报、分红送转、大股东增减持、高管增减持、定向增发、限售股解禁、股权激励、重组并购、ST摘和评级上调等,如下图所示。

可以看出,目前市场经过验证有效的事件已经不少,涵盖了影响股票价格的多个方面。事件驱动策略由于其策略逻辑的独特性,因此与其他常规股票策略相关性很低,再加上事件众多,资金容量大这一特点,使得事件驱动策略成为国外对冲基金非常大类的投资策略。

BigQuant回测引擎能够快速验证事件的有效性,从而开发事件驱动策略。为方便小伙伴顺利开发事件驱动策略,本文以业绩快报中净利润大幅增长为事件,验证该事件是否可以带来超额收益,未来我们会发布更多的基于其他事件的策略,敬请耐心等待。

在回测之前,我们先看策略的完整介绍:

  • 策略逻辑:认为业绩快报中净利润大幅增长为利好消息,会导致价格在一定期限内上涨
  • 事件定义:当业绩快报中公布净利润同比增增长超过30%
  • 股票持有不超过50只,仅当持有数量小于50只时,才买入股票
  • 持有时间:40个交易日

我们的策略流程是:每日更新数据,查看当日发布财务报表并且(归属母公司)净利润季度同比增长率超过30%的公司,如果出现这样的事件,就买入该股票。因此这里的分析和"选股系列"以及"大师系列"的策略依然相似,大家可以结合来看。

策略回测结果为:

从测试结果来看,该事件驱动策略为长期正收益系统。因为财报公布事件会有一个期限规定(年度报告是每年结束后4个月内,半年度是上半年结束后2个月内,季度报告是季度结束后1个月内),所以某些时间段不会有公司公布财务报表,当然那段时间就不会出现业绩快报净利润大幅增长的事件,因此仓位很多时候并不是100%。

备注:上市公司财报披露时间一般是财报发布日期的前一天晚上8点,因此策略回测中订单生成时间是财报公布日前一天,目的是便于回测和实盘保持一致性。

想要查看完整策略,点击 克隆策略一键搞定。

克隆策略

事件驱动策略:基于业绩快报

数据准备函数

In [1]:
def prepare(context):
    instruments = context.instruments
    start_date = context.start_date
    end_date = context.end_date
    # 获取数据
    data = D.financial_statements(instruments, start_date, end_date, 
          fields=['instrument','fs_publish_date','fs_quarter_year',
          'fs_quarter_index','fs_net_profit_yoy'])
    # 选择净利润同比增长率大于30%的股票
    selected = data[data['fs_net_profit_yoy'] > 30] 

    # 获取交易日历
    date = D.trading_days(start_date=start_date,end_date=end_date)
    # 将日期型格式转化为字符型格式
    date = date['date'].apply(lambda x : x.strftime('%Y-%m-%d'))
    # 为尽量接近实盘,事件日期应为财报公布日的前一天
    publish_date = date.shift(-1)
    shift = dict(zip(date, publish_date))

    # 建立事件表
    event = {}
    for dt in date:
        if type(shift[dt]) is str:
            event[dt] = list(selected[selected['fs_publish_date'] == shift[dt]].sort_values(
                'fs_net_profit_yoy',ascending=False ).instrument)
        else:
            event[dt] = []    
    context.event = event 

策略逻辑主体函数

In [2]:
def initialize(context):
   
    context.set_commission(PerOrder(buy_cost=0.0003, sell_cost=0.0013, min_cost=5)) 
    context.daily_buy_stock = context.event
    context.hold_periods = 40 # 持有40天,固定持仓期
    context.stock_max_num = 50 # 最大持仓数量为50只
    context.hold_days = {}
     
def handle_data(context,data):
  
    date = data.current_dt.strftime('%Y-%m-%d') # 日期
    
    # 目前仓位里面的股票列表
    equities = {e.symbol: e for e, p in context.perf_tracker.position_tracker.positions.items()}
    
    for k in equities.keys():
        # 如果持仓时间大于40天
        if context.trading_day_index - context.hold_days[k] >= context.hold_periods \
               and data.can_trade(context.symbol(k)):
            # 卖完
            context.order_target_percent(context.symbol(k),0)
   
    # 还允许建仓的股票数目
    stock_can_buy_num = context.stock_max_num - len(equities)
    # 获取当日买入股票的代码
    stock_to_buy = context.daily_buy_stock[date][:stock_can_buy_num]

    # 等权重买入 
    weight =  1 / context.stock_max_num
    
    # 买入
    for stock in stock_to_buy:
        if data.can_trade(context.symbol(stock)):
            context.order_target_percent(context.symbol(stock), weight)
            # 记录建仓时间的日期索引
            context.hold_days[stock] = context.trading_day_index  

策略回测接口

In [3]:
m=M.trade.v2( 
    instruments=D.instruments(market='CN_STOCK_A'),
    start_date='2010-01-01', 
    end_date='2017-08-01',
    prepare=prepare,
    initialize=initialize,
    handle_data=handle_data,
    order_price_field_buy='open',
    order_price_field_sell='open',
    capital_base=1000000,
    benchmark='000300.INDX',
)
[2018-03-22 19:58:10.576019] INFO: bigquant: backtest.v7 开始运行..
[2018-03-22 19:58:24.447598] INFO: algo: set price type:backward_adjusted
[2018-03-22 20:00:02.144660] INFO: Blotter: 2011-03-01 cancel order Equity(3437 [000973.SZA]) 
[2018-03-22 20:00:02.186730] INFO: Blotter: 2011-03-02 cancel order Equity(1775 [000977.SZA]) 
[2018-03-22 20:00:02.451034] INFO: Blotter: 2011-03-04 cancel order Equity(2962 [000629.SZA]) 
[2018-03-22 20:00:02.512531] INFO: Blotter: 2011-03-07 cancel order Equity(2647 [002422.SZA]) 
[2018-03-22 20:00:02.574669] INFO: Blotter: 2011-03-08 cancel order Equity(2298 [600114.SHA]) 
[2018-03-22 20:00:02.641604] INFO: Blotter: 2011-03-09 cancel order Equity(1213 [600179.SHA]) 
[2018-03-22 20:00:02.667457] INFO: Blotter: 2011-03-10 cancel order Equity(3087 [600567.SHA]) 
[2018-03-22 20:00:02.696841] INFO: Blotter: 2011-03-11 cancel order Equity(617 [600728.SHA]) 
[2018-03-22 20:00:02.754833] INFO: Blotter: 2011-03-15 cancel order Equity(3354 [600221.SHA]) 
[2018-03-22 20:00:02.787623] INFO: Blotter: 2011-03-16 cancel order Equity(355 [002210.SZA]) 
[2018-03-22 20:00:02.857295] INFO: Blotter: 2011-03-17 cancel order Equity(1132 [002092.SZA]) 
[2018-03-22 20:00:02.908875] INFO: Blotter: 2011-03-18 cancel order Equity(2574 [600584.SHA]) 
[2018-03-22 20:00:02.943216] INFO: Blotter: 2011-03-21 cancel order Equity(773 [000902.SZA]) 
[2018-03-22 20:00:11.496672] INFO: Blotter: 2012-03-01 cancel order Equity(2922 [000799.SZA]) 
[2018-03-22 20:00:11.542353] INFO: Blotter: 2012-03-02 cancel order Equity(1863 [300105.SZA]) 
[2018-03-22 20:00:11.637673] INFO: Blotter: 2012-03-05 cancel order Equity(2656 [300127.SZA]) 
[2018-03-22 20:00:11.705130] INFO: Blotter: 2012-03-06 cancel order Equity(2992 [600702.SHA]) 
[2018-03-22 20:00:11.736938] INFO: Blotter: 2012-03-07 cancel order Equity(456 [002321.SZA]) 
[2018-03-22 20:00:11.767191] INFO: Blotter: 2012-03-08 cancel order Equity(3062 [000795.SZA]) 
[2018-03-22 20:00:11.811717] INFO: Blotter: 2012-03-09 cancel order Equity(3379 [000822.SZA]) 
[2018-03-22 20:00:11.848026] INFO: Blotter: 2012-03-12 cancel order Equity(3301 [300195.SZA]) 
[2018-03-22 20:00:11.909838] INFO: Blotter: 2012-03-13 cancel order Equity(1836 [600203.SHA]) 
[2018-03-22 20:00:11.947694] INFO: Blotter: 2012-03-14 cancel order Equity(3364 [300142.SZA]) 
[2018-03-22 20:00:11.995203] INFO: Blotter: 2012-03-16 cancel order Equity(3465 [000030.SZA]) 
[2018-03-22 20:09:49.689308] INFO: Performance: Simulated 1841 trading days out of 1841.
[2018-03-22 20:09:49.694649] INFO: Performance: first open: 2010-01-04 01:30:00+00:00
[2018-03-22 20:09:49.696548] INFO: Performance: last close: 2017-08-01 07:00:00+00:00
  • 收益率93.98%
  • 年化收益率9.49%
  • 基准收益率5.44%
  • 阿尔法0.07
  • 贝塔0.55
  • 夏普比率0.23
  • 胜率0.487
  • 盈亏比1.471
  • 收益波动率21.82%
  • 信息比率0.43
  • 最大回撤39.26%
[2018-03-22 20:10:18.296940] INFO: bigquant: backtest.v7 运行完成[727.720889s].

参考文献
1.“事件驱动策略在国内能不能行得通?” 知乎回答
2.“全面解析对冲基金事件驱动策略” 上海交大 陈欣
3.事件驱动策略系列研报 广发证券
4.事件驱动策略系列研报 长江证券


社区干货与精选整理(持续更新中...)
(chenjianjia) #2

if type(shift[dt]) is str:这个判断是否多余?shift[dt]意思是吧date往后平移一天,把shift[dt]打印出来看,所有date都有对应的后一天交易日历的值,且都是str类型,我觉得可以不用这个判断

这里面为什么p也要参与循环?不是只需要e就可以了吗?


(iQuant) #3

你的第一个问题,这样写只是更稳健,我们会再组织下代码,提高代码质量。
第二个问题,持仓positions是一个字典,e,p参与循环后,e就为字典的键名,p就为字典的键值。这样键名键值一一对应。也可只对e进行循环,这里e就是包含键名、键值的一个元组。