4. 行情数据解析模块设计

行情数据解析,说白了就是给FPGA喂二进制数据,让它能听懂交易所在说什么。我最早接触这个模块时,觉得不就是个协议解析嘛,有什么难的?结果第一次上板调试,数据全乱套了——嗯,从那以后我再也不敢小看这个模块了。

核心要点:行情解析模块是量化交易系统的"耳朵",听错了,后面所有策略都是白搭。

4.1 二进制行情协议解析(以CTP/ITCH为例)

先说说CTP协议。中金所、上期所用的就是它。CTP的报文结构其实挺规整的:

  • 消息头:固定7字节,包含消息长度、消息类型、业务类型
  • 消息体:根据消息类型不同,长度可变
  • 校验域:通常是CRC16或者简单的异或校验

我个人习惯把CTP协议拆成三层来处理:

层级 功能 FPGA实现要点
物理层 字节对齐、帧定界 用状态机检测帧头0xAA 0xBB
链路层 长度校验、CRC校验 并行CRC计算,一个时钟周期搞定
应用层 行情字段提取 按偏移量直接截位,别用循环

再说ITCH协议。这是纳斯达克用的,比CTP更"原始"一些。ITCH的每条消息都是自描述的:

// ITCH消息结构示例
typedef struct {
    uint16_t msg_length;    // 消息总长度(包含自身)
    uint8_t  msg_type;      // 消息类型:'S'=系统事件, 'A'=新增订单
    uint64_t timestamp;     // 纳秒时间戳
    // ... 后面跟着具体字段
} ITCH_HEADER;

我在项目中遇到过一个问题:ITCH的某些消息类型长度是固定的,有些是变长的。一开始我统一用变长处理,结果资源消耗翻了一倍。后来改成固定长度消息用硬连线解析,变长消息才用状态机,资源立马降下来了。

小技巧:解析二进制协议时,尽量用"截位+拼接"代替"移位+掩码"。FPGA里截位不花钱,移位要花LUT。

4.2 状态机设计模式

行情解析的核心就是状态机。我常用的模式是"三段式状态机":

// 三段式状态机示例
// 第一段:状态转移
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        state <= IDLE;
    else
        state <= next_state;
end

// 第二段:次态逻辑
always @(*) begin
    case (state)
        IDLE: begin
            if (rx_valid && rx_data == 8'hAA)
                next_state = HEADER1;
            else
                next_state = IDLE;
        end
        HEADER1: begin
            if (rx_valid && rx_data == 8'hBB)
                next_state = LENGTH;
            else
                next_state = IDLE;  // 帧头不对,重新同步
        end
        // ... 其他状态
    endcase
end

// 第三段:输出逻辑
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        msg_valid <= 1'b0;
    else if (state == CHECK && next_state == DONE)
        msg_valid <= 1'b1;
    else
        msg_valid <= 1'b0;
end

为什么用三段式?因为组合逻辑和时序逻辑分开,调试时一眼就能看出问题在哪。我曾经接手过一个同事写的两段式状态机,状态转移和输出混在一起,查一个bug查了三天——从那以后我坚持用三段式。

状态机设计时要注意几个坑:

  • 默认状态:一定要有default,否则综合出来一堆锁存器
  • 超时处理:如果某个状态卡住了,要有超时跳回IDLE
  • 错误恢复:CRC校验失败后,要能快速重新同步

4.3 数据校验与错误处理

行情数据在传输过程中可能出错。我见过最离谱的一次,光纤被老鼠咬了一口,行情数据隔三差五就跳几个bit。所以校验这块必须做扎实。

常用的校验方式:

校验方式 适用场景 FPGA资源消耗
奇偶校验 单bit错误检测 几乎为零
CRC16 CTP协议标准 约100个LUT
CRC32 ITCH协议标准 约200个LUT
双模冗余 关键字段保护 翻倍(但值得)

错误处理策略我一般分三级:

  1. 丢弃:单包校验失败,直接扔掉,不影响后续
  2. 重同步:连续3包校验失败,说明帧同步丢了,重新搜索帧头
  3. 告警:1秒内错误率超过1%,上报给上位机

注意:千万不要在错误处理里做"重传请求"。FPGA不是CPU,没有操作系统帮你管理重传。行情丢了就丢了,别想着补回来——你补回来的那一刻,行情已经过时了。

4.4 FIFO缓冲与跨时钟域同步

行情数据进来时,时钟域是乱的。网口过来的数据是125MHz,内部处理逻辑可能是200MHz,策略模块可能是300MHz。怎么把这些数据安全地搬来搬去?

我的方案是:每个时钟域入口放一个异步FIFO。

// 异步FIFO实例化(Xilinx原语)
async_fifo #(
    .FIFO_WIDTH(64),    // 数据位宽
    .FIFO_DEPTH(512),   // 深度512,够缓存几毫秒的行情
    .PROG_FULL_THRESH(480)  // 几乎满阈值
) u_fifo (
    .wr_clk(rx_clk),    // 写入时钟
    .rd_clk(sys_clk),   // 读出时钟
    .din(rx_data),
    .wr_en(rx_valid),
    .rd_en(rd_en),
    .dout(sys_data),
    .full(fifo_full),
    .empty(fifo_empty),
    .prog_full(almost_full)
);

跨时钟域同步有个铁律:单bit用两级触发器同步,多bit用FIFO或握手协议。千万别想着用组合逻辑直接跨时钟域——我见过有人这么干,结果仿真全对,上板就随机出错,查了整整一周。

FIFO深度怎么选?我有个经验公式:

  • 最小深度 = 写入速率 × 最大突发长度 / 读出速率
  • 实际深度 = 最小深度 × 2(留余量)
  • 再往上取2的幂(FPGA里2的幂深度效率最高)

举个例子:网口速率125MHz,每8个时钟来一个64bit数据包,内部处理速率200MHz,每4个时钟处理一个包。算下来最小深度是(125/8)/(200/4) = 0.3125,取整后实际用2的幂就是4。但我会选16——为什么?因为行情突发时可能连续来几十个包,深度太小就溢出了。

经验之谈:FIFO的"几乎满"信号一定要用。我习惯在FIFO用到80%时就通知上游降速,而不是等到满了再处理。满了就意味着丢数据,丢数据就意味着策略可能做出错误决策。

知识体系总览

下面这张图把行情解析模块的核心逻辑串起来了:

行情数据解析模块核心逻辑 原始二进制流 帧同步检测 协议解析 三段式状态机 CRC校验 校验失败,重同步 异步FIFO 跨时钟域同步 行情数据输出 错误处理策略 单包失败 → 丢弃 连续失败 → 重同步 高错误率 → 告警 错误反馈

这张图把整个流程串起来了:原始二进制流进来,先做帧同步,然后用状态机解析协议,接着做CRC校验。校验通过的数据进FIFO缓冲,跨时钟域后输出给策略模块。校验失败的话,要么丢弃单包,要么触发重同步。错误处理模块会监控整体错误率,必要时向上告警。

嗯,行情解析模块就讲到这里。记住一句话:解析模块是系统的"守门员",守好了,后面的策略才能安心踢球


公众号:蓝海资料掘金营,微信deep3321