3. 历史行情数据源解析

重建订单簿历史一致性,第一步不是写撮合代码,而是把数据源“读对”。同一只股票、同一个交易日,逐笔委托、逐笔成交、快照行情三类数据在字段含义、时间戳语义和更新机制上差异很大;如果混用或误读,重建出的订单簿会在价格档位、数量甚至买卖方向上系统性偏离真实市场。本节围绕这三类常见数据源,说明各自的关键字段、时间语义,以及它们对一致性重建的具体影响。

一、逐笔委托:订单簿的“增量来源”

逐笔委托(Order/Tick Order)记录的是每一笔新进入撮合系统的委托,以及部分市场中的撤单事件。典型字段包括:证券代码、委托时间、委托序号(交易所通道内的唯一递增编号)、买卖方向、委托价格、委托数量、订单类型(限价/市价/本方最优等)。在深交所等市场中,撤单也以独立记录出现,方向字段常以特定编码表示,需要按交易所文档映射,不能简单当作“卖方向委托”。

逐笔委托的核心价值在于它是订单簿的增量输入:一条限价买单意味着在对应价位增加数量,一条撤单意味着从原价位扣减数量。重建时通常维护“价格档位 → 总量”的聚合结构,或“订单号 → 剩余量”的明细结构。前者内存小但无法精确处理部分成交,后者更贴近真实订单簿但需要与逐笔成交做订单号关联。实际工程中,若数据源提供订单号且逐笔成交也带订单号,应优先采用明细结构,否则只能用聚合结构并接受一定误差。

二、逐笔成交:数量扣减与方向推断

逐笔成交(Trade/Tick Trade)记录每一笔撮合结果,典型字段包括:成交时间、成交序号、成交价格、成交数量、买卖方向标识(部分市场为主动方方向)、买方订单号与卖方订单号(部分市场提供)。逐笔成交本身不直接改变订单簿的“挂单结构”,但它对应着委托数量的扣减:一笔成交意味着买方订单和卖方订单各减少相应数量。

这里最容易出错的是方向语义。部分数据源的“买卖方向”表示主动成交方(即吃单方),而非挂单方;若用它去扣减订单簿,会把方向弄反。正确做法是:有订单号时按订单号扣减;无订单号时,结合成交价与当前买一/卖一判断主动方,再推断被吃掉的是哪一侧挂单。此外,逐笔成交与逐笔委托的序号体系可能不同,跨数据源关联必须依赖订单号或时间+价格+数量的启发式匹配,并记录匹配失败率作为一致性指标。

三、快照行情:定时切片与校验基准

快照行情(Snapshot)按固定频率(如每 3 秒)发布当前订单簿的切片,典型字段包括:快照时间、最新价、成交量、成交额、买一至买十/卖一至卖十的价格与数量、委托总量等。快照不是增量,而是某一时刻的“状态截面”,因此它天然适合作为重建结果的校验基准:在快照时间点上,将重建订单簿的前若干档与快照逐档比对,价格和数量都应一致(允许因发布延迟产生的少量偏差)。

快照的时间戳语义需要特别注意:它通常表示“快照生成时间”而非“数据生效时间”,且不同交易所对时间戳的精度(秒/毫秒)和时区约定不同。重建时应统一转换为同一时区与精度,并以“快照时间之前已处理的全部逐笔事件”作为对齐边界,避免把快照时间之后的事件提前计入。

三类数据源 → 订单簿重建 → 快照校验 时间 → t1 t2 t3 t4(快照点) 逐笔委托(增量) 委托序号 · 方向 · 价格 · 数量 含撤单记录,需按文档映射 时间戳:委托进入撮合时间 逐笔成交(扣减) 成交序号 · 价格 · 数量 · 方向 方向多为主动方,勿直接扣挂单 时间戳:撮合成交时间 快照行情(截面) 买一~买十 / 卖一~卖十 价量 定时切片,非增量 时间戳:快照生成时间 订单簿重建引擎 按序号/时间排序 → 增量应用 → 状态维护 统一时区与时间精度,确定对齐边界 快照逐档比对校验 价格/数量一致 → 一致性通过 一致性风险点 方向语义误读 时间戳精度/时区不一致 跨源序号无法直接关联

四、时间戳语义:一致性的隐形杀手

三类数据源的时间戳往往不是同一含义:逐笔委托是“委托进入撮合系统的时间”,逐笔成交是“撮合完成时间”,快照是“切片生成时间”。在重建时,必须明确以哪条时间线排序。推荐做法是:以交易所发布的序号为主排序键,时间戳为辅;当序号缺失时,按时间戳排序并规定“同一时间戳内先委托后成交”的稳定顺序。若直接把三类数据按各自时间戳混排,可能出现“成交先于其对应委托被处理”的倒挂,导致扣减失败或负数量。

另一个细节是时间精度。若逐笔数据为毫秒、快照为秒,对齐快照边界时应取“快照时间 ≤ 当前处理时间”的最后一个快照,而不是四舍五入。时区上,统一转为交易所本地时间或 UTC 均可,但全流程必须一致,并在元数据中记录转换规则,便于复现。

五、更新机制差异与重建策略

逐笔委托与逐笔成交是事件流,适合“逐条应用”的增量重建;快照是状态流,适合“定时对齐”的校验。工程上常见的组合是:以逐笔委托构建初始订单簿,逐笔成交扣减数量,每到一个快照时间点做一次全档比对,若偏差超过阈值(例如某档数量偏差超过 1 手或价格档位错位),则记录异常并考虑用快照做局部重置。重置会引入“状态跳变”,因此必须记录重置点,保证回测中订单簿状态可追溯。

下面是一段示意性的对齐逻辑(示例参数,非实测结果):

events = merge(order_ticks, trade_ticks, key=seq_no)   # 以序号为主键
book = OrderBook()
last_snap = None
for e in events:
    if e.time >= next_snapshot_time:
        snap = get_snapshot(next_snapshot_time)
        diff = compare(book.topN(10), snap.topN(10))
        if diff.max_qty_gap > 100 or diff.price_mismatch > 0:
            log_anomaly(snap.time, diff)
        last_snap = snap
    book.apply(e)   # 委托加量 / 撤单减量 / 成交扣减

判断重建是否可信,不能只看“跑通没报错”,而要看三类指标:快照比对的一致档位比例、负数量或越界扣减的出现次数、跨源订单号匹配失败率。三者共同构成数据源层面的质量画像,也是后续回测结论是否可靠的前提。

实践建议:拿到新数据源时,先抽取一个交易日,统计逐笔委托与逐笔成交的序号连续性、时间戳单调性、快照档位完整率,再决定采用明细订单簿还是聚合订单簿。数据源的“脾气”摸清了,重建的一致性才有保障。