3. 行情数据接口协议解析

Level2行情接入的第一步不是写业务代码,而是把“线上跑的是什么字节”搞清楚。交易所或行情服务商对外提供的Level2数据,通常以私有二进制协议承载在TCP或UDP之上:TCP负责可靠、有序的逐笔流,UDP负责低时延的切片快照或组播行情。接口协议解析要回答三个问题:连接怎么建立与维持(传输层)、一条消息的边界在哪里(帧结构)、每个字段的偏移、类型和含义是什么(字段定义)。三者缺一,后续的解析、校验和授权核验都会失去基准。

传输层选择:TCP与UDP各自承担什么

TCP接入的典型形态是“登录—订阅—推送”长连接。客户端先与行情网关建立TCP连接,发送登录报文(携带账号、密码或令牌、协议版本),网关返回登录应答;随后客户端发送订阅报文,声明需要的市场、证券类别和行情类型(如逐笔委托、逐笔成交、委托队列);网关持续推送行情消息,并通过心跳报文维持会话。TCP保证字节流可靠有序,但存在粘包问题——一次recv可能读到半条消息,也可能读到三条半消息,因此必须依靠帧头中的长度字段切分。

UDP接入多用于组播行情或对时延极敏感的切片快照。UDP不保证到达和顺序,协议设计上通常给每条报文加序号(如包序号、频道号),接收端按序号检测丢包并请求补包或等待下一轮快照恢复。UDP报文天然有边界(一个数据报即一条消息),但单包长度受限,大消息需要分片重组。实际工程中常见“TCP做登录订阅与可靠通道、UDP做高速推送”的混合架构。

行情接入协议栈与消息帧结构 行情客户端 登录 / 订阅 / 心跳 帧切分 / 字段解码 序号检测 / 补包 TCP 长连接通道 可靠有序 · 字节流 · 需按长度切帧 UDP 数据报通道 低时延 · 有边界 · 可能丢包乱序 行情网关 / 组播源 登录鉴权 · 订阅管理 逐笔委托 / 逐笔成交 委托队列 / 快照切片 一条行情消息的帧结构(示例字段布局) 帧头 魔数 2B 消息长度 uint32 4B 消息类型 uint16 2B 序号 uint32 4B 时间戳 uint64 8B 消息体(按消息类型解码) 证券代码 · 价格 · 数量 · 方向 · 买卖盘 关键字段定义与解码要点 字段 类型 / 长度 含义 解码注意 证券代码 char[8] 标的唯一标识 定长补零,按ASCII截断 价格 int64 以最小价位为单位 需乘价格精度还原 数量 int64 股数或手数 确认单位与字节序 买卖方向 uint8 1买 2卖 等枚举 以协议文档枚举为准

消息结构:先定边界,再谈字段

二进制协议解析最常见的错误是“按经验猜偏移”。正确做法是先依据协议文档确定帧头布局。以上图示例为例:帧头以2字节魔数开头,用于在字节流中定位消息起点;随后是4字节消息长度(通常指整条消息的总字节数,需确认是否含帧头自身);再是2字节消息类型,决定消息体的解码方式;4字节序号用于丢包检测与顺序恢复;8字节时间戳标记行情生成时刻。帧头之后才是变长或定长的消息体。

基于长度字段切帧的伪代码如下,核心是维护一个接收缓冲区,循环取出完整消息:

buf = bytearray()
while True:
    chunk = sock.recv(65536)
    if not chunk:
        break
    buf.extend(chunk)
    while len(buf) >= 6:                      # 至少够读魔数+长度
        magic = int.from_bytes(buf[0:2], 'little')
        if magic != 0x5A5A:                   # 示例魔数,按文档替换
            buf = buf[1:]                     # 失步,滑动一字节重新找头
            continue
        total = int.from_bytes(buf[2:6], 'little')
        if len(buf) < total:
            break                             # 半包,等待更多数据
        msg = bytes(buf[:total])
        del buf[:total]
        dispatch(msg)                         # 按消息类型分发解码

这段代码体现了三个工程要点:魔数校验用于从失步状态恢复;长度字段决定消息边界,解决TCP粘包与半包;消息类型驱动分发,避免用同一套偏移解析所有消息。UDP侧则相反,一个数据报即一条消息,但仍需校验魔数与长度是否自洽,并对序号跳变做丢包记录。

字段定义:偏移、类型、字节序与精度

字段定义必须逐项落实四件事:起始偏移、数据类型与长度、字节序(大端还是小端)、物理量换算规则。价格字段几乎不会以浮点数传输,而是以整数表示“最小价位倍数”,例如某标的价位精度为0.01元,报文中价格字段为12345,则实际价格为123.45元;若误当成分或直接当元,整条行情就错了。数量字段要确认是股还是手,成交量与委托量单位可能不同。证券代码多为定长字符数组,尾部补零,解码时需截断到第一个零字节。时间戳可能是自某基准时刻起的毫秒数,也可能是HHMMSSmmm的压缩整数,必须按文档换算。

字节序是最容易被忽略的坑。同一网关的不同消息若混用大小端,或抓包工具默认按网络序展示而协议实际为小端,都会导致价格数量出现荒谬数值。建议在解析层显式声明字节序,并用一条已知内容的登录应答或心跳报文做“金标准”回归测试:手工按文档算出期望值,与程序解码结果比对,通过后再解析行情消息。

接入实操:从连上到解对一条逐笔成交

假设某柜台以TCP提供逐笔成交推送,协议文档给出:帧头6字节(魔数0x5A5A小端、总长uint32小端),消息类型0x0102表示逐笔成交,消息体依次为证券代码char[8]、成交时间uint32(HHMMSSmmm)、成交价格int64(价位倍数)、成交数量int64、买卖方向uint8。接入流程为:建立TCP连接→发送登录报文并校验应答→发送订阅报文→进入接收循环切帧→按类型0x0102解码→与快照或成交回报交叉核对。

判断解析是否正确,不能只看“程序没报错”。可设置三类校验:其一,字段合理性,价格应落在涨跌停区间内、数量为正、方向枚举在允许集合内;其二,序号连续性,逐笔消息序号应单调递增,出现跳变即记录丢包区间;其三,业务一致性,将逐笔成交累计量与快照中的累计成交量比对,偏差应在可解释范围内。若价格出现量级错误,优先排查字节序与价位精度;若消息错位或解析出乱码代码,优先排查长度字段是否含帧头、魔数是否失步。

协议文档中的“消息长度”定义必须确认口径:是仅消息体长度、含帧头总长,还是含尾部校验字段。口径理解错一位,切帧就会整体错位,表现为代码乱码、价格离谱或持续失步。
建议为每种消息类型建立字段偏移表与一条样例报文(十六进制),作为单元测试固件。协议升级或网关切换时,先跑样例回归,再放开实盘订阅,可显著降低解析层故障排查成本。

完成协议解析后,得到的才是可被业务使用的结构化行情记录。后续的授权核验、行情类型权限判断与数据质量监控,都建立在这一层“字节到字段”的正确映射之上;因此本节强调的不是把报文读进来,而是把边界、偏移、类型、字节序和精度五件事同时钉死,并用样例与业务校验证明解析结果可信。