AllTick
-
逐笔数据实时市场数据金融数据 api量化交易

Tick 级数据接口如何处理乱序数据?从时间戳排序到重复记录清理

了解 Tick 级数据接口如何通过时间戳排序、序列号去重和迟到数据处理,减少旧行情覆盖、重复统计及实时行情处理中的数据错误。

AllTick阅读 5 分钟

使用 Tick 级数据接口开发实时行情系统时,数据成功接收并不代表处理结果一定正确。如果较早的行情晚于较新的行情到达,程序可能覆盖最新价格;如果 WebSocket 重连后收到重复记录,成交量统计和后续计算也可能受到影响。

对金融科技开发者来说,关键不是简单地给数据排序,而是区分事件发生时间与接收时间,再结合事件标识、重排缓冲区和迟到数据规则处理异常。

为什么 Tick 数据会乱序?

Tick 数据通常通过 WebSocket 持续推送。网络延迟、消息队列积压和多线程消费,都可能导致数据的接收顺序与事件时间不一致。

例如,系统先收到时间戳为 10:00:00.120 的行情,随后才收到时间戳为 10:00:00.110 的行情。如果程序每收到一条消息就直接更新最新价格,后一条到达的数据反而会让页面显示旧价格。

这里需要区分两个概念:

  • 事件时间:行情记录对应的时间。
  • 接收时间:客户端收到行情记录的时间。
    下面是用于说明问题的模拟数据,不代表真实市场行情。
到达顺序事件时间报价序号价格
11000501100.20
21020503100.25
31010502100.22
41020503100.25

第三条记录的事件时间早于第二条,属于乱序到达;第四条记录与第二条具有相同的示例序号和价格,可以作为重复事件识别的演示。

实际处理时,不能仅凭价格和时间戳相同就删除记录。两条真实行情可能具有相同的价格,甚至拥有相同的时间戳。是否重复,应优先依据数据源提供的事件标识或序列号判断。

如何使用时间戳排序和序列号去重?

先整理一批已经接收的 Tick 数据,再按统一格式的事件时间排序,是实现基础数据校正的一种简单方法。

AllTick 的实时逐笔行情订阅文档列出了 code、seq、tick_time、price 和 volume 等推送字段。其中,seq 表示报价序号,tick_time 表示行情时间戳。需要注意,这类订阅提供实时最新成交价,不等同于历史逐笔成交查询接口。

下面使用这些字段编写一个简化的 Python 示例。示例假定时间戳已经统一为可比较的整数,并且只有在确认同一产品的 seq 能够稳定识别同一条记录时,才启用基于序列号的去重。

text
def clean_ticks(ticks, seq_is_unique_per_code=False):
    seen = set()
    accepted = []
    rejected = 0

    for index, tick in enumerate(ticks):
        code = tick.get("code")
        tick_time = tick.get("tick_time")
        seq = tick.get("seq")

        if not code or tick_time is None:
            rejected += 1
            continue

        try:
            event_time = int(tick_time)
        except (TypeError, ValueError):
            rejected += 1
            continue

        # 只有确认序列号的唯一性后才启用去重
        if seq_is_unique_per_code and seq is not None:
            key = (code, str(seq))
            if key in seen:
                continue
            seen.add(key)

        # 序列号用于辅助整理相同时间戳的记录
        try:
            seq_key = (0, int(seq))
        except (TypeError, ValueError):
            seq_key = (1, str(seq or ""))

        accepted.append(
            (event_time, seq_key, index, tick)
        )

    accepted.sort(key=lambda item: (
        item[0], item[1], item[2]
    ))

    return [item[3] for item in accepted], rejected

这段代码完成了三件事:检查必要字段、在指定条件下按产品代码与报价序号去重,以及按事件时间和序列号排序。原始接收顺序用于处理排序键完全相同的记录。

实际接入时还需注意两个边界。第一,如果 seq 可能重复使用,或重连后不能稳定标识同一事件,就不能直接使用 (code, seq) 作为去重键。第二,如果时间戳单位不一致,必须先完成规范化,否则数值排序可能产生错误结果。

代码中的去重开关默认关闭。只有核实实际数据的序列号语义后,才能将 seq_is_unique_per_code 设置为 True。示例没有实现跨批次状态保存,也没有将其描述为完整的生产级处理方案。

实时行情如何处理迟到数据?

批次排序只能解决已经收集到的数据顺序问题。如果行情持续通过 WebSocket 到达,系统还需要决定何时释放数据,以及超过等待时间的记录应该如何处理。

常见做法是维护一个有界重排缓冲区,并根据事件时间设置水位线。例如,系统记录目前见过的最大事件时间,再减去允许的迟到范围,得到当前处理水位线。早于水位线的记录可以进入正常处理流程;水位线之后才到达的旧记录,则按迟到数据规则单独处理。

允许的迟到范围需要结合实际行情的到达延迟和业务要求确定,不能为所有品种设置一个固定值。

对于实时价格展示,如果迟到记录的事件时间早于当前已发布的最新状态,就不应直接覆盖最新价格。对于历史统计或 K 线重建,可以将迟到记录保存下来,并在需要时修正受影响的统计结果。两种场景的处理目标不同,不宜共用一条简单的覆盖规则。

去重也需要考虑批次边界。只在一次函数调用中保存已处理的序列号,无法识别后续批次或服务重启后再次出现的记录。需要持续去重的系统,应根据数据源的标识规则保存必要状态,并在断线重连后检查数据连续性。

接入 AllTick 前需要核对什么?

在使用 AllTick 构建逐笔行情处理模块时,建议先根据目标品种核对实时订阅的数据结构,再决定如何排序、去重和处理迟到数据。不要仅根据字段名称推断其含义,也不要假设所有品种都具有相同的事件标识能力。

尤其要确认时间戳的实际单位和序列号的唯一性范围。官方实时逐笔订阅文档将 tick_time 标注为毫秒,但页面示例中展示的时间戳是 10 位数字,两者的呈现存在需要进一步确认的地方。接入前应以实际响应和经确认的接口说明为准,不应未经核实就将其直接转换为秒或毫秒。

如果你正在开发实时行情服务,可以先从 AllTick 官方网站 进入相关产品与文档页面,确认目标品种的推送字段,再使用包含乱序、重复和迟到记录的测试数据验证处理逻辑。

对 Tick 级数据接口来说,时间戳排序解决的是事件先后问题,序列号去重解决的是事件识别问题,而迟到处理决定的是系统如何处理未能按预期时间到达的数据。把这三部分分开设计,才能减少旧行情覆盖、重复统计和回测数据失真的风险。

立即开始接入市场数据

几秒内生成免费 API 密钥,通过一个端点连接所有市场。