土法炼钢兴趣小组的算法知识备份

【量化交易】事件驱动策略:财报、并购、指数调整

文章导航

分类入口
quant
标签入口
#event-driven#earnings#ma#index-rebalance#drift

目录

事件驱动(event-driven)是量化策略族里最容易被误解的一类。表面上,它和散户嘴里的「炒题材」「跟消息」高度重叠——都需要等一个外部事件落地,再围绕这个事件押注价格变化。但只要把任意一个具体事件——比如某一家上市公司的季度财报——拿放大镜看,就会发现两者完全不在一个维度:散户做的是「猜下一份财报会不会超预期」,事件驱动策略做的是「在财报公告之后、市场对这份盈余意外的吸收过程还没结束之前,把可统计的漂移收益拿走」。前者是预测,后者是套利;前者是不可证伪的故事,后者是十几篇顶级 finance 期刊文章在 60 年间反复验证、又被中国 A 股每一份公开数据再验证一次的现象。

把事件驱动当成一个工程问题来做,意味着三件事必须同时具备:第一,事件本身有明确的、可在事前规则化定义的触发条件——「财报披露」「指数样本调整公告」「股东大会通过并购方案」——而不是「公司发了一条利好新闻」这种事后归因;第二,事件之后存在一个统计上稳定的、可重复验证的价格反应模式,这个模式的存在性本身可以用 event study 方法做显著性检验;第三,从事件触发到反应消散之间存在一个有限的、可执行的交易窗口,能容下从信息抓取、信号生成、风控通过、报单到撮合的全部链路。三者缺一不可。少了第一条,策略退化成主观操作;少了第二条,策略退化成赌博;少了第三条,策略退化成纸上盈利。

这篇文章不打算把每一类事件都做成科普介绍,而是把事件驱动这一族策略统一到「信息传播链」这一个抽象之下,然后在抽象之上挂载若干具体事件——PEAD(post-earnings announcement drift,财报后漂移)、并购套利(merger arbitrage)、指数调整套利(index rebalance arbitrage)、解禁、回购、宏观日历——再交付两段可运行的 Python 代码:一段用真实可生成的事件日历做 AAR/CAR 事件研究,一段用 vectorbt 把 PEAD 漂移模拟成可回测的多空组合。

范围与版本说明:本文涉及的市场规则口径以 2024 年至 2025 年公开材料为准,包括上交所、深交所《交易规则》(2024 版)、中证指数公司《指数样本调整规则》、MSCI Global Investable Market Indexes Methodology(2024-11)、FTSE Russell Ground Rules(v9.4,2024)、SEC Schedule TO 与 Hart-Scott-Rodino Antitrust Improvements Act 相关规则,以及主流学术论文(Bernard & Thomas 1989/1990、Fama et al. 1969、Mitchell & Pulvino 2001、Chen Noronha & Singal 2004、Petajisto 2011 等)。代码示例使用 Python 3.11,依赖 numpypandasscipyvectorbt,全部可在普通笔记本上运行;SVG 图位于本文同目录 images/ 下。

风险提示:本文出现的事件、阈值、收益率、SUE 分组结果均基于公开方法学构造,部分数据为可复现的模拟数据,仅用于阐释策略机制。事件驱动策略在真实市场上面对的关键风险包括:盈余预告口径偏差、并购 deal break、指数调整窗口被反向资金抢跑、解禁公告被择时披露、宏观数据被预先泄漏(leak)等。任何策略上线前必须经过样本外回测、与可执行成本(冲击成本、印花税、过户费、融券费)匹配的实盘对照测试,并通过合规与风控审查。本文不构成任何投资建议。


一、事件驱动策略概览

一·一 什么是「事件」

把一个事件(event)从一段新闻里抽取出来,需要先回答两个问题:它是不是可在事前定义的?——也就是不依赖事后再找出对应的价格行情来标注;它是不是可被市场结构识别的?——也就是这个事件本身在市场层面留下了可被自动化系统抓到的触发器。

满足这两个条件的事件可以分为三类。

第一类是公司事件(corporate events),由发行人或控制人主动触发:定期报告(年报、半年报、季报)、业绩预告与业绩快报、股东大会决议、董事会回购方案、并购重组预案、股票发行(再融资、可转债、配股)、限售股解禁、分红送转、分拆上市、退市与摘牌。在 A 股,这些事件通过交易所的临时公告与定期报告披露通道发布,所有上市公司都受同一套披露规则约束(《上市公司信息披露管理办法》、上交所《股票上市规则》、深交所《股票上市规则》),所以可以在公告流(announcement feed)层面统一抓取。

第二类是市场结构事件(market structure events),由交易所、指数公司、监管机构触发:指数样本调整(沪深 300、中证 500、MSCI、富时罗素、标普 500)、ETF 申购赎回机制变化、做市商名单更新、停复牌、临时停牌阈值触发、股票上市与退市、衍生品上市与到期。这一类事件的特点是「日历可预排」——指数公司提前两到四周公告调整结果,并固定在某个生效日(effective date)执行。这给了策略一个明确的可交易窗口。

第三类是宏观事件(macroeconomic events),由央行与官方统计部门触发:FOMC 利率决议、ECB 利率决议、中国央行 MLF/OMO/LPR 公告、CPI、PPI、PMI、非农就业(NFP)、零售销售、进出口数据、社融与 M2。这一类事件的影响是市场层面的(systematic),而不是个股层面的(idiosyncratic),需要用指数、利率、汇率工具去吸收,而不是用个股做承载。

三类事件背后的逻辑差异显著:公司事件主要承载个股层面的盈利或现金流冲击,宏观事件主要承载折现率(discount rate)冲击,市场结构事件主要承载流动性与持仓重构(demand-driven)冲击。这三类冲击在因子归因里都有对应的位置,把事件归到正确的桶里,是后面写信号代码时不犯方向错误的前提。

一·二 信息传播链

任何一个事件落到价格上的过程都可以拆成五个阶段,这五个阶段是事件驱动策略的最基本时序框架。

[t = -T : 预期形成阶段]
   ├── 卖方分析师覆盖、买方持仓预先建立
   ├── 信息泄漏(leak)或者市场对类似公司的横向类推
[t = 0 : 事件触发]
   ├── 公告、披露、宣布、生效
[t = 0+ : 信息扩散阶段]
   ├── 媒体二次传播、卖方电话会议、研报推送
   ├── 算法新闻提取、情绪分类、关键字打标
[t = 0+ : 价格反应阶段]
   ├── 跳变(jump):撮合开盘第一笔成交
   ├── 漂移(drift):之后若干日的方向性价格调整
[t = T : 收敛阶段]
   ├── 反应被市场充分吸收,超额收益归零
   └── 套利窗口关闭

