PIT 数据与财报时点
一种不会被任何价格检查抓到的未来函数
你的价格数据干净得无可挑剔:八项体检全过,日历一天不差,未来函数两把尺子都测过,一个 shift 都不缺。
然后你写了这样一行:
factor = fundamentals.set_index("statDate")["roe"] # 按报告期对齐
实测:在这只样本股上,这一行让你的因子在 47.8% 的交易日里,握着一个当时还没有公告的数字。
平均而言,你提前 54 天知道了业绩。年报是 102.5 天。
而在这段提前期里,股价平均波动 8.25%——一年四份报告,如果每次都站对方向,那是年化 33% 的"免费"超额。
这就是 PIT 问题。它不会被任何价格检查抓到,因为价格本身完全没有问题。
考试成绩是 6 月 20 日考完的,但成绩单 7 月 15 日才寄到家。
如果你在复盘"我妈知道成绩后的反应",却把她的反应记在 6 月 20 日那天——你就凭空给了她 25 天的先知。
那 25 天里发生的每一件事,都被错误地归因了。
一、报告期 ≠ 披露日
财务数据有两个日期,任何一个数据源都会同时给你:
- statDate 报告期:数据描述的是截至哪一天的经营状况(如 2024-03-31)
- pubDate 披露日:这份报告实际公告的日子(如 2024-04-28)
实测 12 只沪深300 成分股、288 份季报(2019–2024):
平均滞后 54.0 天 | 中位 31 天 | 最短 22 天 | 最长 121 天
分位:P25 29 天 | P75 66 天 | P95 118 天
按报告期分季度看,差异极大:
| 报告期 | 份数 | 平均滞后 | 最长 |
|---|---|---|---|
| Q1(截至 3-31) | 72 | 28.3 天 | 30 天 |
| Q2(截至 6-30) | 72 | 56.2 天 | 62 天 |
| Q3(截至 9-30) | 72 | 29.2 天 | 31 天 |
| Q4(截至 12-31) | 72 | 102.5 天 | 121 天 |
年报滞后最久——年报披露截止日是次年 4 月 30 日,所以 12 月 31 日的数据,你可能要到四个月后才真正拿得到。
如果你按 statDate 对齐,就等于在这 102 天里,提前握着全市场的年报业绩。
二、这个泄漏值多少钱
光说"不对"没有说服力。我们把它换算成钱。
对每一份报告,计算 statDate 到 pubDate 之间的股价涨跌(后复权):
可计算样本 288 份
提前期平均涨跌 +0.36% | 中位 -1.01%
绝对值平均 8.25% ← 这才是泄漏的信息量
区间最大 +114.2% | 最小 -34.2%
注意为什么要看绝对值:平均涨跌只有 +0.36%,看起来无关紧要。但泄漏的价值不在方向,在信息量——你提前知道业绩好坏,就能选择做多或做空。
每份报告的提前期平均波动 8.25%,一年四份。若每次都站对方向,年化约 33% 的"免费"超额。
这 33% 不是策略收益,而是「信息提前量」本身的价值。
它意味着:如果你的回测里有 PIT 泄漏,那么你看到的超额收益中,有相当一部分不是策略的能力,而是这 33% 的一个分成。
更麻烦的是:这类泄漏做出来的回测,曲线会非常漂亮而且很稳——因为它每个季度都能"预知"一次业绩。稳定的超额比疯狂的超额更容易骗到人,包括骗到你自己。
🔧 动手做:量一量"提前知道业绩"这个未来函数(5 分钟)
跑 python pit_leak.py。它统计财报的报告期 → 披露日滞后。
你会看到(实测):12 只样本股 288 份季报,报告期到真正披露平均滞后 54 天,中位 31 天,年报最长 121 天(Q4 平均 102.5 天)。如果你的因子按报告期对齐(而不是披露日),你就等于提前 54 天用上了当时还没公布的业绩。
想明白:这是一种任何价格检查都抓不到的未来函数——价格全对、shift 也做了,但你用的财务数字在那个时间点根本还没公开。修法不是"记得对齐",是让错误对齐在数据层就不可能:每个财务值都带上它的披露日,取数时只允许取"披露日 ≤ 当前"的。基本面因子,先问:你对齐的是报告期还是披露日?
三、同一个因子,两种对齐
拿一只样本股的 ROE 因子,两种对齐方式各铺一条时间序列:
wrong.loc[idx >= r["statDate"]] = val # ❌ 报告期当天就"知道"
right.loc[idx >= r["pubDate"]] = val # ✅ 披露日之后才知道
实测结果:
两种对齐取值不同的交易日:888 天 / 1856 天 (47.8%)
将近一半的交易日,两个版本的因子值是不一样的。
不是偶尔差一点,是接近一半的时间,错误版本握着一个当时还没公告的数字。
四、修法:让错误的对齐在数据层就不可能发生
单靠"记得用 pubDate"是不够的——这正是 p0.3 那条教训:人记不住。工程上的做法是把生效日写进数据模型:
CREATE TABLE fundamentals (
symbol VARCHAR,
stat_date DATE, -- 报告期:只用于说明这份数据描述的是哪一期
effective_date DATE, -- 生效日 = pubDate:下游只允许用这个 join
roe DOUBLE, revenue DOUBLE, ...,
source VARCHAR, fetched_at TIMESTAMP,
PRIMARY KEY (symbol, stat_date)
);
三条纪律:
- 入库时就把 pubDate 写成
effective_date,不要留给下游去理解 - 下游 join 一律用
effective_date,stat_date只用于展示和分组 - 同一份报告有修正版时(如更正公告),按新的 pubDate 再插一条,而不是覆盖——因为"当时你看到的"确实是旧版本
第三条是完整 PIT 的核心,也是最容易被忽略的:真正的 PIT 数据库,记录的不是"这个数字是多少",而是"在每一个时刻,你能看到的这个数字是多少"。
📌 免责:本课为技术教学,样本为沪深300 成分股按代码序取前 12 只(非精选),仅用于统计演示,不构成任何投资建议。
P1 章最硬的一节结束。四个数字:
- 54 天:报告期到披露日的平均滞后(中位 31,最长 121)
- 102.5 天:年报的平均滞后——你可能四个月后才真正拿到 12 月 31 日的数据
- 8.25%:提前期内股价的平均绝对波动 → 年化约 33% 的"免费"超额
- 47.8%:两种对齐下因子取值不同的交易日占比
一条修法:把 pubDate 写成 effective_date,下游只允许按它 join;有修正版就再插一条,不覆盖——因为"当时你看到的"确实是旧版本。
下一节是 P1 的收尾:绘图工程。听起来最软,但它承担一个硬任务——让数据的问题自己跳出来。这一章讲的所有坑,除权跳空、停牌占位、日历断档,在正确的图上都是一眼可见的;而在一张默认参数的折线图上,它们全都藏得很好。
🔎 来源与核验· 5 条,点开核对
