1. 理解CAPM与事件研究法的底层逻辑
我第一次接触CAPM模型是在研究生时期,当时只觉得是一堆数学公式。直到工作后用它分析上市公司财报影响,才发现这个诞生于1960年代的模型至今仍是金融分析的基石。简单来说,CAPM(资本资产定价模型)告诉我们:股票的预期收益率=无风险利率+β×市场风险溢价。这个β系数就像股票的"敏感度",衡量它相对于大盘的波动程度。
举个例子,某科技公司β=1.5,意味着当市场上涨10%时,它可能上涨15%;反之市场下跌时它也会跌得更猛。而像水电公司这类稳定行业,β通常小于1。去年我用这个模型分析新能源车企,发现它们的β普遍在2.0以上,这解释了为何这些股票波动剧烈。
事件研究法则是CAPM的延伸应用。当公司发布财报、并购重组等重大事件发生时,我们可以用CAPM计算"本该获得"的正常收益率,再对比实际收益率,差值就是超额收益率(AR)。把事件窗口期内每天的AR累加,就得到累计超额收益率(CAR)——它直接反映了市场对该事件的反应强度。
2. 数据准备与β系数计算实战
做量化分析最头疼的往往是数据获取。我常用的免费数据源有Yahoo Finance的API(适合美股)和AKShare库(适合A股)。以分析A股某白酒企业年报事件为例,我们需要准备:
- 该股票至少过去一年的日收益率数据
- 对应时间段的市场指数(如沪深300)收益率
- 无风险利率(一般用1年期国债收益率)
import akshare as ak import pandas as pd # 获取贵州茅台(600519)和沪深300数据 stock_data = ak.stock_zh_a_daily(symbol="sh600519", adjust="hfq") index_data = ak.stock_zh_index_daily(symbol="sh000300") # 计算日收益率 stock_data['return'] = stock_data['close'].pct_change() index_data['return'] = index_data['close'].pct_change() # 合并数据 merged_data = pd.merge(stock_data[['date','return']], index_data[['date','return']], on='date', suffixes=('_stock','_index'))计算β系数时要注意几个坑:
- 数据频率影响结果:日数据计算的β通常比月数据更不稳定
- 时间窗口选择:一般用事件前1年数据,但周期性行业可能需要更长
- 极端值处理:像2020年疫情初期的市场异常波动建议做Winsorize处理
from statsmodels.api import OLS # 准备回归数据 X = merged_data['return_index'].iloc[1:] # 市场收益率 y = merged_data['return_stock'].iloc[1:] # 个股收益率 X = sm.add_constant(X) # 添加截距项 # 线性回归计算β model = OLS(y, X).fit() beta = model.params[1] print(f"贵州茅台的β系数为:{beta:.2f}")3. 事件窗口与正常收益率建模
确定事件日(比如年报发布日期)后,需要定义事件窗口。常见设置是[-20,20],即事件前后各20个交易日。但要注意:
- 窗口太短可能遗漏市场反应
- 窗口太长会引入其他噪音因素
- 最好避开财报季、政策发布等密集事件期
计算正常收益率的CAPM公式看似简单,但实操中有几个关键点:
- 无风险利率要用匹配的时间周期,日收益率对应日化无风险利率
- 市场收益率建议用流通市值加权指数
- 对于停牌日期的处理要一致(建议向前填充)
# 年化无风险利率转为日化 annual_rf = 0.015 # 假设1.5% daily_rf = (1 + annual_rf)**(1/252) - 1 # 计算每日正常收益率 merged_data['normal_return'] = daily_rf + beta * ( merged_data['return_index'] - daily_rf) # 可视化对比 import matplotlib.pyplot as plt plt.figure(figsize=(10,6)) plt.plot(merged_data['date'], merged_data['return_stock'], label='实际收益率') plt.plot(merged_data['date'], merged_data['normal_return'], label='正常收益率') plt.legend() plt.title("实际收益率 vs CAPM预测收益率") plt.show()4. CAR计算与结果解读技巧
超额收益率(AR)就像股票的"异常表现分",计算方法是:
AR = 实际收益率 - 正常收益率而CAR则是把事件窗口内每天的AR累加,相当于考察期内异常表现的总分。但要注意几个常见误区:
- 统计显著性检验:CAR是否显著不为0?建议用t检验验证
- 行业对比:单独CAR值意义有限,需要同行业公司对比
- 事件类型差异:利好事件CAR应为正,利空则为负
# 定义事件日(假设2023-03-31) event_date = pd.to_datetime('2023-03-31') event_idx = merged_data[merged_data['date']==event_date].index[0] # 设置事件窗口 window_start = event_idx - 20 window_end = event_idx + 20 # 计算AR和CAR merged_data['AR'] = merged_data['return_stock'] - merged_data['normal_return'] car = merged_data['AR'].iloc[window_start:window_end+1].sum() # 输出结果 print(f"事件窗口[{window_start}:{window_end}]的CAR值为:{car:.2%}") # 绘制CAR走势 merged_data['CAR'] = merged_data['AR'].cumsum() plt.figure(figsize=(10,6)) plt.plot(merged_data['date'], merged_data['CAR']) plt.axvline(x=event_date, color='r', linestyle='--') plt.title("累计超额收益率(CAR)走势") plt.show()去年分析某电商平台财报时,我发现虽然CAR整体为正,但细分看:
- 财报前5天CAR就开始上升(可能存在信息泄露)
- 财报次日CAR跳升3.2%(超预期)
- 之后10天持续回落(利好兑现)
这种精细分析比单纯看最终CAR值更有价值。
5. 案例:用CAR分析并购公告效应
以某知名互联网公司收购案为例,完整走一遍流程:
数据准备阶段
- 获取收购方、被收购方及同行业可比公司数据
- 市场指数选用中概股指数(HXC)
- 事件日确定为2022-08-15(公告日)
模型构建
# 计算各公司β betas = {} for ticker in ['BIDU', 'TARGET', 'COMP1', 'COMP2']: model = OLS(returns[ticker], sm.add_constant(returns['HXC'])).fit() betas[ticker] = model.params[1] # 计算正常收益率 for ticker in returns.columns[:-1]: # 最后一列是市场指数 returns[f'{ticker}_normal'] = daily_rf + betas[ticker] * ( returns['HXC'] - daily_rf)CAR分析
# 定义事件窗口 event_idx = returns.index.get_loc('2022-08-15') window = slice(event_idx-10, event_idx+10) # 计算CAR car_results = {} for ticker in ['BIDU', 'TARGET']: ar = returns[ticker] - returns[f'{ticker}_normal'] car_results[ticker] = ar[window].sum() print(f"百度CAR: {car_results['BIDU']:.2%}") print(f"标的公司CAR: {car_results['TARGET']:.2%}")
实际运行发现:
- 收购方(BIDU)CAR为-2.3%(市场担忧溢价过高)
- 被收购方CAR高达+18.7%(溢价体现)
- 同业公司CAR也普遍上涨(行业估值提升)
这种分析可以帮助判断市场对并购交易的看法,以及是否存在协同效应预期。
6. 常见问题与解决方案
在金融实验室带学生做CAR分析时,最常遇到的几个问题:
问题1:β系数不稳定
- 解决方案:尝试用不同时间窗口(3个月/6个月/1年)计算β,取中位数
- 进阶方法:采用动态β模型(如Kalman Filter)
问题2:事件窗口内有其他干扰
- 解决方案:控制变量法,加入行业CAR作为解释变量
- 示例代码:
# 计算行业平均CAR industry_car = np.mean([car_results[t] for t in ['COMP1','COMP2']]) # 调整后的CAR adjusted_car = car_results['BIDU'] - 0.5*industry_car
问题3:小样本统计效力不足
- 解决方案:扩大样本量(建议至少30个事件)
- 替代方案:采用bootstrap方法重采样
问题4:CAR方向与预期相反可能原因:
- 市场提前消化信息(检查事件日前CAR)
- 模型设定有问题(检查β计算是否正确)
- 存在未被考虑的变量(如政策变化)
有次分析医药集采事件,发现中标企业CAR反而上升。深入分析才发现这些企业通过以价换量,市场份额提升抵消了降价影响。这说明CAR分析必须结合行业知识。