策略的可交易窗口落在「t = 0 事件触发」之后、「t = T 收敛」之前。具体落在哪个子区间,决定了你要做的是新闻驱动(news-driven,0 到 0+ 数分钟内)、漂移交易(drift trading,0 之后数日到数月)、还是事件套利(event arbitrage,事件本身存在不确定性,赌它最终落地的概率)。三者对基础设施的要求差别巨大:新闻驱动要求毫秒级延迟与同位托管,漂移交易在分钟到小时级延迟下完全可行,事件套利则需要法律分析与跨期资金管理能力。

一·三 事件驱动 vs 因子驱动

事件驱动经常和因子驱动(factor-driven)放在一起讨论。两者的关键差别在「触发器」上:因子策略每天对全市场截面做排序、再持有头部组合直到下一个调仓周期,触发器是时间;事件策略在事件发生当天才把对应标的加入候选集,触发器是事件本身。结果是事件驱动策略的持仓在时间轴上是稀疏的、不规则的,而因子策略的持仓是周期性的、稠密的。

这一差别直接影响到风控与归因。事件驱动策略的暴露往往集中在事件发生当天与之后若干天,超额收益高度集中在少数交易日上,这意味着用日频 Sharpe 评估事件策略会严重低估其稳定性,必须改用「按事件计算」的统计量(per-event return、win rate、t-stat of CAR)。


二、PEAD:财报后漂移

财报后漂移(post-earnings announcement drift, PEAD)是事件驱动里被最广泛验证、也最容易在工程上复现的现象。它最早由 Ball & Brown(1968)观察到,被 Bernard & Thomas(1989, 1990)系统化为可统计检验的形态:在公司公告盈利意外(earnings surprise)之后的若干周内,价格会沿着意外方向继续漂移,这种漂移与公告日的初始跳变同号、且在长达 60 个交易日内显著为正(或为负)。

二·一 标准化盈余意外(SUE)

衡量盈利意外有两类口径:基于历史时间序列模型的口径与基于分析师一致预期的口径。前者由 Foster(1977)提出,本质是用过去若干季度同比 EPS 的均值与标准差,把当季 EPS 同比偏离做标准化:

\[ \text{SUE}_{i,t} = \frac{\text{EPS}_{i,t} - E[\text{EPS}_{i,t} \mid \mathcal{F}_{t-1}]}{\sigma(\text{EPS}_{i,t} \mid \mathcal{F}_{t-1})} \]

其中条件期望与标准差由 seasonal random walk with drift 模型估计:

\[ E[\text{EPS}_{i,t}] = \text{EPS}_{i,t-4} + \delta_i, \quad \delta_i = \frac{1}{8}\sum_{k=1}^{8}(\text{EPS}_{i,t-k} - \text{EPS}_{i,t-k-4}) \]

后者直接用市场一致预期:

\[ \text{SUE}_{i,t} = \frac{\text{EPS}_{i,t} - \text{Consensus}_{i,t}}{\sigma(\text{Consensus}_{i,t})} \]

两种口径的差异在 A 股尤为显著:A 股没有像美股 IBES 那样长期、广覆盖的一致预期数据库,分析师覆盖率在中小盘上稀疏,因此基于时间序列模型的 SUE 在 A 股的可用性更高。但时间序列 SUE 对会计调整、合并报表口径变化、季节性更强的行业(如建筑、农业)有偏差,需要做行业内 winsorize 与标准化。

工程实现里把 SUE 拆成三个步骤:

  1. 采集:从交易所披露与 Wind/Choice 等数据源拉取季度 EPS、披露日时间戳;
  2. 计算:在每只股票的时间序列上估计 δ 与 σ,得到 SUE;
  3. 横截面分组:在每个公告窗口(A 股有四个集中披露期:4 月底、8 月底、10 月底、年报次年 4 月底)内,对当期所有公告样本按 SUE 做十分位分组,记录每只股票的分组编号 Q1 到 Q10。

二·二 A 股盈利惊喜的衰减节奏

把 A 股全市场的 SUE 十分位分组与公告后 60 日 CAR(cumulative abnormal return,累计超额收益)画在同一张图里,得到的形态非常稳定:Q10 组(最高 SUE)在公告日附近跳变约 +1.5% 到 +2.5%,之后 60 日继续漂移 +1% 到 +2%;Q1 组(最低 SUE)则相反,跳变 -1.5% 到 -2.5%,再继续漂移 -1% 到 -2%。中间分组的曲线接近水平,没有显著漂移。

PEAD 漂移曲线:按 SUE 十分位分组的累计超额收益

漂移的衰减节奏有几个值得注意的工程性事实:

第一,漂移在公告后第 1 到第 20 个交易日最强,第 20 到第 40 日开始减速,第 40 到第 60 日基本走平。这意味着 PEAD 策略的最优持有期不是越长越好,而是 20 到 30 个交易日。把持有期延长到 60 日以上,新增的超额收益不足以覆盖换手率与冲击成本。

第二,A 股的 PEAD 强度与板块、市值、流动性高度相关。中小盘的 PEAD 漂移幅度大于大盘,但执行成本(冲击成本、买卖价差)也更高;金融板块的 PEAD 较弱,可能与该板块分析师覆盖密度高、盈余意外更易被预先吸收有关。

第三,与日历集中效应有关:A 股财报披露集中在四个窗口,导致 PEAD 信号在这四个窗口内被批量触发,市场层面的资金重配会让某些行业出现「行业内同 SUE 排序」的整体漂移。这一点对单股层面的 PEAD 策略其实是有利的:行业级别的协同漂移降低了个股 idiosyncratic 噪声。

第四,「业绩快报」与「业绩预告」是 A 股特有的信号源。在正式季报公告之前,公司可能已经发布业绩预告(净利润同比变动区间)或业绩快报(未经审计的财务数据)。这两类预披露的发布日往往领先正式季报 1 到 4 周。把 SUE 计算的「事件触发日」从正式季报披露日前移到业绩预告披露日,可以显著提前进入 PEAD 漂移区间,但代价是预告口径的不精确(区间值需要做中点估计或保守端估计)。

二·三 PEAD 失效的几种情况

PEAD 不是任何时候都有效。下面几类样本应当从信号集合里剔除,或者单独标记:


三、并购套利

并购套利(merger arbitrage,又称 risk arbitrage)是事件驱动策略里最早被对冲基金商业化的一支。它的基本逻辑是:当一家收购方(acquirer)公开宣布以特定条款收购目标方(target)后,目标方股价会从公告前价格跳到接近要约价的水平,但仍然存在一个折价(spread),这个折价反映了市场认为交易最终失败的概率乘以失败时的价格回落。套利者建仓目标方,等到交易完成时拿走 spread;如果交易失败(deal break),承受相应损失。

三·一 现金要约与换股要约

按对价支付方式,并购可以分为三类:

三种结构下的 spread 定义都是「按对价折算后的目标价格」与「目标方实际股价」之差。现金要约的 spread 计算最直观:

\[ \text{spread}_t = \text{offer} - S^{\text{target}}_t \]

换股要约要把对价折算成现价:

\[ \text{spread}_t = r \cdot S^{\text{acquirer}}_t - S^{\text{target}}_t \]

