2. 逐笔数据特征与校验挑战
逐笔数据(tick-by-tick)是交易所行情中最细粒度的一类数据:逐笔委托记录每一笔新订单的进入、撤单与交易所内部撮合产生的成交,逐笔成交则记录每一笔撮合成交的价格、数量与买卖方向。与快照行情相比,逐笔数据不是"某一时刻的截面",而是"一段时间内事件的完整序列"。这一本质差异决定了它的校验方式不能照搬快照的"逐字段比对",而必须围绕序列的完整性、顺序性和一致性来设计。
一、逐笔数据的四个固有特征
高频。以沪深交易所为例,单只活跃股票在开盘集合竞价与尾盘阶段,每秒可产生数十到数百条逐笔记录;全市场一天累计的逐笔委托与成交记录可达数亿条量级。高频带来的直接后果是:校验不能依赖人工抽样,必须全量流式处理;同时时间戳精度往往到毫秒甚至微秒,任何按秒聚合的校验都会掩盖乱序问题。
乱序。逐笔数据在传输链路上会经过交易所撮合系统、行情网关、专线、落地服务器、消息队列等多个环节。不同通道的延迟抖动、多线程发送、网络重传,都会导致记录到达顺序与交易所产生顺序不一致。乱序的典型表现是:同一只股票的两条记录,序号(如交易所的 SeqNum 或应用层序号)递增,但本地接收时间戳倒挂。校验时必须区分"交易所序号顺序"与"本地到达顺序",前者才是业务真序。
重复。重复来源有两类:一是传输层重传(如组播补包、TCP 重连后的重发),二是上游系统"至少一次"投递语义导致的重复写入。重复记录如果只按"记录内容相同"去重,会误删同一毫秒内真实发生的两笔同价同量成交。正确做法是结合交易所序号或唯一消息 ID 判重,而不是靠字段值比对。
缺失。缺失比重复更隐蔽。逐笔数据缺失可能发生在单只股票上(某段序号不连续),也可能发生在整个通道上(某台行情网关宕机导致一段时间无数据)。缺失的直接判据是序号断档,但序号断档也可能是"该股票本就没有事件"造成的假象——例如某冷门股长时间无委托,序号自然不增长。因此缺失校验必须结合"该股票是否处于交易状态""同通道其他股票是否有数据"等上下文来判断。
二、端到端校验的核心难点
把上述特征放到"端到端"的语境下,校验的难点不再是单点规则,而是判据之间的相互干扰。具体表现为三组矛盾。
第一组:乱序与缺失的混淆。当本地按到达顺序处理时,一条迟到的记录会让序号看起来"先断后补"。如果校验程序一看到断档就报警,会产生大量误报;如果等到收盘再统一判断,又失去了实时发现缺失的能力。工程上的折中做法是设置一个"乱序容忍窗口":例如允许记录在 500ms 内乱序到达,窗口内序号断档暂不判定为缺失,窗口滑过后仍缺才升级为缺失告警。窗口长度需要根据实测的链路抖动分布来定,不能拍脑袋。
第二组:重复与真实同质事件的混淆。同一毫秒内出现两笔价格、数量、方向完全相同的成交,在活跃股上是完全可能的真实事件。若用"内容哈希"去重,就会误删。因此判重键必须包含交易所侧的唯一标识(序号或消息 ID),并且去重操作要放在"按序号排序"之后,否则重复记录可能因到达顺序不同而被误判为乱序。
第三组:高频下的状态维护成本。逐笔校验需要为每只股票维护"期望的下一个序号""已见序号集合""委托剩余量"等状态。全市场数千只股票、数亿条记录,状态内存与去重集合的规模会迅速膨胀。实践中常用"按股票分片 + 滑动窗口"的方式:每只股票只保留最近 N 条序号的状态,窗口外的历史序号不再参与判重,从而把内存控制在可接受范围。代价是极晚到达的重复记录可能漏判,这属于可接受的工程取舍,但必须在校验报告中注明。
三、一个可落地的校验判定示例
下面给出一个针对单只股票的逐笔校验伪代码片段,展示三条判据如何协同工作。示例参数(窗口 500ms、序号起点 1001)均为教学假设,实际取值应来自链路实测。
# 教学示例:单股票逐笔校验核心逻辑(伪代码)
WINDOW_MS = 500 # 乱序容忍窗口
expected_seq = 1001 # 期望的下一个交易所序号
seen = set() # 窗口内已见序号,用于判重
pending = {} # 序号 -> 记录,暂存窗口内乱序记录
def on_tick(rec):
seq, ts_ex, ts_local = rec.seq, rec.exchange_ts, rec.local_ts
# 判据③:时序合理性
if not in_trading_session(ts_ex):
report("时序异常", rec); return
# 判据②:业务一致性(委托剩余量)
if rec.type == "ORDER" and rec.remain < 0:
report("委托剩余量为负", rec); return
# 判重:以交易所序号为键,而非内容哈希
if seq in seen:
report("重复记录", rec); return
seen.add(seq)
# 判据①:序号连续性 + 乱序窗口
if seq == expected_seq:
emit(rec); expected_seq += 1
# 尝试从 pending 中续接
while expected_seq in pending:
emit(pending.pop(expected_seq)); expected_seq += 1
elif seq > expected_seq:
pending[seq] = rec # 暂存,等待窗口判定
if ts_local - window_start > WINDOW_MS:
report("疑似缺失", expected_seq, seq)
expected_seq = seq + 1 # 跳过缺口,继续校验
else:
report("序号回退", rec) # seq < expected_seq 且未在 seen 中
这段逻辑的关键点在于:判重先于判序,避免重复记录污染序号状态;乱序记录进入 pending 而非直接报错,给迟到记录一个续接机会;窗口超时后才判定缺失,并在报告中记录缺口区间,便于后续补包或标记。运行后应观察三类输出:正常 emit 的记录数、pending 中最终未续接的记录数(即真实乱序残留)、以及缺失缺口列表。若某只股票的缺失缺口在收盘后仍无法补齐,则该股票当日逐笔数据应标记为"不完整",下游回测或指标计算需据此降级处理。
需要强调的是,逐笔校验的目标不是"零告警",而是"告警可解释、缺口可追溯"。高频、乱序、重复、缺失是逐笔数据的常态而非异常,校验系统的价值在于把不可见的链路问题转化为可定位、可量化的数据质量指标,让下游使用者清楚知道"这份逐笔数据在哪些时段、哪些标的上是可信的"。