3、行情数据协议解析(一):FIX协议二进制格式解析、消息头/消息体/校验和、使用FPGA解析FIX协议的关键点

各位同学,今天我们来聊聊行情数据协议解析的第一站——FIX协议。说实话,FIX协议在金融圈里是个老面孔了,从90年代诞生到现在,几乎成了电子化交易的事实标准。但咱们做FPGA硬件加速的,关心的不是它怎么在软件里跑,而是怎么在硬件里把它“吃透”。

我刚开始接触FIX协议时,第一反应是:这玩意儿字段也太多了吧?后来做多了才发现,真正在行情链路里高频传输的,其实就那么几个核心字段。咱们今天就把这些核心点拆开揉碎了讲清楚。

3.1 FIX协议二进制格式概览

FIX协议有两种编码方式:一种是传统的文本版(Tag=Value),另一种是二进制版(FAST/FIX Binary)。咱们做硬件加速,肯定选二进制版。为什么?你想想看,文本解析在FPGA里做,光是ASCII转数字就要消耗大量LUT资源,而二进制格式天然对齐硬件处理。

二进制FIX的消息结构其实很简单,就三部分:

  • 消息头(Header):固定长度,包含消息类型、序列号、时间戳等元信息
  • 消息体(Body):变长部分,承载具体的行情数据(如订单簿、成交记录)
  • 校验和(Checksum):消息末尾的3字节校验值,用于完整性验证

我习惯把FIX消息比作一个“信封”:信封外面写着发件人、收件人(消息头),里面装着信纸(消息体),最后贴个防伪标签(校验和)。

3.2 消息头解析:固定字段的硬件友好设计

二进制FIX的消息头通常是固定长度的,比如24字节或32字节。这在FPGA里简直是“天选之子”——固定长度意味着我们可以用简单的状态机+计数器来解析,不需要复杂的可变长度处理逻辑。

一个典型的消息头包含以下字段:

字段名 字节偏移 长度(字节) 说明
MsgType 0 1 消息类型(如'D'表示行情快照)
SeqNum 1 4 消息序列号(小端序)
SendTime 5 8 发送时间戳(纳秒精度)
BodyLength 13 2 消息体长度(不含头和校验和)
Reserved 15 9 保留字段,通常填0

嗯,这里要注意:不同交易所的FIX实现可能有细微差别。比如有的交易所把SeqNum放在第5字节,有的放在第1字节。我在项目中遇到过一家交易所,它的消息头长度是28字节而不是标准的24字节,当时排查了好久才发现是文档版本没对齐。

我的建议:在写FPGA解析逻辑前,先拿一份真实的抓包数据做验证。别完全相信文档,文档和实现之间往往有“温差”。

3.3 消息体解析:变长字段的挑战

消息体是变长的,长度由消息头里的BodyLength字段指定。这部分是FPGA解析的难点——因为变长意味着我们不能用固定流水线来处理。

我常用的做法是:

  1. 先缓存整个消息体:用BRAM或分布式RAM把BodyLength指定长度的数据存下来
  2. 再按字段类型解析:根据MsgType决定解析模板(比如行情快照和逐笔成交的字段结构不同)
  3. 字段对齐:把变长字段(如股票代码)填充到固定宽度,方便后续处理

举个例子,一个行情快照消息体可能长这样:

// 伪代码:行情快照消息体解析
// 假设BodyLength = 64字节
// 字段布局:
// [0:7]   Symbol (8字节, ASCII, 不足补空格)
// [8:15]  BidPrice (8字节, 定点数, 精度1e-4)
// [16:23] AskPrice (8字节, 定点数)
// [24:27] BidSize (4字节, 整数)
// [28:31] AskSize (4字节, 整数)
// [32:63] Reserved (32字节, 扩展用)

