3. 逐笔事件类型与语义

盘口快照描述的是某一时刻的“静态切片”,而逐笔事件描述的是两次切片之间“发生了什么”。要把两者联动展示,第一步不是画图,而是把逐笔事件本身讲清楚:一条事件由哪些字段构成、事件类型如何编码、事件流在时间轴上有什么特性,以及一条事件究竟对应到哪一张快照。本节围绕这四个问题展开,并给出可直接用于解析与对齐的字段约定和示例。

一、逐笔事件的字段定义

一条逐笔事件本质上是一条带时间戳的“状态变更记录”。工程上通常拆成三层字段:标识层、时间层、内容层。标识层用于唯一定位事件,至少包含证券代码、市场/通道号、事件序号;时间层包含交易所时间戳与本地接收时间戳;内容层则随事件类型不同而不同,但应保留统一的公共字段,便于统一解析。

下面是一份可直接落地的字段约定示例(字段名与取值均为教学示例,实际以所用行情源文档为准):

公共字段
  symbol        证券代码,如 "600000"
  market        市场标识,如 "SH"
  seq           通道内事件序号,单调递增
  ex_time       交易所时间,精确到毫秒或微秒
  local_time    本地接收时间,用于测延迟
  type          事件类型编码,见下节
  price         委托价/成交价,单位:元
  volume        委托量/成交量,单位:股
  side          方向,"B" 买 / "S" 卖
  order_id      委托编号(委托、撤单必填)
  trade_id      成交编号(成交必填)
  bs_flag       成交方向标志,"B" 主动买 / "S" 主动卖 / "N" 未知

需要特别强调两点。第一,价格与数量必须明确单位与精度:A 股价格通常以 0.01 元为最小变动单位,若行情源以“分”为整数传输,解析时必须除以 100,否则盘口价格会整体放大 100 倍。第二,order_id 与 trade_id 是联动的关键外键:成交事件通过 order_id 关联到具体委托,撤单事件通过 order_id 关联到被撤委托,缺少这两个字段就无法还原“哪笔委托被成交/被撤销”。

二、事件类型编码

事件类型编码应满足“可扩展、可判等、可分组”。常见做法是用一个整型或短字符串编码,并在解析层维护一张映射表。教学示例可采用如下编码:

编码事件类型语义关键字段
1委托(Order)新增一笔买卖委托进入订单簿order_id, price, volume, side
2成交(Trade)两笔委托撮合成功trade_id, order_id, price, volume, bs_flag
3撤单(Cancel)某笔未成交委托被撤销order_id, volume(撤销量)
4状态(Status)开收盘、临时停牌等市场状态变更状态码

编码设计上有两个易错点。其一,不要把“成交”与“主动方向”混为一谈:成交事件本身只说明撮合发生,bs_flag 才表达是买方主动还是卖方主动,二者是不同维度。其二,撤单要区分“全撤”与“部撤”:若 volume 字段表示撤销量,则部撤时该值小于原委托量,剩余量仍留在订单簿中;若表示撤后剩余量,语义完全相反。解析前必须与数据源文档核对,否则盘口数量会算错。

三、事件流的时间序列特性

逐笔事件流不是简单的“按时间排序的列表”,它有三个必须处理的特性:有序性、并发性与乱序性。有序性指同一通道内 seq 单调递增,可作为去重与断点续传的依据;并发性指不同通道(如不同市场、不同撮合引擎)的事件在本地接收时可能交错到达;乱序性指网络抖动可能导致 ex_time 与到达顺序不一致。

因此工程上通常采用“双时间轴”策略:以 ex_time 作为业务时间轴用于展示与回放,以 local_time 作为接收时间轴用于监控延迟;排序时优先按 (market, seq) 排序,而不是仅按 ex_time 排序,避免同一毫秒内多条事件顺序错乱。下图展示了事件流在时间轴上的推进方式,以及事件与快照的对应关系。

逐笔事件流与快照的时间对应关系 业务时间轴(ex_time)用于展示与回放;接收时间轴(local_time)用于延迟监控 时间 快照 S1 快照 S2 快照 S3 委托 #A001 买 10.00 × 500 成交 #T001 10.00 × 200 主动买 撤单 #A001 撤 300 股 委托 #A002 卖 10.02 × 400 成交 #T002 10.02 × 400 主动卖 状态变更 临时停牌 S1 → S2 之间:委托、成交、撤单依次作用于订单簿 S2 → S3 之间:新委托、成交、状态变更 订单簿状态推演(买一档,示例) 初始:买一 10.00 × 500 成交 200 后:买一 10.00 × 300 撤单 300 后:买一档清空 S2 快照应体现:买一档为空,卖一 10.02 × 400 成交 #T001 关联 order_id=A001 撤单 #A001 关联 order_id=A001 成交 #T002 关联 order_id=A002 状态事件不改变订单簿,仅改变可交易状态

从图中可以读出一条重要规则:快照是事件累积作用的结果,而不是事件的替代品。S1 到 S2 之间发生了委托、成交、撤单三类事件,S2 的盘口必须等于 S1 盘口叠加这三条事件后的状态。若 S2 与推演结果不一致,说明事件丢失、重复或字段解析有误,这正是联动展示最有价值的排障场景。

四、事件与快照的对应关系

对应关系有两种常见口径,必须明确选择其一并在全文保持一致。第一种是“区间归属”:事件归属于其 ex_time 落在 (Sk, Sk+1] 区间内,即事件作用于下一张快照。第二种是“时点归属”:事件归属于 ex_time 之前最近的一张快照。两种口径在边界事件上结果不同,联动展示时若前后端口径不一致,就会出现“事件已显示但盘口未变”的错觉。

实操中建议采用区间归属,并在解析层维护一个“待应用事件队列”:每收到一条事件,先按 (market, seq) 去重,再按类型更新内存订单簿;每收到一张快照,用快照覆盖内存订单簿并记录差异。差异为零说明对齐正确,差异非零则输出告警,附带 symbol、seq、字段名与两侧取值,便于定位。

判断对齐是否成功的实用依据:对同一 symbol,连续 N 张快照的“事件推演盘口”与“实际快照盘口”在买一/卖一价格与数量上完全一致,且事件序号无跳变、无重复。教学示例中 N 可取 20~50,足以覆盖一次集合竞价到连续竞价的切换。
常见误区:把 bs_flag 当作委托方向使用。委托事件的 side 表示挂单方向,成交事件的 bs_flag 表示主动方方向,二者语义不同。若在联动展示中混用,会出现“买单却显示为主动卖”的错误标注。

最后给出一个最小可用的解析伪代码,帮助把上述约定落到实现层:

for ev in event_stream:              # 已按 (market, seq) 排序
    if ev.seq <= last_seq[ev.market]: continue   # 去重
    last_seq[ev.market] = ev.seq
    if ev.type == 1:  book.add_order(ev)
    elif ev.type == 2: book.apply_trade(ev)
    elif ev.type == 3: book.apply_cancel(ev)
    elif ev.type == 4: book.set_status(ev)
    pending.append(ev)               # 等待下一张快照校验

on_snapshot(snap):
    diff = compare(book, snap)
    if diff: alert(snap.symbol, diff)
    book = snap; pending.clear()

把字段、编码、时序、对应关系四件事定清楚之后,逐笔事件才真正具备“可解析、可回放、可校验”的工程属性,后续的联动展示才有可靠的数据基础。