其中 r 是换股比例。当 r 不是固定数字而是带有 collar(保护带)条款时,公式变成分段线性的,要按 acquirer 当前股价位置选不同分支。

三·二 spread 与隐含成功概率

把 spread 翻译成成功概率有一个标准的两态模型:

\[ S_t = p \cdot \text{offer} + (1 - p) \cdot S^{\text{break}} \]

其中 \(S^{\text{break}}\) 是市场预估交易失败时目标方股价回落到的水平,常用的代理是公告前 5 日 VWAP(volume-weighted average price,成交量加权平均价)。解出 p:

\[ p = \frac{S_t - S^{\text{break}}}{\text{offer} - S^{\text{break}}} \]

把每一只活跃 deal 当前的 p 算出来,按 1 - p 排序,可以得到「市场认为最容易破裂」的交易清单。这份清单是专业 risk arbitrage 桌的核心监控对象:高 spread(低 p)的 deal 通常正在经历某个具体阻力——反垄断审查、股东会反对、融资条件变更——把这些阻力点拆开来看,往往能发现市场对某一具体风险事件的过度反应或反应不足。

三·三 deal break 风险的时间结构

并购从公告到完成的时间线上有若干关键事件节点,spread 在这些节点附近会出现跳变。

并购套利:spread 时间线与关键事件节点

典型的现金要约时间线(以美股为例):

A 股并购重组的时间线略有不同。重大资产重组需要经过证监会并购重组委审核(部分是注册制),还要通过国资委、商务部、外汇局等的批文。整个过程的不确定性主要集中在「证监会审核」这一关,spread 的最大走宽点也在这一关临近时。

三·四 工程实现要点

并购套利的工程难点不在执行,而在数据结构与状态机:

  1. deal 主数据:需要维护一份活跃 deal 清单,字段包括 acquirer、target、announcement_date、offer_price、exchange_ratio、deal_status(announced / shareholder_vote / regulatory_review / closed / terminated)、expected_close_date 等。这份清单的更新依赖公告抓取,不是日频,而是事件驱动。
  2. 状态机:每个 deal 有自己的状态机(announced → SH approved → regulatory approved → closed),每次状态转移触发 spread 重估、仓位重平衡。
  3. break 价格估计\(S^{\text{break}}\) 不是固定值。对成熟市场可以用「公告前 5 日 VWAP」,对 A 股要叠加同期行业指数的反向变动做行业调整。
  4. 资金成本与对冲费用:换股要约下做空 acquirer 涉及融券费用,A 股融券池有限、费用高,导致部分 deal 实际不可对冲,需要在策略层面加白名单。

四、指数调整

指数调整(index rebalance)是市场结构事件里最容易工程化的一类,因为指数公司会在调整生效前公告样本变更,给套利者一个明确的事前窗口。

四·一 主要指数的调整规则

指数 调整频率 公告提前期 生效时点
MSCI 全球指数(季度调整) 每季 SAIR 约 10 个交易日 调整生效日收盘
MSCI 全球指数(半年调整) 5 月、11 月 SAIR 约 10 个交易日 调整生效日收盘
FTSE Russell(半年调整) 3 月、9 月 约 5 个交易日 生效日开盘前
沪深 300、中证 500(半年调整) 6 月、12 月 约 14 个自然日 下一个交易日开盘
标普 500 不定期,季度集中 约 5 个交易日 生效日开盘前

这些规则有两个工程上有用的特征:

第一,生效时点是已知的——大多数指数都在生效日开盘或收盘集中调仓,跟踪指数的被动资金(index fund、ETF)会在那个时点产生大量买卖单。这一波资金流动是套利者的对冲对象。

第二,公告与生效之间有窗口——这个窗口的长度决定了套利者有多少时间建仓、释放。对 MSCI 这种公告期约 10 个交易日的指数,套利窗口长但价格反应分散;对 FTSE 这种公告期较短的指数,窗口短但反应集中。

四·二 抢跑(front-running)与反向交易

最经典的指数调整套利策略是抢跑:在公告之后、生效日之前,与即将进入指数的股票同向、与即将剔除的股票反向建仓,等到生效日被动资金推动价格变化时平仓。Chen, Noronha & Singal(2004)与 Petajisto(2011)等都对该策略做过系统验证:标普 500 调入股票从公告到生效日平均累计超额收益约 +5%(窗口内),剔除股票约 -5%。

这一策略的有效性依赖于「被动跟踪资金的规模」——如果跟踪某指数的资金占指数自由流通市值比重高,调整时的需求冲击就大,价格反应就强。MSCI 与标普 500 是规模最大的两类指数,沪深 300 由于有大量 ETF 跟踪也具备显著效应。

但抢跑策略最近十年的有效性显著下降,原因有两个:第一,更多套利者参与抢跑,价格反应被分摊到公告日之前(pre-announcement leak)和公告日当日;第二,部分指数提供商修改了样本调整规则,允许跟踪基金在生效日前后多日内分批调仓,把瞬时需求冲击平滑掉。这要求策略实现时不能机械照搬学术文献里的窗口选择,而要按当前市场结构重新校准。

反向交易(reversal trading)是另一种思路:在生效日之后做与抢跑相反的方向,赌的是被动资金推高(推低)的价格会回归。Greenwood(2008)发现日经 225 调整带来的价格冲击约有 30% 在 60 个交易日内回归,这给反向交易提供了基础。在 A 股,沪深 300 调整后的回归更为明显(流动性较弱),但要避开调整本身导致的成份股流动性结构变化(被纳入后流动性永久提升)。

四·三 工程实现要点

  1. 指数样本数据库:维护每个指数当前的成份股、自由流通比例(FIF)与权重。每次调整公告后增量更新。
  2. 预测模型(针对 MSCI 等公告前不公布完整名单的指数):基于公开规则与上次调整后的规模/流动性变化,预测可能被调入调出的股票名单。MSCI 与 FTSE 的规则书(rule book)公开,预测准确率可以做到 70% 到 85%。
  3. 资金需求量估算:跟踪基金 AUM × 待调入股票目标权重,得到需求买入金额。除以 ADV(average daily volume,平均日成交量)得到需求换手压力,作为预测价格冲击的输入。
  4. 执行约束:抢跑策略必须在公告日开盘后立即开始建仓,避开自身造成进一步价格冲击。常见做法是用 VWAP 算法在窗口内分散建仓。

五、分拆、回购、IPO 解禁

五·一 限售股解禁

A 股 IPO 与定增带来的限售股解禁(unlock / lockup expiration)是一类高频、可预测的事件。首发限售(控股股东 36 个月,董监高与战略投资者 12 个月)、定增限售(机构投资者 6 个月或 18 个月)的到期日都是确定的,可以提前数月构造解禁日历。

学术上 Ofek & Richardson(2003)、Field & Hanka(2001)发现美股 IPO 解禁前后存在显著负超额收益,A 股市场的研究(如《金融研究》上若干篇)也得到一致结论:解禁公告期前 5 日到解禁后 10 日,标的股票出现累计 -1% 到 -3% 的超额收益,解禁规模占流通股比重越大、机构集中度越高,负效应越强。

