你的财务数据在穿越时空——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]

三条工程规范:

  1. 入库校验:缺 announce_date 的记录直接拒绝入库,不做隐式填充;
  2. 回测接口默认 PIT:全量视图只在数据审计时用,加显式 allow_future=True 才能开;
  3. 定期对账:抽样对比「当时快照」与「当前数据」,偏差超阈值就报警。

如果你的数据源没有公告日

现实很骨感:不少免费数据源只有报告期,没有公告日。三个降级方案,按优先级:

  1. 换有公告日的数据源(哪怕付费)——这是唯一彻底的解法;
  2. 保守估计:按行业惯例给每个报告期加固定披露滞后(如一季报 +25 天),并在 evidence 里声明这是估计值;
  3. 降低频率:月频以下的策略受此影响更大,把调仓频率降到季频后延,偏差会显著缩小。

一句话总结

数据的「可得时间」比数据本身更重要。 没有 point-in-time 的回测,绩效数字只对写论文有意义,对真金白银没有。


本文为数据工程方法演示,不构成投资建议。文中数字为对照实验示例,不代表任何真实收益。