2. 逐笔数据源解析与字段映射

逐笔盘口回放的第一步,不是写回放引擎,而是把交易所原始逐笔数据“读懂”。同一笔成交,在逐笔委托流和逐笔成交流里各有一套字段;不同交易所对时间戳精度、编码格式、买卖方向的定义也不一致。如果这一步没有统一,后续的回放时间轴、盘口重建和撮合校验都会建立在错误的前提上。本节围绕“解析—理解—映射”三个动作展开,目标是产出一张可执行的字段映射表。

一、两类逐笔数据流的本质区别

Level-2 逐笔数据通常包含两条独立的数据流:逐笔委托(Order/Tick Order)和逐笔成交(Trade/Tick Trade)。逐笔委托记录的是“订单进入撮合系统”的事件,包含委托编号、买卖方向、价格、数量、订单类型;逐笔成交记录的是“撮合完成”的事件,包含成交编号、买卖双方委托编号、成交价格、成交数量、成交方向。二者是因果关系:委托是成交的输入,成交是委托的结果。

回放盘口时,必须同时消费两条流。只吃成交流,无法还原挂单的增减;只吃委托流,无法知道哪些委托被成交、成交在什么价位。因此解析阶段要明确:每条流各自负责重建盘口的哪一部分状态。

二、字段含义与常见陷阱

以常见的逐笔委托字段为例,需要重点核对以下几项:

  • 委托编号(OrderNo/BidNo):交易所内唯一,但不同交易所编号规则不同,有的按通道递增,有的全局递增。回放时作为主键使用,不能假设跨交易所可比。
  • 买卖方向(Side):常见编码为 1=买、2=卖,也有 B/S 字符或 0/1 编码。必须按数据源文档确认,不能凭经验猜测。
  • 订单类型(OrdType):区分限价、市价、本方最优等。市价单在逐笔委托中可能没有价格字段,回放时需特殊处理。
  • 价格与数量:注意价格单位(元还是厘)和数量单位(股还是手)。部分交易所价格字段为整数放大 10000 倍,直接当元使用会造成数量级错误。

逐笔成交字段的陷阱集中在成交方向和买卖委托编号。成交方向通常表示“主动方方向”,即买方主动成交还是卖方主动成交,这与盘口买卖压力判断直接相关。买卖委托编号用于把成交关联回委托流,若数据源不提供,则只能靠价格和数量近似匹配,回放精度会下降。

三、时间戳精度与编码格式

时间戳是回放排序的核心。常见精度有毫秒(13 位)、微秒(16 位)和纳秒(19 位)。同一交易所内两条流的时间戳精度可能不同,例如委托流为微秒、成交流为毫秒。映射时必须统一到最高精度,低精度数据在缺失位补零,而不是四舍五入。排序时若同一时间戳出现多条记录,需用交易所序号(SeqNo)作为第二排序键,保证回放顺序与真实撮合顺序一致。

编码格式方面,需确认字符集(GBK 或 UTF-8)、字段分隔符(二进制定长、CSV、JSON)以及数值是否为小端序。二进制定长结构在解析时要严格按字节偏移读取,字段错位是最常见的解析故障。

时间戳统一时,切勿把毫秒值乘以 1000 后与微秒值直接比较而不检查单位来源。建议在解析层为每个字段附加单位标签,映射到标准字段时再统一换算。

四、建立原始字段到标准字段的映射表

映射表是本节的核心交付物。标准字段建议围绕回放需求设计:事件时间、事件类型、委托编号、买卖方向、价格、数量、成交编号、买方委托编号、卖方委托编号、交易所序号。下表给出一个示例映射(字段名与编码为教学示例,实际以数据源文档为准):

标准字段逐笔委托原始字段逐笔成交原始字段转换规则
event_timeTransactTime(微秒)TradeTime(毫秒)统一为微秒,毫秒值×1000
event_type固定值 ORDER固定值 TRADE按流类型赋值
order_noOrderNo—直接映射
sideSide(1/2)—1→BUY,2→SELL
pricePrice(×10000 整数)TradePrice(×10000)除以 10000 转为元
volumeVolume(股)TradeVolume(股)直接映射
trade_no—TradeNo直接映射
bid_order_no—BidOrderNo关联委托流
ask_order_no—AskOrderNo关联委托流
seq_noSeqNoSeqNo第二排序键

映射表落地时,建议用一段解析代码验证字段偏移和单位换算是否正确。例如读取一条委托记录后,打印原始字节与解析结果对照,确认价格除以 10000 后落在合理区间(如 5~500 元),数量为 100 的整数倍。若出现价格 0.0005 或数量 3 这类异常值,说明单位或偏移有误。

# 示例:逐笔委托单条记录解析(字段偏移为教学假设)
raw = read_record(stream)          # 定长二进制记录
order = {
    "order_no": int.from_bytes(raw[0:8], "little"),
    "side":     raw[8],
    "price":    int.from_bytes(raw[9:13], "little") / 10000.0,
    "volume":   int.from_bytes(raw[13:17], "little"),
    "time_us":  int.from_bytes(raw[17:25], "little"),
    "seq_no":   int.from_bytes(raw[25:33], "little"),
}
assert 0 < order["price"] < 10000, "价格单位异常"
assert order["volume"] % 100 == 0, "数量单位异常"

下图展示了两条原始数据流经过解析、单位换算、时间统一后,汇入标准回放事件流的结构关系。注意委托流与成交流在映射后仍保持各自的事件类型,通过委托编号建立关联,而不是合并成一条记录。

逐笔数据源解析与字段映射结构 逐笔委托流 OrderNo / Side / Price Volume / TransactTime(μs) OrdType / SeqNo 编码:二进制定长 逐笔成交流 TradeNo / TradePrice TradeVolume / TradeTime(ms) BidOrderNo / AskOrderNo 编码:二进制定长 解析与转换层 按字节偏移读取 价格 ÷10000 转元 时间统一为微秒 方向编码转 BUY/SELL 字段单位校验 异常值断言 输出:标准字段对象 标准回放事件流 event_type = ORDER order_no / side / price / volume event_type = TRADE trade_no / bid_order_no / ask_order_no 排序键 event_time → seq_no 成交通过 bid/ask_order_no 关联回委托

五、映射完成后的校验要点

映射表不是写完就结束,需要用真实数据做三项校验。第一,数量守恒:同一交易日内,逐笔成交的成交量总和应等于日线成交量(允许因集合竞价处理存在少量差异)。第二,关联完整率:成交记录中的买卖委托编号应能在委托流中找到对应记录,关联率低于阈值说明字段映射或数据源缺失有问题。第三,时间单调性:按 event_time 与 seq_no 排序后,时间戳不应出现大幅回退,若回退超过一个合理窗口,需检查是否混入了不同精度的时间字段。

完成以上步骤后,原始逐笔数据才真正变成“可回放”的标准事件流。映射表应作为数据集说明文档的一部分固化下来,后续每次接入新数据源,只需替换原始字段列,转换规则和校验逻辑可以复用。