4. 集合竞价数据源与消息解析

在订单簿重建中,集合竞价阶段是最容易“翻车”的时段。原因不在于算法复杂,而在于数据源本身的语义与连续竞价不同:连续竞价期间,订单簿由逐笔委托和逐笔成交持续驱动;而集合竞价期间,交易所通常不发布逐笔委托明细,只发布行情快照中的虚拟集合竞价簿(virtual book),并在撮合完成后一次性给出成交结果。如果解析时把快照字段当成普通盘口、把逐笔成交当成连续撮合,重建出的订单簿就会出现“凭空多出/消失的挂单”。因此,本节的核心任务是:搞清楚三类数据在集合竞价期间的字段含义,并给出可落地的解析方法。

一、三类数据源在集合竞价期间的定位

以沪深交易所常见的 Level-2 行情为例,集合竞价期间可用的数据主要有三类:

  • 行情快照(Snapshot):按固定频率(如 3 秒)发布,包含最新价、成交量、成交额、买卖十档价格与数量,以及集合竞价阶段的虚拟撮合价、虚拟匹配量、未匹配量等字段。它是集合竞价期间唯一能反映订单簿形态的数据源。
  • 逐笔委托(Order):连续竞价期间逐笔推送新增委托与撤单;但在开盘/收盘集合竞价期间,多数交易所不推送逐笔委托,或仅推送部分类型。解析时必须按市场与时段做开关判断,不能默认“有委托流”。
  • 逐笔成交(Trade):集合竞价撮合完成后,以批量成交形式推送,通常带有成交编号、价格、数量、买卖方向标识。它是集合竞价结果的确认,而不是过程数据。

三者的关系可以概括为:快照描述“竞价过程中的虚拟簿状态”,逐笔成交描述“竞价结束后的撮合结果”,逐笔委托在竞价期间大多缺席。下图展示了这一数据流与解析分支:

集合竞价期间三类数据源的解析路径 行情快照 3 秒一帧 · 虚拟簿 逐笔委托 竞价期间多不推送 逐笔成交 撮合后批量推送 解析虚拟撮合价 匹配量 / 未匹配量 买卖十档 → 虚拟簿 按帧覆盖,不累加 时段开关判断 竞价期:跳过委托流 连续期:正常入簿 避免重复计数 结果确认 成交价 = 集合竞价价 成交量核对快照 方向标识辨买卖 集合竞价订单簿状态 虚拟簿 → 撮合结果 → 切换连续竞价

二、行情快照字段的解析要点

集合竞价快照中最关键的字段是虚拟撮合价(部分接口称“参考价/匹配价”)、虚拟匹配量和未匹配量。它们的含义是:若此刻立即撮合,将以该价格成交多少数量、剩余多少未成交。解析时要注意三点:

  1. 快照是覆盖而非增量。每一帧的买卖十档代表当前虚拟簿的截面,重建时应整帧替换,而不是把相邻帧的档位累加,否则会重复计数。
  2. 虚拟撮合价可能为空或为 0。在竞价早期、买卖无交叉时,交易所可能不给出撮合价。解析代码必须做空值保护,不能把 0 当作有效价格参与计算。
  3. 未匹配量的方向。未匹配量通常配合买卖方向字段,表示在虚拟撮合价上买方或卖方剩余的数量,用于判断竞价偏向(买方剩余意味着买压较强)。

一个常见的解析陷阱是把快照的“最新价”当作集合竞价成交价。竞价未结束时最新价往往为 0 或沿用昨收,真正的成交价要等撮合完成后的逐笔成交或收盘快照确认。

三、逐笔委托与逐笔成交的解析方法

逐笔委托在集合竞价期间的处理原则是“先判断时段,再决定是否入簿”。实现上通常维护一个交易阶段状态机:当状态为“开盘集合竞价”或“收盘集合竞价”时,即使收到委托消息,也不将其插入连续竞价订单簿,而是记录到旁路队列或直接忽略;当状态切换为“连续竞价”后,再恢复正常入簿。这样可以避免竞价期间的委托与竞价结果重复计入。

逐笔成交的解析重点在于方向标识与去重。集合竞价撮合完成后,交易所会推送一批成交记录,每条记录包含成交编号、价格、数量以及买卖方向(部分市场用 BS 标志,部分用内外盘标识)。解析时应:

  • 以成交编号或“时间戳+序号”作为唯一键去重,防止重传导致重复累加成交量;
  • 校验成交价是否等于快照中的虚拟撮合价,若不等,说明快照与成交之间存在时序差,应以成交为准;
  • 将成交量的总和与快照中的累计成交量字段比对,作为数据完整性的自检。

四、一个可执行的解析流程示例

下面给出一个简化的伪代码,展示如何在同一套消息处理循环中区分三类数据:

on_message(msg):
    if msg.type == "SNAPSHOT":
        book.replace_virtual(msg.bid_levels, msg.ask_levels)
        book.virtual_price = msg.match_price or None
        book.unmatched = (msg.unmatched_side, msg.unmatched_qty)

    elif msg.type == "ORDER":
        if phase in ("OPEN_AUCTION", "CLOSE_AUCTION"):
            return                      # 竞价期不入连续簿
        book.insert_order(msg)

    elif msg.type == "TRADE":
        if msg.trade_id in seen_ids:
            return                      # 去重
        seen_ids.add(msg.trade_id)
        book.apply_trade(msg.price, msg.qty, msg.side)
        if phase == "AUCTION_RESULT":
            assert abs(msg.price - book.virtual_price) < 1e-6

这段逻辑的关键在于:快照走“替换虚拟簿”分支,委托走“时段判断”分支,成交走“去重+结果确认”分支。三条分支互不干扰,才能保证集合竞价切换瞬间订单簿状态一致。

五、解析质量的判断依据

完成解析后,可以用几个指标自检:竞价结束时的累计成交量是否与逐笔成交汇总一致;虚拟撮合价与最终成交价是否吻合;切换连续竞价后第一帧快照的买卖档位是否与重建簿的档位连续。若出现成交量对不上或档位跳变,通常说明快照被误当增量累加,或竞价期委托被错误入簿。把这些检查固化为解析模块的断言,比事后人工排查更可靠。

提示:不同交易所对集合竞价期间逐笔委托的推送策略不同,接入前应查阅对应市场的行情接口文档,并在代码中用配置项显式声明“竞价期是否接收委托流”,而不是依赖默认行为。