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 |
| 双模冗余 | 关键字段保护 | 翻倍(但值得) |
错误处理策略我一般分三级:
- 丢弃:单包校验失败,直接扔掉,不影响后续
- 重同步:连续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缓冲,跨时钟域后输出给策略模块。校验失败的话,要么丢弃单包,要么触发重同步。错误处理模块会监控整体错误率,必要时向上告警。
嗯,行情解析模块就讲到这里。记住一句话:解析模块是系统的"守门员",守好了,后面的策略才能安心踢球。
公众号:蓝海资料掘金营,微信deep3321