7.4 盈利超预期(SUE)与 PEAD:信息摩擦下的持续漂移¶
矛盾:有效市场说好消息会被瞬间定价,为什么财报超预期后股价还会继续涨 60 天?
PEAD 为什么会持续存在?¶
Ball & Brown(1968)发现这个现象后已过去 50 年,PEAD 仍未消失,原因是:
- 信息处理有摩擦:财报有数百页,散户和部分机构无法当天分析完
- 分析师更新迟缓:卖方分析师的盈利预测往往保守,不会在一天内大幅上调
- 套利有成本:需要持仓 60 天,期间有其他风险占用资金
- 注意力分散:财报季同时发布数百家,资金关注度被稀释
学习目标¶
- 理解 SUE(标准化盈利惊喜)的含义和计算
- 演示 PEAD 事件研究(Event Study)
- 分析 PEAD 在低分析师覆盖股中为何更强
In [1]:
Copied!
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)
print('OK')
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy import stats
np.random.seed(42)
print('OK')
OK
In [2]:
Copied!
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
import matplotlib.pyplot as plt
# 1. 设置系统自带的中文字体(这里使用黑体 SimHei)
plt.rcParams['font.sans-serif'] = ['SimHei'] # 如果你想用微软雅黑,可以改成 ['Microsoft YaHei']
# 2. 解决更换字体后,负号(-)显示为方块的问题
plt.rcParams['axes.unicode_minus'] = False
第一部分:从盈利惊喜到 SUE¶
为什么要标准化?¶
原始盈利惊喜 = 实际EPS - 预期EPS,但 A 股公司 EPS 从几分钱到几十元不等, 跨公司比较无意义。标准化解决了这个问题:
$$\text{SUE} = \frac{\text{实际EPS} - \text{预期EPS}}{\sigma_{\text{历史惊喜}}}$$
SUE = 2 意味着:这次的利润超预期幅度是该公司历史平均惊喜的 2 倍标准差。
分析师为什么系统性低估?¶
卖方分析师有保守预测的激励机制:
- 超预期比不及预期更容易让客户和公司管理层满意
- 低估后公司财报发布超预期,分析师显得"专业"
- 这是行业内公认的"管理预期"策略
In [3]:
Copied!
np.random.seed(42)
n = 500
true_eps = np.random.normal(2.0, 0.8, n)
consensus = true_eps + np.random.normal(-0.05, 0.25, n) # 分析师系统低估!
actual = true_eps + np.random.normal(0, 0.15, n)
surprise = actual - consensus
sue = surprise / np.random.uniform(0.2, 0.5, n)
print(f'正惊喜比例: {(surprise>0).mean():.1%} (分析师低估→正惊喜>50%是正常现象)')
print(f'平均惊喜: {surprise.mean():.4f} (正数确认分析师系统低估)')
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].hist(surprise, bins=50, color='steelblue', alpha=0.7, edgecolor='none')
axes[0].axvline(0, color='red', lw=2, linestyle='--', label='0线')
axes[0].axvline(surprise.mean(), color='orange', lw=2, label=f'均值={surprise.mean():.3f}')
axes[0].set_title('原始盈利惊喜分布\n均值>0证实分析师系统低估')
axes[0].legend()
axes[1].hist(sue, bins=50, color='darkorange', alpha=0.7, edgecolor='none')
for v,c,l in [(-1,'darkred','强负惊喜'), (1,'green','强正惊喜')]:
axes[1].axvline(v, color=c, lw=1.5, linestyle=':', label=l)
axes[1].set_title('SUE分布\n标准化后可跨公司跨期比较')
axes[1].legend()
plt.tight_layout(); plt.show()
np.random.seed(42)
n = 500
true_eps = np.random.normal(2.0, 0.8, n)
consensus = true_eps + np.random.normal(-0.05, 0.25, n) # 分析师系统低估!
actual = true_eps + np.random.normal(0, 0.15, n)
surprise = actual - consensus
sue = surprise / np.random.uniform(0.2, 0.5, n)
print(f'正惊喜比例: {(surprise>0).mean():.1%} (分析师低估→正惊喜>50%是正常现象)')
print(f'平均惊喜: {surprise.mean():.4f} (正数确认分析师系统低估)')
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
axes[0].hist(surprise, bins=50, color='steelblue', alpha=0.7, edgecolor='none')
axes[0].axvline(0, color='red', lw=2, linestyle='--', label='0线')
axes[0].axvline(surprise.mean(), color='orange', lw=2, label=f'均值={surprise.mean():.3f}')
axes[0].set_title('原始盈利惊喜分布\n均值>0证实分析师系统低估')
axes[0].legend()
axes[1].hist(sue, bins=50, color='darkorange', alpha=0.7, edgecolor='none')
for v,c,l in [(-1,'darkred','强负惊喜'), (1,'green','强正惊喜')]:
axes[1].axvline(v, color=c, lw=1.5, linestyle=':', label=l)
axes[1].set_title('SUE分布\n标准化后可跨公司跨期比较')
axes[1].legend()
plt.tight_layout(); plt.show()
正惊喜比例: 59.0% (分析师低估→正惊喜>50%是正常现象) 平均惊喜: 0.0583 (正数确认分析师系统低估)
In [5]:
Copied!
np.random.seed(42)
days = np.arange(-30, 61)
labels = ['极负SUE(<-1)', '弱负(-1,-0.3)', '中性', '弱正(0.3,1)', '极正(>1)']
sue_means = [-1.5, -0.6, 0.0, 0.6, 1.5]
colors = ['#c0392b','#e67e22','#95a5a6','#27ae60','#1a5276']
fig, ax = plt.subplots(figsize=(13, 6))
for sue_m, color, label in zip(sue_means, colors, labels):
car = np.zeros(len(days))
for i, d in enumerate(days):
if d < 0:
car[i] = 0.0003 * sue_m * d
elif d == 0:
prev = car[i-1] if i > 0 else 0
car[i] = prev + 0.012 * sue_m
else:
car[i] = car[i-1] + 0.00045*sue_m + np.random.normal(0, 0.005)
ax.plot(days, car*100, color=color, lw=2.2, label=label)
ax.axvline(0, color='black', lw=2, linestyle='--', label='财报发布日 Day 0')
ax.axhline(0, color='gray', lw=0.8)
ax.fill_betweenx([-8, 8], 0, 60, alpha=0.05, color='blue')
ax.annotate('PEAD区间(Day0后持续分化)', xy=(30, 1), fontsize=9, color='blue')
ax.set_xlabel('相对财报发布日的交易天数')
ax.set_ylabel('累积超额收益率 CAR (%)')
ax.set_title('PEAD事件研究:按SUE分组的累积超额收益关键:Day 0之后各组仍在继续分化,这就是「漂移」')
ax.legend(fontsize=9); ax.grid(alpha=0.3)
plt.tight_layout(); plt.show()
print('如果市场有效: Day 0 之后所有组的 CAR 应平行(不再分化)')
print('实际结果: Day 0 之后各组仍明显分化 → 证明信息消化存在摩擦')
np.random.seed(42)
days = np.arange(-30, 61)
labels = ['极负SUE(<-1)', '弱负(-1,-0.3)', '中性', '弱正(0.3,1)', '极正(>1)']
sue_means = [-1.5, -0.6, 0.0, 0.6, 1.5]
colors = ['#c0392b','#e67e22','#95a5a6','#27ae60','#1a5276']
fig, ax = plt.subplots(figsize=(13, 6))
for sue_m, color, label in zip(sue_means, colors, labels):
car = np.zeros(len(days))
for i, d in enumerate(days):
if d < 0:
car[i] = 0.0003 * sue_m * d
elif d == 0:
prev = car[i-1] if i > 0 else 0
car[i] = prev + 0.012 * sue_m
else:
car[i] = car[i-1] + 0.00045*sue_m + np.random.normal(0, 0.005)
ax.plot(days, car*100, color=color, lw=2.2, label=label)
ax.axvline(0, color='black', lw=2, linestyle='--', label='财报发布日 Day 0')
ax.axhline(0, color='gray', lw=0.8)
ax.fill_betweenx([-8, 8], 0, 60, alpha=0.05, color='blue')
ax.annotate('PEAD区间(Day0后持续分化)', xy=(30, 1), fontsize=9, color='blue')
ax.set_xlabel('相对财报发布日的交易天数')
ax.set_ylabel('累积超额收益率 CAR (%)')
ax.set_title('PEAD事件研究:按SUE分组的累积超额收益关键:Day 0之后各组仍在继续分化,这就是「漂移」')
ax.legend(fontsize=9); ax.grid(alpha=0.3)
plt.tight_layout(); plt.show()
print('如果市场有效: Day 0 之后所有组的 CAR 应平行(不再分化)')
print('实际结果: Day 0 之后各组仍明显分化 → 证明信息消化存在摩擦')
如果市场有效: Day 0 之后所有组的 CAR 应平行(不再分化) 实际结果: Day 0 之后各组仍明显分化 → 证明信息消化存在摩擦
第三部分:为什么低覆盖率小盘股 PEAD 更强?¶
| 特征 | 大盘股 | 小盘股 |
|---|---|---|
| 分析师覆盖 | 10-30人 | 0-3人 |
| 媒体报道 | 大量 | 极少 |
| 机构持仓比例 | 高 | 低 |
| 信息传播速度 | 快 | 慢 |
| PEAD 强度 | 弱(20天) | 强(60+天) |
逻辑:信息传播越慢,市场消化越迟缓,PEAD 持续时间越长。 量化交易员可以在小盘、低覆盖的段中获取最强的 PEAD 信号。
In [7]:
Copied!
np.random.seed(42)
N = 300
coverage = np.random.poisson(lam=8, size=N).clip(0, 30)
# PEAD 强度与覆盖率负相关
pead_str = np.random.normal(0, 0.15, N) + 0.25 / (1 + 0.3*coverage)
r, p = stats.pearsonr(coverage, pead_str)
print(f'覆盖率 vs PEAD强度 相关系数: r={r:.3f} (p={p:.4f})')
plt.figure(figsize=(8, 4))
plt.scatter(coverage, pead_str*100, alpha=0.4, s=20, color='steelblue')
m, b, *_ = stats.linregress(coverage, pead_str)
xr = np.linspace(0, 30, 100)
plt.plot(xr, (m*xr+b)*100, 'r-', lw=2, label=f'r={r:.3f}')
plt.xlabel('分析师覆盖数量(人)')
plt.ylabel('PEAD强度(60天CAR %)')
plt.title('分析师覆盖越少 → PEAD 越强(信息摩擦越大,套利机会越持久)')
plt.legend(); plt.grid(alpha=0.3); plt.show()
np.random.seed(42)
N = 300
coverage = np.random.poisson(lam=8, size=N).clip(0, 30)
# PEAD 强度与覆盖率负相关
pead_str = np.random.normal(0, 0.15, N) + 0.25 / (1 + 0.3*coverage)
r, p = stats.pearsonr(coverage, pead_str)
print(f'覆盖率 vs PEAD强度 相关系数: r={r:.3f} (p={p:.4f})')
plt.figure(figsize=(8, 4))
plt.scatter(coverage, pead_str*100, alpha=0.4, s=20, color='steelblue')
m, b, *_ = stats.linregress(coverage, pead_str)
xr = np.linspace(0, 30, 100)
plt.plot(xr, (m*xr+b)*100, 'r-', lw=2, label=f'r={r:.3f}')
plt.xlabel('分析师覆盖数量(人)')
plt.ylabel('PEAD强度(60天CAR %)')
plt.title('分析师覆盖越少 → PEAD 越强(信息摩擦越大,套利机会越持久)')
plt.legend(); plt.grid(alpha=0.3); plt.show()
覆盖率 vs PEAD强度 相关系数: r=-0.130 (p=0.0238)
🎯 练习¶
- 对 A 股所有沪深300成份股,用过去 5 年季报数据计算真实 SUE 因子,验证 PEAD 是否存在。
- PEAD 的最大风险是「反转」:超预期后再公告下修。设计止损规则(跌破发布日价格的5%则平仓)。
- 比较 A 股与美股 PEAD 效应:A 股散户比例更高,PEAD 持续时间应该更长还是更短?查阅文献。
下一节 → ../05_portfolio/08_dynamic_asset_allocation.ipynb
In [ ]:
Copied!