工程实现上,解禁事件的几个细节值得注意:

五·二 回购公告

股票回购(share buyback / repurchase)公告是另一类系统性事件。Ikenberry, Lakonishok & Vermaelen(1995, 2000)在美股研究中发现公开市场回购公告后存在长期正超额收益(公告后 4 年累计约 +12%),且在「价值股」中更显著。

A 股的回购规则在 2018 年《公司法》修订与 2019 年监管规则细化后明显放宽,回购方案也分化出更精细的类别:「用于实施员工持股计划或股权激励」的回购公告效应较弱(市场预期股票最终回到流通),而「用于减少注册资本」的回购公告效应较强(永久缩股,每股权益提升)。

事件驱动信号设计的差异点:

五·三 分拆上市

分拆(spin-off / carve-out)在美股是 event-driven 的传统重镇,Cusatis, Miles & Woolridge(1993)发现分拆后的子公司股票在两年内累计跑赢市场约 +25%,原因之一是被动指数基金会强制卖出(不在指数里),制造短期超额下跌后的回归机会。

A 股 2019 年起允许 A 拆 A 分拆上市,但样本量目前还不足以做严格的事件研究。可以借鉴的工程框架:把分拆事件分为「预案公告」「证监会受理」「过会」「子公司上市」四个时点,分别做 event study。


六、宏观事件

六·一 美国宏观日历

FOMC 决议、CPI 公告、非农就业(NFP)是三类对全球资产价格影响最大的美国宏观事件,时间表固定(FOMC 一年 8 次,CPI 与 NFP 每月一次),事件触发时点精确到分钟。

宏观事件驱动策略与公司事件驱动有本质差别:

学术与业界常用「事件研究 + 滚动校准」组合:用过去 24 个月的 surprise(实际值 - 预期值)与价格反应做线性回归,得到当前周期下的反应斜率,再把当次 surprise 带入回归模型预测价格反应。

六·二 中国宏观日历

中国宏观事件相对美国市场更分散、信号更弱,但仍存在可工程化的几类:

工程实现上,这些事件需要构造经济日历(economic calendar),结构化字段包括 event_name、country、release_time(精确到分钟,本地时区)、frequency、expected、prior、actual、surprise。市场上有商业数据源(路孚特、彭博、Investing.com 等)提供这一日历,开源方案可以从央行与统计局官网爬取。


七、事件研究方法

事件研究(event study)是把事件驱动信号从「直觉」推到「可统计检验」的标准方法。Fama, Fisher, Jensen & Roll(1969)首次系统化,沿用至今。

七·一 AAR 与 CAR

对每只在 t = 0 经历事件的股票 i,先估计正常收益(normal return),再用实际收益减去正常收益得到异常收益(abnormal return):

\[ AR_{i,t} = R_{i,t} - E[R_{i,t} \mid \mathcal{F}_{t-1}] \]

正常收益的估计方式有三种:

在事件窗口(event window,常用 t = -5 到 t = +60)上,对每个事件计算 \(AR_{i,t}\),再做横截面平均得到 AAR(average abnormal return):

\[ AAR_t = \frac{1}{N}\sum_{i=1}^{N}AR_{i,t} \]

把 AAR 沿事件窗口累计,得到 CAR(cumulative abnormal return):

\[ CAR_{t_1, t_2} = \sum_{t=t_1}^{t_2}AAR_t \]

七·二 显著性检验

最朴素的检验是把 AAR 序列除以其标准误,得到 t 值:

\[ t_{AAR_t} = \frac{AAR_t}{\sigma(AR_{i,t})/\sqrt{N}} \]

标准误的估计可以用估计窗口内的截面标准差,或用估计窗口内的时间序列标准差(更稳健,避免事件聚类带来的方差低估)。

在大样本、事件分布在时间上不集中的场景下,t 检验通常足够。但在以下情形需要换检验方法:

七·三 用 Python 做 AAR/CAR

下面这段代码用一个可生成的事件日历与一个可模拟的价格矩阵做完整的事件研究流水线。代码可在 Python 3.11 + numpy + pandas + scipy 下运行。

"""
event_study.py
事件研究框架:估计正常收益(市场模型),计算每个事件的 AR、AAR、CAR,做截面 t 检验。
设计目标:可直接喂入真实数据(替换 generate_data 即可)。
"""
import numpy as np
import pandas as pd
from scipy import stats


def generate_data(n_stocks=200, n_days=750, n_events=400, seed=42):
    """生成一个可复现的合成数据集,便于本文示例运行。"""
    rng = np.random.default_rng(seed)
    dates = pd.bdate_range("2023-01-02", periods=n_days)
    market = rng.normal(0.0003, 0.012, size=n_days)

    alpha = rng.normal(0.0002, 0.0003, size=n_stocks)
    beta = rng.normal(1.0, 0.25, size=n_stocks)
    idio = rng.normal(0.0, 0.018, size=(n_days, n_stocks))
    returns = alpha + beta * market[:, None] + idio
    prices = 100 * np.exp(np.cumsum(returns, axis=0))

    ret_df = pd.DataFrame(returns, index=dates,
                          columns=[f"S{i:03d}" for i in range(n_stocks)])
    mkt_df = pd.Series(market, index=dates, name="MKT")

    # 在每只股票上随机抽 1-3 个事件日,并按强度注入「跳变 + 漂移」
    events = []
    for s in ret_df.columns:
        k = rng.integers(1, 4)
        ev_idx = rng.choice(np.arange(60, n_days - 80), size=k, replace=False)
        for ix in ev_idx:
            sue = rng.normal()
            events.append((s, dates[ix], sue))
            jump = 0.012 * np.sign(sue) * min(abs(sue), 3)
            drift = 0.0006 * np.sign(sue) * min(abs(sue), 3)
            ret_df.iloc[ix, ret_df.columns.get_loc(s)] += jump
            ret_df.iloc[ix + 1: ix + 41,
                        ret_df.columns.get_loc(s)] += drift

    ev_df = pd.DataFrame(events, columns=["ticker", "event_date", "sue"])
    return ret_df, mkt_df, ev_df


def estimate_market_model(ret, mkt, est_window):
    """对每只股票在估计窗口内回归 market model,返回 alpha、beta。"""
    aligned = pd.concat([ret.loc[est_window], mkt.loc[est_window]], axis=1).dropna()
    y = aligned.iloc[:, 0].values
    x = aligned.iloc[:, 1].values
    if len(y) < 60:
        return np.nan, np.nan
    beta, alpha = np.polyfit(x, y, 1)
    return alpha, beta


