← 返回笔记
数据工程 2026年8月2日 约 9 分钟读完

As-of:为什么电力交易回测必须保存历史可见快照

历史数据库里的「最终值」不等于交易决策当时能看到的值。理解 event_time、published_at、available_at 的区别,以及为什么 available_at <= decision_time 是回测的硬门禁。

方法文章As-of回测数据泄漏Point-in-time

引言:回测里最隐蔽的错误,不是模型选错,而是数据用错

做回测时,我们很容易把注意力放在模型本身:特征工程对不对、超参数调没调好、损失函数是否合理。但有一个更基础的问题,如果搞错了,后面的一切都建立在流沙上——你喂给模型的,是不是模型在当时真的能看到的数?

电力市场的数据和股票日线收盘价有一个重要区别:一个交易时段的价格,在结算后还会被反复修订。历史数据库里存着的”最终值”,并不等于交易决策当时能够看到的值。如果回测直接读取后修订的数据,就可能把未来信息带回历史,造成数据泄漏和虚假的模型表现。

这篇文章要讲清楚三个时间、一道硬门禁,以及 Point-in-time 快照最少要保存哪些字段。

一、电力交易里的三个时间

在电力市场的数据链路中,一个事实从”发生”到”可用”,中间隔着不止一个时间戳。至少有三种时间需要区分:

  • 事件发生时间(event_time):事实真正发生的时刻。比如某台机组在 14:00 跳机、某条联络线在 20:00 停运。
  • 信息发布时间(published_at):这个事实被正式对外发布的时间。比如调度机构在 15:30 发布检修计划。
  • 系统可用时间(available_at):在你的数据系统里,这条记录真正可以被查询到的时间。它通常晚于 published_at,因为中间还有采集、传输、清洗、入库的过程。

对回测而言,真正重要的不是 event_time,而是 available_at。原因很简单:

一个决策者在 decision_time 时刻,只能使用 available_at <= decision_time 的数据。

如果你在回测里用了一条 event_time 早于决策时刻、但 available_at 晚于决策时刻的数据,就等于让模型”提前看到了未来”。这正是数据泄漏最常见、也最难察觉的来源之一。

二、四个时间戳,各管一件事

在实际数据建模中,通常至少需要四类时间戳,它们各管一件事:

字段含义回答的问题
event_time事实发生时间这件事什么时候发生的?
published_at对外发布时间市场什么时候知道这件事?
available_at系统可用时间我的系统什么时候能查到?
ingested_at入库时间这条记录什么时候写进数据库?
  • event_time 描述物理世界;
  • published_at 描述市场信息环境;
  • available_at 描述你自己的数据环境;
  • ingested_at 描述数据流水线本身。

其中 ingested_at 最容易被忽略,但它对排查问题很有用:如果发现某批数据入库时间异常晚,说明流水线存在延迟,这会直接影响 available_at 的边界。

三、available_at <= decision_time:为什么是硬门禁

回测的核心逻辑是模拟一次真实决策:在某个时刻,基于当时可见的信息,做出一个交易或报价决定。

如果把数据门禁写死成:

available_atdecision_time\text{available\_at} \leq \text{decision\_time}

那么模型在回测中看到的每一行数据,都严格早于或等于决策时刻。这是防止数据泄漏的硬门禁——它不需要判断特征重不重要、相关性高不高,直接按时间边界把未来信息挡在门外。

软门禁和硬门禁的区别在于:

  • 软门禁:我”尽量”不用未来的数据,但某些特征因为工程原因用了,希望影响不大。
  • 硬门禁:任何一行数据,只要 available_at 晚于 decision_time,就永远不可能进入这个决策的特征集。

在回测场景中,必须使用硬门禁。原因不是严谨性洁癖,而是因为电力市场价格的自相关性很强:未来的价格与当前决策之间往往存在真实的统计关联,哪怕只泄漏一点点未来信息,也会让回测指标显著虚高,而且这种虚高在样本外几乎一定会消失。

四、完整率门禁与 As-of 门禁不是一回事

很多系统会做”数据完整率检查”,比如:

  • 当天的价格数据完整率达到 95% 以上,才允许运行模型;
  • 缺失超过 5% 就告警或降级。

