As-of:为什么电力交易回测必须保存历史可见快照
历史数据库里的「最终值」不等于交易决策当时能看到的值。理解 event_time、published_at、available_at 的区别,以及为什么 available_at <= decision_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_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 拒绝/通过的原因码
其中有三组字段特别重要:
- 时间戳组(event_time / published_at / available_at / ingested_at):用来判断一条数据在任意决策时刻是否可见;
- 版本组(revision_no / snapshot_id):用来区分”当时看到的版本”和”后来的修订版”;
- 门禁组(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 硬门禁,短期看是增加数据工程成本,长期看是让每一次回测结果都经得起追问:这个指标,在真实世界里到底能不能重现?
这篇文章是方法系列的第一篇,后续会继续展开数据工程与回测设计的其他主题。
说明:本文为方法文章,示例均为教学性质,不含任何市场主体的内部数据。所有时间字段和门禁设计以通用数据工程实践为准。