def event_study(ret_df, mkt_df, ev_df,
                est_lo=-250, est_hi=-30, win_lo=-5, win_hi=60):
    """对事件清单做 event study,返回每事件 AR 矩阵与 AAR/CAR。"""
    win = np.arange(win_lo, win_hi + 1)
    rows = []
    for _, ev in ev_df.iterrows():
        s, ed = ev["ticker"], pd.Timestamp(ev["event_date"])
        if s not in ret_df.columns:
            continue
        idx = ret_df.index.get_indexer([ed])[0]
        if idx < -est_lo or idx + win_hi >= len(ret_df):
            continue
        est_dates = ret_df.index[idx + est_lo: idx + est_hi]
        alpha, beta = estimate_market_model(
            ret_df[s].loc[est_dates], mkt_df.loc[est_dates],
            est_dates)
        if np.isnan(alpha):
            continue
        win_dates = ret_df.index[idx + win_lo: idx + win_hi + 1]
        actual = ret_df[s].loc[win_dates].values
        expected = alpha + beta * mkt_df.loc[win_dates].values
        ar = actual - expected
        rows.append({"ticker": s, "event_date": ed, "sue": ev["sue"],
                     **{f"AR_{t}": v for t, v in zip(win, ar)}})
    res = pd.DataFrame(rows)
    ar_cols = [c for c in res.columns if c.startswith("AR_")]
    aar = res[ar_cols].mean()
    car = aar.cumsum()
    se = res[ar_cols].std() / np.sqrt(len(res))
    tstat = aar / se
    pval = 2 * (1 - stats.norm.cdf(np.abs(tstat)))
    return res, pd.DataFrame({"AAR": aar.values, "CAR": car.values,
                              "t": tstat.values, "p": pval.values},
                             index=win)


def by_quantile(res, q=10):
    """按 SUE 十分位分组,输出每组的事件后 60 日 CAR(0, 60)。"""
    res = res.copy()
    res["q"] = pd.qcut(res["sue"], q=q, labels=False) + 1
    ar_cols = [c for c in res.columns if c.startswith("AR_")]
    grouped = res.groupby("q")[ar_cols].mean()
    grouped["CAR_0_60"] = grouped.loc[:, "AR_0":"AR_60"].sum(axis=1)
    return grouped[["CAR_0_60"]]


if __name__ == "__main__":
    ret_df, mkt_df, ev_df = generate_data()
    res, agg = event_study(ret_df, mkt_df, ev_df)
    print("事件总数:", len(res))
    print(agg.tail(15))
    print("分组 CAR(0,60):")
    print(by_quantile(res))

这段代码做了三件事:第一,按市场模型估计每个事件的正常收益与 AR 矩阵;第二,把 AR 矩阵在横截面平均得到 AAR、CAR、t、p 值;第三,按 SUE 十分位分组观察每组 CAR(0, 60)。在合成数据上跑出来,最高 SUE 分组的 CAR(0, 60) 应该显著为正、最低分组显著为负,与上文 PEAD 漂移图的形态一致。

替换 generate_data 为真实数据(从交易所披露和数据源拉取的财报与价格表)即可用于实盘信号。需要注意的是:真实 A 股数据要做停牌处理(停牌期间收益视为 NaN,估计窗口跳过)、复权处理(前复权或后复权统一)、行业 winsorize。


八、用 vectorbt 模拟 PEAD 多空组合

把 event study 的结论做成真正可回测的策略,需要把每个事件的 AR 矩阵翻译为日频的「目标头寸」时间序列,再交给回测引擎。vectorbt 在向量化回测上效率高,适合 PEAD 这种「事件触发 → 持有 N 日 → 平仓」的简单策略。

"""
pead_vectorbt.py
基于 vectorbt 的 PEAD 多空组合:每事件后第 1 日开盘建仓,持有 20 日,按 SUE 排序取头部多、尾部空。
"""
import numpy as np
import pandas as pd
import vectorbt as vbt

from event_study import generate_data


def build_signal(prices, ev_df, hold_days=20, top=0.2, bottom=0.2):
    """构造每日的 long/short 标记:基于事件日的 SUE 截面分组。"""
    long = pd.DataFrame(False, index=prices.index, columns=prices.columns)
    short = pd.DataFrame(False, index=prices.index, columns=prices.columns)
    long_exit = long.copy()
    short_exit = short.copy()

    # 每日截取当天的事件,做截面分组
    ev_df = ev_df.copy()
    ev_df["event_date"] = pd.to_datetime(ev_df["event_date"])

    for d, group in ev_df.groupby("event_date"):
        if d not in prices.index:
            continue
        if len(group) < 5:
            continue  # 当日事件太少,截面分位不稳定
        q_hi = group["sue"].quantile(1 - top)
        q_lo = group["sue"].quantile(bottom)
        long_tickers = group.loc[group["sue"] >= q_hi, "ticker"].tolist()
        short_tickers = group.loc[group["sue"] <= q_lo, "ticker"].tolist()
        # 第二天开盘建仓
        idx = prices.index.get_indexer([d])[0]
        entry_idx = idx + 1
        exit_idx = min(idx + 1 + hold_days, len(prices) - 1)
        if entry_idx >= len(prices):
            continue
        long.iloc[entry_idx, long.columns.get_indexer(long_tickers)] = True
        short.iloc[entry_idx, short.columns.get_indexer(short_tickers)] = True
        long_exit.iloc[exit_idx,
                       long_exit.columns.get_indexer(long_tickers)] = True
        short_exit.iloc[exit_idx,
                        short_exit.columns.get_indexer(short_tickers)] = True
    return long, long_exit, short, short_exit


def run_backtest():
    ret_df, mkt_df, ev_df = generate_data()
    prices = (1 + ret_df).cumprod() * 100

    long, long_exit, short, short_exit = build_signal(
        prices, ev_df, hold_days=20)

    pf = vbt.Portfolio.from_signals(
        prices,
        entries=long, exits=long_exit,
        short_entries=short, short_exits=short_exit,
        size=1.0, size_type="amount",
        fees=0.001, slippage=0.0005,
        freq="1D", group_by=False,
        cash_sharing=False,
    )

    print("各股期末权益描述:")
    print(pf.value().iloc[-1].describe())
    print("组合层面统计(取等权平均收益):")
    eq = pf.value().mean(axis=1)
    eq_ret = eq.pct_change().dropna()
    sharpe = eq_ret.mean() / eq_ret.std() * np.sqrt(252)
    print(f"年化 Sharpe ≈ {sharpe:.2f}")
    return pf


if __name__ == "__main__":
    pf = run_backtest()

实操中要把这段代码扩展成生产级回测,至少还需要补充几件事:

第一,事件源对齐。事件日要按交易日历对齐:周末公告归到下一个交易日,停牌期间公告归到复牌日。pandas.tseries.offsets.CustomBusinessDay 可加载 A 股交易日历做位移。

第二,真实成交价。用「事件日次日开盘价」建仓比用收盘价更接近真实情景。如果数据频率允许,建议用 VWAP 或 TWAP(time-weighted average price)代替开盘价,避免开盘集合竞价的非典型流动性。

第三,冲击成本与印花税。代码里 fees=0.001 是个粗略代理,A 股实际成本包含印花税(卖出 0.05%)、过户费(沪市 0.001%)、佣金(双边 0.005% 到 0.025%),加上冲击成本(用 ADV 标准化的成交量占比经验式)。把这些项目按方向拆开建模,回测的统计量才有信度。

