← 返回研究论文
RESEARCH PAPER 01DATA ENGINEERING

从行情到特征:
构建可回放的市场数据管线

事件时间、迟到数据与跨市场时间对齐,如何共同构成可重现的决策输入。

原创技术研究2023.08.19研究设计与方法

摘要

跨市场策略的输入不是一张天然同步的行情表。不同交易场所的消息存在延迟、乱序和修订,同一时刻生成的特征也可能因回放方式不同而改变。本文为 LUMENOVA 提出一套可回放的数据管线:同时记录事件发生时间与数据可用时间,以水位线管理窗口进度,以时间截面查询对齐市场状态,再将特征及其输入范围保存为决策快照。核心目标是让历史研究重现决策当时能够看到的信息。

01 / 为什么同一份行情会产生不同结论

假设策略每秒比较两个市场的报价。研究时,将事后收齐的数据按交易所时间排序,通常能得到整齐的序列;实盘决策时,某个较早发生的报价却可能尚未到达。若回测直接读取整理后的序列,就会把当时未知的报价放入特征。数据没有来自未来的时间戳,信息却来自未来。

因此需要回答两个问题:这条消息描述哪个时刻的市场,它又从哪个时刻起能被策略读取?本文将前者记为 event_ts,后者记为 available_ts。接收时间 received_ts 另行保存,用来区分网络传输和解析入库的耗时。统一时区与单位只能统一表示,不能消除交易场所之间的时钟误差。

02 / 先保留事实,再构建时间截面

管线首先追加保存原始事件和修订版本,然后按市场与品种组织状态。设特征窗口截至事件时间 t,策略在决策截止时间 d 读取快照。只有同时满足 event_ts ≤ t 与 available_ts ≤ d 的记录可以参与计算。窗口末端与决策时刻分别保存,计算等待和调度延迟便不会被隐藏。

Market FeedsRaw Event LogTime & Version GateAs-of JoinFeature Snapshot
Late RecordsRaw Event LogRevised Research Replay
图 1:迟到数据仍进入事件日志;历史决策快照与事后修订视图通过不同的时间截面查询产生。

水位线决定等待程度,不赋予历史可见性

水位线用于表达事件时间的处理进度。多输入算子的进度通常取各输入水位线的最小值;空闲输入可能使进度停滞,需要识别并标记。Apache Flink 官方文档说明了这些机制。

本文按数据分区维护已见最大事件时间 Mᵢ,以配置的乱序容忍量 δᵢ 形成近似进度 Wᵢ = Mᵢ − δᵢ。当参与计算的活跃分区均越过窗口末端时,窗口可按该策略完成。若到达决策截止仍未满足,则发布带完整度标记的快照或跳过决策。数据源空闲时,相关市场同时标为缺失,不能把“停止等待”解释为“报价有效”。

迟到消息和更正记录继续追加,不覆盖已用于决策的快照。研究可以生成修订视图来衡量迟到影响,但重现原决策时仍使用原来的可用时间边界。

03 / 最小事件模型与快照字段

字段类型用途
venue / instrument字符串标识市场和统一品种;现货与不同合约分别保留。
event_id / revision字符串 / 整数标识逻辑事件与修订版本,识别重复投递。
event_tsUTC 微秒事件发生时间;同一事件的内容修订沿用此时间。
received_tsUTC 微秒采集端首次收到该版本的时间。
available_tsUTC 微秒该版本完成解析并可被特征服务读取的时间。
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 / 完整度、延迟与可解释性的取舍

增加等待通常能收齐更多乱序事件,也会推迟特征发布并增加状态占用。允许不完整快照可降低等待,却要求策略显式处理缺失和过期输入。本文选择按市场配置等待与新鲜度阈值,将完整度随特征传递,并分别保存决策视图和修订视图。管线由此给出的不只是一个特征值,还包括这个值在什么时刻、基于哪些信息成立。

参考文献