摘要
跨市场策略的输入不是一张天然同步的行情表。不同交易场所的消息存在延迟、乱序和修订,同一时刻生成的特征也可能因回放方式不同而改变。本文为 LUMENOVA 提出一套可回放的数据管线:同时记录事件发生时间与数据可用时间,以水位线管理窗口进度,以时间截面查询对齐市场状态,再将特征及其输入范围保存为决策快照。核心目标是让历史研究重现决策当时能够看到的信息。
01 / 为什么同一份行情会产生不同结论
假设策略每秒比较两个市场的报价。研究时,将事后收齐的数据按交易所时间排序,通常能得到整齐的序列;实盘决策时,某个较早发生的报价却可能尚未到达。若回测直接读取整理后的序列,就会把当时未知的报价放入特征。数据没有来自未来的时间戳,信息却来自未来。
因此需要回答两个问题:这条消息描述哪个时刻的市场,它又从哪个时刻起能被策略读取?本文将前者记为 event_ts,后者记为 available_ts。接收时间 received_ts 另行保存,用来区分网络传输和解析入库的耗时。统一时区与单位只能统一表示,不能消除交易场所之间的时钟误差。
02 / 先保留事实,再构建时间截面
管线首先追加保存原始事件和修订版本,然后按市场与品种组织状态。设特征窗口截至事件时间 t,策略在决策截止时间 d 读取快照。只有同时满足 event_ts ≤ t 与 available_ts ≤ d 的记录可以参与计算。窗口末端与决策时刻分别保存,计算等待和调度延迟便不会被隐藏。
水位线决定等待程度,不赋予历史可见性
水位线用于表达事件时间的处理进度。多输入算子的进度通常取各输入水位线的最小值;空闲输入可能使进度停滞,需要识别并标记。Apache Flink 官方文档说明了这些机制。
本文按数据分区维护已见最大事件时间 Mᵢ,以配置的乱序容忍量 δᵢ 形成近似进度 Wᵢ = Mᵢ − δᵢ。当参与计算的活跃分区均越过窗口末端时,窗口可按该策略完成。若到达决策截止仍未满足,则发布带完整度标记的快照或跳过决策。数据源空闲时,相关市场同时标为缺失,不能把“停止等待”解释为“报价有效”。
迟到消息和更正记录继续追加,不覆盖已用于决策的快照。研究可以生成修订视图来衡量迟到影响,但重现原决策时仍使用原来的可用时间边界。
03 / 最小事件模型与快照字段
| 字段 | 类型 | 用途 |
|---|---|---|
venue / instrument | 字符串 | 标识市场和统一品种;现货与不同合约分别保留。 |
event_id / revision | 字符串 / 整数 | 标识逻辑事件与修订版本,识别重复投递。 |
event_ts | UTC 微秒 | 事件发生时间;同一事件的内容修订沿用此时间。 |
received_ts | UTC 微秒 | 采集端首次收到该版本的时间。 |
available_ts | UTC 微秒 | 该版本完成解析并可被特征服务读取的时间。 |
source_seq | 整数 | 同一来源内的顺序依据,不用于比较不同市场。 |
event_type / payload | 枚举 / 结构体 | 区分报价、成交等事件,保存价格、数量和单位。 |
quality | 状态集合 | 记录缺失、过期和来源时间质量。 |
特征快照额外保存 window_end、decision_cutoff、计算规则版本、输入事件版本及各市场报价年龄。重复事件按来源、品种、事件编号和修订号合并;回放使用固定顺序处理相同时间的消息。这样既能解释某次信号,也能区分数据变化与规则变化。
04 / 用可见版本执行跨市场对齐
下面的 Python 片段表达快照选择逻辑。输入是已标准化的同一品种报价,时间均为整数微秒;每个事件的修订只更正内容。算法先筛选当时可见的版本,再选择各市场在窗口末端之前的最后一条报价。生产实现可用分区索引减少扫描,选择语义保持一致。
def quote_snapshot(events, window_end, decision_cutoff, max_age):
visible = {}
for event in events:
if event["available_ts"] > decision_cutoff:
continue
if event["event_ts"] > window_end:
continue
key = (event["venue"], event["event_id"])
previous = visible.get(key)
if previous is None or event["revision"] > previous["revision"]:
visible[key] = event
latest = {}
for event in visible.values():
venue = event["venue"]
order = (event["event_ts"], event["source_seq"])
previous = latest.get(venue)
if previous is None or order > (
previous["event_ts"], previous["source_seq"]
):
latest[venue] = event
return {
venue: event
for venue, event in latest.items()
if window_end - event["event_ts"] <= max_age
}
跨市场连接采用向后查找,不选时间上最近但尚未发生的报价。超过最大年龄的报价输出缺失,并保留缺失原因。比较可交易价差时,还须统一报价币种、合约乘数与买卖方向;时间对齐不能替代这些经济含义的对齐。
成交量加权均价等窗口特征采用同一可见性规则,在 (t − L, t] 内累计价格乘数量与数量,再求比值。标准化参数只从训练时间段拟合。新增一条晚到记录可以改变修订后的均价,不能倒流改变原决策输入。
05 / 算例:迟到 10 毫秒,价差方向改变
以下为说明计算规则而构造的报价,价格单位相同。窗口末端为 10:00:01.000,决策截止为 10:00:01.020,报价最大年龄为 200 毫秒。为简化算例,价格取中间价。
- 市场 A:事件发生于
00.950,在00.960可用,价格 100.10。 - 市场 B 的旧报价:事件发生于
00.880,在00.890可用,价格 100.20。 - 市场 B 的新报价:事件发生于
00.980,在01.030可用,价格 99.95。
决策时 B 的新报价仍不可用,因此快照选择 A 的 100.10 和 B 的 100.20,报价年龄分别为 50 与 120 毫秒。按 (B − A) / A × 10,000 计算,价差约为 +9.99 个基点。若事后仅按事件时间连接,会选入 99.95,得到约 −14.99 个基点。方向翻转完全来自可见性差异,不能归因于策略模型。本例未计买卖价差和费用,不用于判断交易收益。
06 / 如何评价数据管线
评价应先检查输入重现,再讨论预测效果。将同一段事件日志按原可用时间顺序回放,逐次对比在线快照的事件版本、缺失标记与特征数值;再与事后完整视图比较,量化迟到数据的影响。后者是诊断参照,不能作为原决策的输入。
- 时间一致性:记录快照差异率,以及触发差异的修订、排序或配置变化。
- 数据新鲜度:分别统计各市场报价年龄、可用延迟分位数与缺失比例,避免总体均值掩盖慢源。
- 等待成本:在不同乱序容忍量下比较窗口完成比例、发布时间和缓存占用。
模型评估按时间向前推进训练与测试。scikit-learn 的 TimeSeriesSplit 文档说明了顺序切分及训练末端的 gap,并要求等间隔数据以便比较各折覆盖的时长。本文进一步按标签的实际结束时间排除跨入测试期的训练数据;固定数据条数间隔不能替代这一检查。
07 / 完整度、延迟与可解释性的取舍
增加等待通常能收齐更多乱序事件,也会推迟特征发布并增加状态占用。允许不完整快照可降低等待,却要求策略显式处理缺失和过期输入。本文选择按市场配置等待与新鲜度阈值,将完整度随特征传递,并分别保存决策视图和修订视图。管线由此给出的不只是一个特征值,还包括这个值在什么时刻、基于哪些信息成立。
参考文献
- Apache Flink — Generating Watermarks:事件时间进度、多输入水位线与空闲源处理。
- scikit-learn — TimeSeriesSplit:时间序列切分与 gap 参数。
← 返回研究论文