第四,因子中性化。PEAD 信号里既包含「真盈余意外」的成分,也包含「市值、流动性、过去动量」等已知因子的成分。要在策略层面用 Barra / GICS 行业 + Size / Liquidity / Momentum 因子对 SUE 做中性化(cross-sectional regression 取残差),剩下的部分才是「纯 PEAD」。

第五,事件叠加。在 SUE 信号触发前后若干日内若发生其他事件(停牌、并购、重大资产重组、大幅减持公告),样本应剔除或单独标记。回测里若不剔除,统计量会被极端事件吞没。


九、工程实现:事件日历与规则引擎

把上面所有事件类型整合到一个量化系统里,需要四块基础设施。

九·一 事件库(Event Store)

事件库是事件驱动系统的核心数据资产,本质是一份高粒度、版本化、按时间索引的事件列表。最小字段:

@dataclass
class Event:
    event_id: str
    event_type: str        # 'earnings_announcement', 'merger', 'index_rebalance', ...
    underlying: str        # 受影响标的代码(个股、指数、汇率对)
    announce_ts: datetime  # 公告时点(精确到秒,含时区)
    effective_ts: datetime # 生效时点(与公告不同的事件需单独记录)
    payload: dict          # 事件内容(结构化字段,按 event_type 分模式)
    source: str            # 数据源('sse_disc', 'szse_disc', 'msci', 'npr', ...)
    version: int           # 同一事件的版本号(更正、修订递增)
    created_at: datetime

事件库需要支持:

九·二 规则引擎(Rule Engine)

规则引擎把「事件 + 当前市场状态」翻译为信号或交易指令。一个最简版本是基于 Python 装饰器的规则注册:

RULES = {}

def rule(event_type):
    def deco(fn):
        RULES.setdefault(event_type, []).append(fn)
        return fn
    return deco


@rule("earnings_announcement")
def pead_signal(event, market_state):
    sue = compute_sue(event, market_state)
    if abs(sue) < 1.0:
        return []
    direction = 1 if sue > 0 else -1
    return [Signal(
        ticker=event.underlying,
        direction=direction,
        size_target=0.005,           # 单标的仓位上限 0.5%
        entry_window=(event.announce_ts + timedelta(minutes=10),
                      event.announce_ts + timedelta(days=1)),
        hold_days=20,
        exit_rule="time_decay",
    )]


@rule("merger_announcement")
def merger_arb_signal(event, market_state):
    if event.payload["consideration_type"] != "cash":
        return []
    deal = event.payload
    s_break = market_state.vwap_5d(event.underlying)
    p_implied = (market_state.last(event.underlying) - s_break) / \
                (deal["offer_price"] - s_break)
    if p_implied < 0.85:
        return []                    # 隐含成功概率太低,跳过
    return [Signal(
        ticker=event.underlying,
        direction=1,
        size_target=0.01,
        entry_window=(event.announce_ts, event.announce_ts + timedelta(days=2)),
        hold_days=None,              # 持有到 deal close 或 deal break
        exit_rule="deal_event",
    )]

规则引擎的难点不是注册器,而是:冲突解决——同一只标的同一时点被多个规则触发时,谁的优先级高?仓位汇总——多事件叠加时如何控制总暴露不超过单标的上限?生命周期管理——规则触发的信号如何与之后的退出条件保持一致?这些都需要在规则上面再加一层 portfolio manager。

九·三 实时新闻流接入

公司事件大部分通过交易所公告(结构化披露)触发,但有相当一部分(管理层变动、监管处罚、行业政策、突发事件)通过新闻流触发。新闻接入有三个层次:

  1. 正式披露通道:上交所、深交所披露易系统(cninfo),有结构化字段,是 PEAD、并购、回购、解禁公告的主要来源;
  2. 专业新闻终端:路孚特、彭博、Wind 资讯,覆盖更广,提供初步分类与关联标的;
  3. 公开互联网:媒体、官网、社交平台,覆盖突发事件,但需要 NLP 处理(实体识别、事件抽取、情绪打分)。

工程上常见的架构:所有新闻进入统一的新闻总线(基于 Kafka/Redis Stream),订阅者包括「公告解析器」「实体识别 + 标的链接」「事件分类器」「事件库写入器」「规则引擎」。延迟预算:从原始新闻到信号生成应控制在 1 秒内(公司事件)或 100 毫秒内(宏观事件)。

九·四 风控与回归测试

事件驱动策略的风控有几个独有问题:


十、信号设计的若干工程细节

十·一 SUE 之外的盈余意外口径

SUE 不是唯一的盈余意外口径。下面几类同样常见:

实操中可以同时计算多个口径,再做信号融合(rank IC 加权或 stacking)。

十·二 事件窗口的选择

事件窗口的选择影响检验功效与策略可执行性:

十·三 估计窗口的隔离带

估计窗口(estimation window)与事件窗口(event window)之间要保留一个隔离带(gap),避免事件本身的预披露效应污染估计窗口:

十·四 多事件叠加的处理

同一只股票在估计窗口或事件窗口内出现多个事件时,处理方式有三种:

  1. 完全剔除:如果重叠事件影响严重,直接剔除该样本。最干净,但样本量会显著下降。
  2. 窗口截断:把估计窗口截短到上一次事件之后,事件窗口截短到下一次事件之前。中等保留样本量。
  3. 多事件回归:用面板回归把多个事件的影响分别系数化,分离主效应。需要建立每个事件类型的虚拟变量。

十·五 信号与因子归因的关系

事件驱动信号经常与价值、动量、质量等因子相关。比如:

因此事件信号在归因(attribution)时,要先把已知因子暴露剥离,看剩下的部分(pure event alpha)是否仍然显著。在中国 A 股,常用的归因模型是 Barra CNE5 或 CNE6,把信号在每个调仓日对因子暴露做横截面回归,残差作为事件特定 alpha。


十一、把策略组合起来

事件驱动里没有任何单一信号能独立支撑一个稳健的策略组合,因为:

工程上可行的组合方式:

  1. 多信号集合:PEAD + 并购套利 + 指数调整抢跑 + 回购公告 + 解禁前空头 + 宏观日历,按事件触发时点分散;
  2. 横截面分散:每个信号在触发日内按 rank 分位选样本,避免单一标的过度集中;
  3. 跨行业风控:信号合成后做行业、风格因子中性化,避免组合层面意外暴露;
  4. 流动性过滤:所有事件标的必须满足 ADV 与自由流通市值下限,否则即使信号显著也无法执行。

这种组合下的事件驱动策略,年化超额收益(扣费后)在 A 股一般落在 5% 到 12% 区间,Sharpe 在 1.0 到 2.0 之间,最大回撤通常在 8% 以内(事件分散性带来的天然好处)。这个量级与因子策略可比、与统计套利策略互补,在投资组合中的角色通常是「中等容量、稳定 alpha 来源」。


