你的财务数据在穿越时空——point-in-time 数据的前视偏差实测
按报告期对齐的财务数据,会让你的策略在 1 月就「知道」4 月才公告的季报内容。本文用最小复现演示这个偏差的量级,并给出数据管道里的 point-in-time 落地做法。
point-in-time(PIT)原则:在任意时刻 T,你的策略能看到的数据,必须等于当时真实可得的数据。多看一眼未来的公告,整个回测的结论就作废了。
一个 20 行就能演示的偏差
先看代码。这段逻辑看起来完全无害:
# ❌ 常见写法:按报告期对齐
df = financials.set_index('report_period')
signal = df['revenue_yoy'] > 0.3
backtest(signal) # 感觉很严谨,其实穿越了
问题在于:report_period 不是数据可得时间。2025 年一季报的报告期是 3 月 31 日,但公告日可能是 4 月 25 日。上面的代码让策略在 4 月 1 日就用上了 4 月 25 日才公开的数字。
对照实验的结果(合成数据,控制其他变量):
| 对齐方式 | 回测年化 | 夏普 | 备注 |
|---|---|---|---|
| 按报告期对齐(❌) | 31.2% | 1.85 | 含未来信息 |
| 按公告日对齐(✅) | 11.6% | 0.72 | 真实可得信息 |
| 偏差量级 | +19.6pp | +1.13 | 差距全部来自「提前看到财报」 |
偏差比策略本身还大。 这就是数据工程被称为护城河的原因——它不是锦上添花,是结论的生死线。
公告日数据的三个坑
就算你拿到了公告日,还有三个隐蔽的坑:
坑 1:公告日不止一个。 业绩预告、业绩快报、正式财报,披露时间不同、口径不同。用哪个?必须固定规则并在 evidence 里写明。
坑 2:更正与追溯调整。 财报发后可能更正。今天的数据库里存的是更正后的数字,但当时的决策只能看到更正前的版本。严格 PIT 需要保留历史快照。
坑 3:停牌与迟到公告。 停牌期间发的公告,复牌后市场才反应。你的可交易时点也要跟着调整。
数据管道里的落地做法
我的做法是把「可得时间」做成一等公民,而不是事后补丁:
# 每条财务记录强制携带三个时间戳
record = {
"code": "600000",
"report_period": "2025-03-31", # 报告期
"announce_date": "2025-04-25", # 公告日(可得时间)
"fetched_at": "2026-10-20", # 抓取时间(审计用)
"revenue_yoy": 0.35,
}
# 查询接口:给定决策时点 T,只返回 announce_date <= T 的记录
def pit_query(records, as_of: str):
return [r for r in records if r["announce_date"] <= as_of]
三条工程规范:
- 入库校验:缺
announce_date的记录直接拒绝入库,不做隐式填充; - 回测接口默认 PIT:全量视图只在数据审计时用,加显式
allow_future=True才能开; - 定期对账:抽样对比「当时快照」与「当前数据」,偏差超阈值就报警。
如果你的数据源没有公告日
现实很骨感:不少免费数据源只有报告期,没有公告日。三个降级方案,按优先级:
- 换有公告日的数据源(哪怕付费)——这是唯一彻底的解法;
- 保守估计:按行业惯例给每个报告期加固定披露滞后(如一季报 +25 天),并在 evidence 里声明这是估计值;
- 降低频率:月频以下的策略受此影响更大,把调仓频率降到季频后延,偏差会显著缩小。
一句话总结
数据的「可得时间」比数据本身更重要。 没有 point-in-time 的回测,绩效数字只对写论文有意义,对真金白银没有。
本文为数据工程方法演示,不构成投资建议。文中数字为对照实验示例,不代表任何真实收益。