阅读提示

这篇讲解 EventStudy 模块——如何分析特定事件(如年报披露、并购公告、指数调入)前后,股票的超额收益如何。学完之后,你能独立完成一次完整的事件研究。

导言

事件研究(Event Study)是量化金融里最经典的分析方法之一。它的核心问题是:某个事件(如年报披露、并购公告、分红)发生后,股票有没有显著的超额收益?

Betalens 的 EventStudy 模块提供了灵活的事件研究能力:

  • 单标的:看某只股票在某个事件前后的收益曲线。
  • 多标的平均:把一批股票的事件窗口收益取平均,得出”事件平均效应”。
  • 基准超额:用沪深 300 等基准的同期收益做对照,得到超额收益。
  • 固定持有期:指定持有期(如事件后持有 20 天),直接算总收益。

基本用法

1
2
3
4
5
from betalens.datafeed import Datafeed
from betalens.eventstudy import EventStudy

# 初始化(传入行情表名)
es = EventStudy(Datafeed("daily_market"))

场景 1:单标的事件分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 事件列表(可以是 pandas DataFrame 或 list of dicts)
events = pd.DataFrame({
"code": ["000001.SZ"] * 3,
"event_date": ["2022-04-29", "2023-04-28", "2024-04-26"],
})

result = es.analyze(
events,
code="000001.SZ",
window_before=5, # 事件前 5 个交易日
window_after=10, # 事件后 10 个交易日
metric="收盘价(元)",
mode="flexible", # 灵活模式:返回事件前后每天的收益
benchmark_code=None, # 无基准
)

print(result.summary)

mode 参数:

mode 含义
flexible 返回事件前后每天的收益曲线
fixed_holding 返回固定持有期(如事件后持有 N 天)的总收益
benchmark_excess 返回相对于基准的超额收益

场景 2:多标的平均事件效应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 批量事件:多只股票在同一天的事件
events_batch = pd.DataFrame({
"code": ["000001.SZ", "000002.SZ", "000004.SZ"],
"event_date": ["2023-04-28", "2023-04-28", "2023-04-28"],
"event_type": ["年报披露", "年报披露", "年报披露"],
})

result = es.analyze(
events_batch,
code="000001.SZ", # 主标的
window_before=5,
window_after=20,
metric="收盘价(元)",
mode="flexible",
benchmark_code=None,
)

# result 返回的是多标的平均效应
print(result.average_cumulative_return)

场景 3:超额收益分析

1
2
3
4
5
6
7
8
9
10
# 相对于沪深300的超额收益
result = es.analyze(
events,
code="000001.SZ",
window_before=5,
window_after=20,
metric="收盘价(元)",
mode="benchmark_excess",
benchmark_code="000300.SH", # 沪深300
)

返回结果包含:

  • 事件窗口收益:每天的收益率。
  • 累计超额收益(CAR):窗口内每一天的累计超额收益。
  • 显著性检验:t 统计量和 p 值。

场景 4:固定持有期

1
2
3
4
5
6
7
8
9
10
11
12
# 年报披露后持有 20 个交易日
result = es.analyze(
events,
code="000001.SZ",
window_before=0,
window_after=20,
metric="收盘价(元)",
mode="fixed_holding",
benchmark_code="000300.SH",
)
print(result.holding_period_return) # 持有 20 日的总收益
print(result.excess_return) # 超额收益(相对基准)

开发者侧:EventStudy 的内部逻辑

从源码角度,EventStudy.analyze 的核心流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def analyze(events, code, window_before, window_after, ...):
results = []
for each event:
# 1. 找到事件窗口的交易日序列
window_dates = get_trade_days_in_window(event_date, window_before, window_after)

# 2. 获取窗口内每日价格
prices = data.query_time_range(codes=[code], dates=window_dates, ...)

# 3. 计算每日收益率
returns = prices.pct_change().dropna()

# 4. 计算 CAR(累计超额收益)
if benchmark_code:
benchmark_returns = get_benchmark_returns(window_dates, benchmark_code)
excess_returns = returns - benchmark_returns
car = excess_returns.cumsum()

results.append({"returns": returns, "car": car, ...})

# 5. 对多标的做平均
return EventStudyResult(average_car=mean(results), ...)

常见错误

1. 事件日期不是交易日

1
2
3
# 事件日期是 2023-04-29(周日),EventStudy 会自动平移到最近的后一个交易日
# 但如果你希望事件发生在周一,需要自己预处理
events["event_date"] = pd.bdate_range(events["event_date"], events["event_date"]) # 移到周一

2. 事件窗口内停牌

如果事件窗口内股票停牌,收益率会是 0 或 NaN。需要在分析前检查停牌情况:

1
2
3
4
# 检查事件窗口内的停牌比例
停牌日 = trade_status_matrix[trade_status_matrix[code] == 0].index
if len(停牌日.intersection(window_dates)) > len(window_dates) * 0.3:
print("警告:事件窗口内停牌比例超过 30%,结果可能不可靠")

延伸阅读