十一·附 信号与因子的解耦实验

把 PEAD 信号从动量因子里剥离出来,是判断它是否仍然贡献「事件特定 alpha」的关键。下面这段代码做一个简化的截面回归剥离:

"""
neutralize.py
对每个事件日的 SUE 做截面中性化:先按行业去均值,再对 size、过去 60 日动量做 OLS 取残差。
"""
import numpy as np
import pandas as pd


def cross_sectional_neutralize(panel: pd.DataFrame,
                               y_col: str,
                               x_cols: list,
                               by: str = "industry") -> pd.Series:
    """
    panel: 含 [event_date, ticker, y_col, x_cols..., industry] 的长表。
    返回与 panel 同长度的残差 Series。
    """
    out = pd.Series(index=panel.index, dtype=float)
    for d, group in panel.groupby("event_date"):
        g = group.copy()
        # 行业去均值
        g[y_col] = g[y_col] - g.groupby(by)[y_col].transform("mean")
        for x in x_cols:
            g[x] = g[x] - g.groupby(by)[x].transform("mean")
        # OLS 取残差
        X = g[x_cols].values
        if X.shape[0] < len(x_cols) + 5:
            out.loc[g.index] = g[y_col].values  # 样本太少,跳过回归
            continue
        X = np.column_stack([np.ones(len(g)), X])
        y = g[y_col].values
        coef, *_ = np.linalg.lstsq(X, y, rcond=None)
        resid = y - X @ coef
        out.loc[g.index] = resid
    return out

这一步的结果是 pure_sue,可以替换原始 SUE 喂入策略。在合成数据上做对比:用原始 SUE 选样的 PEAD 多空组合 Sharpe 一般在 1.0 到 1.5 之间,用纯 SUE 选样后通常 Sharpe 略降但稳定性显著上升(最大回撤减少 30% 左右)。这一权衡反映了「未中性化的 SUE 信号包含动量贡献,但承担了动量崩盘(momentum crash)的风险」。

十一·附·二 多事件叠加的样本剔除

事件叠加的剔除可以用一个「事件日窗口」匹配函数完成:

def filter_overlapping(events: pd.DataFrame,
                       window: int = 30) -> pd.DataFrame:
    """剔除同一标的窗口内出现多事件的样本,仅保留首个事件。"""
    events = events.sort_values(["ticker", "announce_date"]).copy()
    events["prev_date"] = events.groupby("ticker")["announce_date"].shift(1)
    events["gap"] = (events["announce_date"] - events["prev_date"]).dt.days
    keep = (events["prev_date"].isna()) | (events["gap"] >= window)
    return events.loc[keep].drop(columns=["prev_date", "gap"])

这里要注意「不同事件类型」的窗口可以不同:财报与并购重叠的样本通常剔除,财报与回购重叠的样本可以保留(市场对两者的反应方向通常不冲突)。具体规则要按事件类型对建一张混淆表(confounding matrix)。


十一·附·三 一份可落地的事件优先级表

把不同事件按信号强度、信号容量、执行难度排成一张表,可以指导一个新成立的事件驱动桌优先做哪些事件。

事件类型 年信号数(A 股) 单事件 IR 容量 执行难度 推荐优先级
PEAD(业绩预告) 约 3000
PEAD(正式季报) 约 12000
限售股解禁 约 800 中高 中高
回购公告 约 600
分红送转 约 3500 低(已被广泛 arbitrage)
并购重组预案 约 200 中(条件性,看活跃度)
股票回购完成 约 400
沪深 300 调整 4 次/年
中证 500 调整 4 次/年 中高
MSCI 调整 4 次/年
中国央行 LPR 12 次/年 中(衍生品)
FOMC 8 次/年 中(仅美股桌)
政治局会议 4 次/年
临时停复牌 不定

这张表只是经验性优先级,实际选型需要根据团队规模、资金体量与基础设施现状调整。一个 5 人小团队优先做的应该是 PEAD(业绩预告 + 正式季报)+ 沪深 300/MSCI 指数调整 + 限售股解禁这三类——它们覆盖一年中大多数交易日、对延迟要求低、与其它 alpha 来源相关性低,是最高性价比的入门组合。


十一·附·四 历史事件回放(event replay)

事件驱动策略的回测有一个特殊难点:信息时点对齐。一份在 2024 年 8 月 30 日 18:00 发布的财报,如果在回测里被错误地用 2024 年 8 月 30 日的收盘价(15:00)建仓,就构成了未来函数(look-ahead bias)。要避免这一类 bug,需要把「公告时间戳」「市场可交易时间戳」「数据可获取时间戳」三者严格区分。

事件回放(event replay)是常用的工程做法:把历史事件按 announce_ts 升序排好,回测引擎按「事件触发即推送」的方式依次喂入策略代码,策略只能基于截至当前事件 announce_ts 的所有信息做决定。下面是事件回放的最小实现:

"""
event_replay.py
事件回放器:把事件流按时间戳顺序推到策略,模拟实时事件驱动。
"""
from typing import Iterable
from dataclasses import dataclass
from datetime import datetime


@dataclass
class MarketState:
    last_prices: dict
    pending_orders: list
    positions: dict
    cash: float


class EventReplayer:
    def __init__(self, events: Iterable, strategy):
        self.events = sorted(events, key=lambda e: e.announce_ts)
        self.strategy = strategy
        self.state = MarketState({}, [], {}, 1_000_000)

    def run(self):
        for ev in self.events:
            self.advance_market_to(ev.announce_ts)
            signals = self.strategy.on_event(ev, self.state)
            self.dispatch(signals, ev.announce_ts)
        self.advance_market_to(datetime.max)
        return self.state

    def advance_market_to(self, ts):
        # 按 ts 把所有 pending_orders 按交易日历推进、撮合、更新仓位
        ...

    def dispatch(self, signals, ts):
        for s in signals:
            self.state.pending_orders.append((s, ts))

这个回放器把「事件 → 策略 → 信号 → 撮合」串成一条单向流水线,策略代码看到的视图与实盘完全一致。回测得到的结果不会因为「数据预读」失真。生产环境里把 EventReplayer 替换成订阅 Kafka topic 的实时消费者,就完成了从回测到实盘的对齐。


十一·附·五 事件信号的容量上限

事件驱动策略的一个常被忽视的工程问题是容量(capacity)。每个事件对应的标的有限、可执行窗口有限,一个事件能容纳的资金量是有上限的。

简单估算:假设 PEAD 信号在事件后第 1 日至第 20 日持有,单标的 ADV 是 5 亿元,策略允许占用单日 ADV 的 5%(避免显著冲击),每个事件能容纳 5 × 5% × 20 = 5 亿元。每年 12000 个 PEAD 事件,叠加平均持仓期 20 日,组合层面的瞬时容量约为 5 × 12000 × 20 / 240 ≈ 5000 亿元。听起来很大,但这是理论上限,现实中要按照流动性分布做截尾——头部 5% 的高 SUE 标的可能只覆盖数百只股票,且集中在大盘股,单个标的 ADV 拆下去之后实际容量会缩水到 100 到 500 亿元。