完整率门禁检查的是数据覆盖度,As-of 门禁检查的是数据时效性,两者不能互相替代。

  • 完整率门禁回答:数据够不够全?
  • As-of 门禁回答:数据是不是当时可见的?

一个系统可以同时满足两者,也可以只满足其中一个。比如:

  • 某天完整率 100%,但其中 20% 的记录是事后修订的最终值——完整率通过,As-of 不通过;
  • 某天完整率 80%,但每条记录都严格符合 available_at <= decision_time——As-of 通过,完整率不通过。

真实的回测系统两个门禁都需要:先用 As-of 门禁保证没有未来信息,再用完整率门禁判断这一天能不能支撑可靠决策。

五、Point-in-time 快照最少要保存哪些字段

要支持严格的 As-of 回测,仅仅保存”当前值”是不够的,必须保存每一个时点可见的历史版本。一个最小可用的快照,至少应该包含:

delivery_interval   交割时段(哪个交易日、哪个时段)
decision_time       决策时刻(回测中模拟的决策时间)
event_time          事件发生时间
published_at        对外发布时间
available_at        系统可用时间
ingested_at         入库时间
revision_no         修订版本号
snapshot_id         快照 ID
source_id           数据来源 ID
rule_version        规则版本号
completeness_ratio  当时的完整率
gate_status         门禁状态
reason_code         拒绝/通过的原因码

其中有三组字段特别重要:

  1. 时间戳组(event_time / published_at / available_at / ingested_at):用来判断一条数据在任意决策时刻是否可见;
  2. 版本组(revision_no / snapshot_id):用来区分”当时看到的版本”和”后来的修订版”;
  3. 门禁组(completeness_ratio / gate_status / reason_code):用来记录当时数据质量的判断依据,方便复盘时知道为什么某天用了或没用某条数据。

六、一个错误回测与正确回测的对照

用一个简化的例子说明两者的差别。

假设某省电力市场在 7 月 1 日发生了一次价格尖峰。事后数据库里的”最终值”显示,当天 19:00 的现货价格是 1500 元/MWh。但在 7 月 1 日当天 16:00 做决策时,系统里能查到的价格还只有 400 元/MWh——因为结算和修订流程直到 7 月 3 日才完成。

  • 错误回测:直接读取历史库的最终值,在 7 月 1 日 16:00 的特征里用上”19:00 价格=1500”这条信息。模型会学到”下午价格会飙到 1500”,回测指标非常漂亮。
  • 正确回测:读取 7 月 1 日 16:00 时刻的快照,当时的特征是”19:00 价格=400(待修订)“。模型只能基于真实可见的信息决策。

两种回测跑出来的是两个完全不同的模型,而只有后者在实盘里才有可能重现。

七、用 snapshot_id + model_version + rule_version 重现实验

最后,一个容易被低估但非常实用的问题:回测结果能不能被完整重现?

一个实验的可重现性,取决于三个版本号是否齐全:

  • snapshot_id:用的是哪一份数据快照;
  • model_version:用的是哪一个模型;
  • rule_version:用的是哪一套交易/报价规则。

三者缺一不可。只记录 model_version 而不记录 snapshot_id,过了一个月数据更新,同样的模型代码可能跑出完全不同的结果,而你无法解释差异来自模型还是数据;只记录 snapshot_id 而不记录 rule_version,规则改了一行,结果变化也无法归因。

把这三个版本号连同每次实验的输入输出一起归档,才具备最基本的实验可重现性。这也是把回测从”跑过一次”变成”可以被审计、被复用的资产”的起点。

结语:回测的底线是”不偷看未来”

回测的本质,是诚实地模拟一次当时的决策。技术可以越来越复杂——更精细的特征、更强的模型、更快的流水线——但最底层的诚实不能丢:你喂给模型的每一行数据,都必须是决策当时真实可见的。

保存 Point-in-time 快照、实施 As-of 硬门禁,短期看是增加数据工程成本,长期看是让每一次回测结果都经得起追问:这个指标,在真实世界里到底能不能重现?

这篇文章是方法系列的第一篇,后续会继续展开数据工程与回测设计的其他主题。

说明:本文为方法文章,示例均为教学性质,不含任何市场主体的内部数据。所有时间字段和门禁设计以通用数据工程实践为准。