// FPGA解析逻辑(Verilog风格)
always @(posedge clk) begin
  if (body_valid) begin
    symbol    <= body_data[0*8 +: 64];   // 取前8字节
    bid_price <= body_data[8*8 +: 64];   // 取第8-15字节
    ask_price <= body_data[16*8 +: 64];
    bid_size  <= body_data[24*8 +: 32];
    ask_size  <= body_data[28*8 +: 32];
  end
end

你看,一旦确定了字段偏移,FPGA的位切片操作就能发挥优势——一个时钟周期就能把所有字段提取出来,比CPU逐字节解析快几个数量级。

3.4 校验和计算:硬件加速的甜点

FIX协议的校验和计算方式很简单:从消息头第一个字节到消息体最后一个字节,所有字节求和后取低8位,再与消息末尾的3字节校验值比较。

说白了就是:

checksum = (sum_of_all_bytes) & 0xFF
// 然后与消息末尾的3字节ASCII数字比较
// 比如校验和是0x5A,消息末尾就是"090"(ASCII码)

在FPGA里做这个计算,我推荐用流水线加法器树。为什么?因为求和操作可以拆成多级流水,每个时钟周期处理8字节或16字节,吞吐量轻松达到100Gbps以上。

我曾经在一个项目中,用Xilinx的DSP48E2做校验和累加,配合BRAM做数据缓存,实现了单时钟周期处理32字节的校验逻辑。整个模块只用了不到200个LUT,延迟只有3个时钟周期。

避坑指南:我曾经踩过一个坑——校验和计算时忘了排除校验和字段本身。FIX协议规定校验和只计算消息头和消息体,不包括末尾的3字节校验值。如果你把校验值也算进去,那永远算不对。

3.5 使用FPGA解析FIX协议的关键点

做了这么多年FPGA加速,我总结了几个关键点,分享给大家:

  • 状态机设计要简洁:FIX解析的状态机通常只需要3-4个状态(IDLE、HEADER、BODY、CHECKSUM)。别搞复杂了,状态越多越容易出时序问题。
  • 数据对齐是王道:FPGA处理64位或128位数据最拿手。如果消息头是24字节,那就用3个64位周期读完,别一个字节一个字节地读。
  • 预留扩展接口:交易所经常升级协议,比如增加新字段。我习惯在解析模块里留几个“万能槽位”,遇到未知字段就自动跳过,不影响主流程。
  • 错误处理要快:校验和错误或字段越界时,直接丢弃整条消息并上报错误计数器。别尝试修复,行情数据时效性极高,修复不如重传。

下面这张图是我自己画的FIX协议解析流水线结构,大家可以参考:

FIX协议FPGA解析流水线 原始数据流 消息头解析 固定字段提取 消息体缓存 BRAM存储 字段解析 按模板提取 校验和计算(并行) 流水线加法器树 校验和比较 与消息末尾比对 解析后的结构化数据 供后续处理模块使用 错误处理 校验失败→丢弃+计数 输入 解析阶段 校验 输出 错误处理

这张图展示了我常用的五级流水线结构。你看,校验和计算是和消息体解析并行的,这样不会额外增加延迟。错误处理模块挂在旁边,一旦发现校验失败,直接丢弃整条消息,不影响主流水线。

核心要点回顾:

  • 二进制FIX协议由消息头(固定长度)、消息体(变长)、校验和(3字节)三部分组成
  • 消息头解析适合用固定状态机+计数器实现,一个时钟周期可提取所有字段
  • 消息体解析需要先缓存再按模板提取,变长字段要填充到固定宽度
  • 校验和计算用流水线加法器树,吞吐量高且延迟低
  • 预留扩展接口和快速错误处理机制,应对协议升级和异常情况

好了,关于FIX协议的二进制解析就讲到这里。下一节我们会深入另一个主流协议——STEP协议,看看它和FIX有什么异同,以及在FPGA里怎么处理它的特殊字段。各位回去可以拿一份真实的行情抓包数据练练手,纸上得来终觉浅嘛。


专注资料整理