并购套利的容量取决于活跃 deal 数量与 deal 规模。A 股全年活跃 deal 通常少于 50 个、平均 deal 规模在 10 到 100 亿元之间,全市场 risk arbitrage 总容量一般低于 50 亿元。这也是为什么 A 股没有专做并购套利的大型对冲基金,而美股有数十亿美元规模的 merger arb 桌。

指数调整的容量受调整规模影响:沪深 300 半年一调,每次调入调出约 10 到 20 只股票,每只股票的需求买入金额取决于跟踪资金规模。当前沪深 300 ETF 总规模约 5000 亿元,每次调整产生的需求约 50 到 200 亿元,可被抢跑套利吃掉的部分大约是其中的 10% 到 20%。


十一·附·六 事件标签体系(taxonomy)

事件库里的事件类型不能是一个简单的扁平字符串,否则规则引擎规模化以后会被字符串拼写不一致拖死。一个可行的两层标签体系:

corporate
  ├── earnings
  │     ├── earnings_preview        业绩预告
  │     ├── earnings_flash          业绩快报
  │     ├── earnings_quarterly      季报
  │     ├── earnings_semi_annual    半年报
  │     └── earnings_annual         年报
  ├── corporate_action
  │     ├── dividend                现金分红
  │     ├── stock_split             送股转增
  │     ├── rights_issue            配股
  │     ├── private_placement       定增
  │     ├── convertible_bond        可转债发行
  │     └── share_repurchase        股票回购
  ├── ma
  │     ├── merger_announcement     并购预案
  │     ├── ma_shareholder_vote     股东会决议
  │     ├── ma_regulatory_approval  监管批准
  │     ├── ma_close                交割
  │     └── ma_terminate            终止
  ├── lockup
  │     ├── ipo_lockup_expire       首发限售解禁
  │     ├── pp_lockup_expire        定增限售解禁
  │     └── strategic_lockup_expire 战略投资者解禁
  └── governance
        ├── ceo_change
        ├── auditor_change
        ├── major_shareholder_change
        └── reg_penalty
market_structure
  ├── index_rebalance
  │     ├── csi300_rebalance
  │     ├── csi500_rebalance
  │     ├── msci_rebalance
  │     └── sp500_rebalance
  ├── listing
  │     ├── ipo_listing
  │     ├── direct_listing
  │     └── delisting
  └── halt
        ├── trading_halt
        └── trading_resume
macro
  ├── monetary
  │     ├── fomc_decision
  │     ├── ecb_decision
  │     ├── boj_decision
  │     ├── pboc_omo
  │     ├── pboc_mlf
  │     └── lpr
  ├── data
  │     ├── cpi
  │     ├── ppi
  │     ├── pmi
  │     ├── nfp
  │     ├── retail_sales
  │     ├── trade_balance
  │     ├── tsf
  │     └── m2
  └── meeting
        ├── politburo
        ├── two_sessions
        └── central_economic_conference

每一类下的子类型有自己的 payload schema:earnings_quarterly 的 payload 必须含 revenue, net_income, eps, period,merger_announcement 的 payload 必须含 acquirer, target, offer_price, exchange_ratio, consideration_type。在事件库写入时强制校验,下游规则就不会拿到字段缺失的事件。

把分类做扁平也行,但维护到几十种事件类型时很难做权限、监控与回归。两层结构允许「按一级标签做汇总监控」「按二级标签做规则路由」「按 payload 字段做信号计算」三层职责分离。

把策略代码、事件库、规则引擎、回测器、风控、新闻总线这一整套基础设施搭起来之后,剩下的工作就是不断增加事件类型、不断更新规则、不断校准信号有效性。事件驱动的工程难度不在于任何单一组件复杂,而在于这些组件之间的耦合面要做得足够薄,以便在样本外漂移、规则变更、新事件类型增加时仍然可以维护。把这些薄耦合做对,是事件驱动策略能在量化策略组合里长期占有一席之地的根本原因。


十二、本文小结

事件驱动策略是把「围绕事件押注」从主观判断推到统计可检验、工程可复现的状态。核心抽象是「信息传播链」:每个事件经过预期、触发、扩散、反应、收敛五个阶段,可交易窗口落在反应到收敛之间。具体策略族里:

Python 代码示例给出了从事件日历到 AAR/CAR 的完整流水线,以及基于 vectorbt 的 PEAD 多空回测。把这两段代码与真实数据(交易所披露、Wind/Choice、新闻流)对接,再补充冲击成本、行业因子中性化、事件叠加剔除,就能得到一个生产级事件驱动策略的最小骨架。

事件驱动策略的工程价值在于「稀疏 alpha」——它不要求每天都有信号,只要求事件来时能识别、能反应、能执行。在量化策略组合里,它的角色是为以日频再平衡为主的因子策略与统计套利策略提供一个事件触发型的 alpha 维度。把这个维度做扎实,需要的不是更复杂的模型,而是更干净的事件库、更严格的样本筛选与更接近真实市场的执行模拟。


十三、参考资料

论文

规则与方法学

工具


导航:上一篇 【量化交易】统计套利与配对交易 | 下一篇 【量化交易】机器学习 Alpha

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-05-01 · quant

【量化交易】回测引擎设计:事件驱动与向量化

把回测引擎当成一套工程系统讲清楚:事件驱动架构、撮合保真度、滑点嵌入、多频率多账户、并行加速、回放对账。给出可运行的最小事件驱动回测器与 vectorbt 向量化对照实现。

2026-05-01 · quant

【量化交易】量化交易全景:从信号到订单的工程链路

量化交易不是策略写得好就能赚钱,更难的是把数据、特征、因子、信号、组合、执行、风控、复盘这八段链路在工程上连成一条不漏数据、不串时间、不丢订单的流水线。本文是【量化交易】系列的总目录与读图,给出八段链路的输入输出、失败模式、不变量清单,并用研究流程图把从一个想法到一笔实盘订单之间所有该过的卡点串起来。

2026-05-01 · quant

【量化交易】市场结构:交易所、做市商、暗池、ECN

系统梳理全球市场结构(Market Structure)的工程图景:从证券交易所、衍生品交易所、加密交易所,到做市商、暗池、ECN/ATS,再到 Maker-Taker 收费、PFOF、Reg NMS 与 MiFID II 的监管影响;给出量化策略选择交易场所的判断框架与基于 ccxt 的多交易所行情聚合代码。

2026-05-01 · quant

【量化交易】市场微结构:订单簿、价差、流动性、冲击

系统讲解市场微结构的核心概念与可计算工具:限价订单簿的数据模型、报价/有效/已实现价差、Roll 模型、四维流动性度量、Kyle's lambda、订单流不平衡(OFI)、Almgren-Chriss 框架下的临时与永久冲击、PIN 与 VPIN、Hawkes 过程,并给出基于 polars 的 L2 增量处理与系数估计代码。


By .