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 排序,避免同一毫秒内多条事件顺序错乱。下图展示了事件流在时间轴上的推进方式,以及事件与快照的对应关系。
从图中可以读出一条重要规则:快照是事件累积作用的结果,而不是事件的替代品。S1 到 S2 之间发生了委托、成交、撤单三类事件,S2 的盘口必须等于 S1 盘口叠加这三条事件后的状态。若 S2 与推演结果不一致,说明事件丢失、重复或字段解析有误,这正是联动展示最有价值的排障场景。
四、事件与快照的对应关系
对应关系有两种常见口径,必须明确选择其一并在全文保持一致。第一种是“区间归属”:事件归属于其 ex_time 落在 (Sk, Sk+1] 区间内,即事件作用于下一张快照。第二种是“时点归属”:事件归属于 ex_time 之前最近的一张快照。两种口径在边界事件上结果不同,联动展示时若前后端口径不一致,就会出现“事件已显示但盘口未变”的错觉。
实操中建议采用区间归属,并在解析层维护一个“待应用事件队列”:每收到一条事件,先按 (market, seq) 去重,再按类型更新内存订单簿;每收到一张快照,用快照覆盖内存订单簿并记录差异。差异为零说明对齐正确,差异非零则输出告警,附带 symbol、seq、字段名与两侧取值,便于定位。
最后给出一个最小可用的解析伪代码,帮助把上述约定落到实现层:
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()
把字段、编码、时序、对应关系四件事定清楚之后,逐笔事件才真正具备“可解析、可回放、可校验”的工程属性,后续的联动展示才有可靠的数据基础。