第四章:市场数据解析——行情数据的“翻译官”

做量化交易的,都知道一句话:行情是命,数据是血。

但说实话,我刚入行那会儿,真没把行情解析当回事。觉得不就是网络上来个包,拆开看看价格嘛。直到有一次,我负责的FPGA加速卡在实盘环境中丢了一个tick,导致套利策略开了反向仓……嗯,那天晚上我对着逻辑分析仪坐了四个小时。

从那以后,我养成了一个习惯:永远不要假设行情数据是干净的

这一章,咱们就聊聊行情数据解析那些事儿。说白了,就是怎么把交易所发来的“天书”,变成FPGA能理解的“白话”。


4.1 行情数据格式:ITCH / OUCH / FIX

目前主流的交易所,用的协议就那么几种。我挑三个最常见的讲讲。

4.1.1 ITCH——最“啰嗦”的协议

ITCH是纳斯达克搞的。它有个特点:每个消息都告诉你“发生了什么”

比如一个订单进来,ITCH会发一条“添加订单”消息,告诉你价格、数量、订单编号。订单被成交了,再发一条“执行交易”消息。订单被撤单了,再发一条“删除订单”消息。

你想想看,这多好理解?但代价就是——数据量巨大。我测过,在高峰时段,ITCH的带宽能占到总流量的60%以上。

ITCH的消息格式很固定。每条消息的第一个字节是消息类型,后面跟着固定长度的字段。举个例子:

// ITCH 5.0 的 "Add Order" 消息 (Type 'A')
Byte 0:    消息类型 (0x41 = 'A')
Byte 1-2:  时间戳 (纳秒,低16位)
Byte 3-6:  订单编号 (uint32)
Byte 7-8:  买卖方向 (0x42='B' 买, 0x53='S' 卖)
Byte 9-12: 价格 (uint32, 价格 = 原始值 / 10000)
Byte 13-14: 数量 (uint16)
Byte 15-17: 股票代码索引 (uint24)

我在项目中遇到过一个问题:ITCH的某些字段是“可选”的,比如“交叉交易”消息里的“交叉类型”。如果你按固定偏移去解析,很容易读错。我的建议是:每个消息类型单独写一个解析状态机,别偷懒用统一模板。

4.1.2 OUCH——给“下单”用的协议

OUCH和ITCH是“兄弟协议”。ITCH用来收行情,OUCH用来下单。

OUCH的消息更短。因为它不需要告诉你市场状态,只需要告诉交易所“我要买/卖多少”。

典型的OUCH下单消息长这样:

// OUCH 4.0 的 "Enter Order" 消息
Byte 0:    消息类型 (0x4F = 'O')
Byte 1-4:  订单编号 (uint32)
Byte 5:    买卖方向 (0x42='B', 0x53='S')
Byte 6-9:  价格 (uint32)
Byte 10-11: 数量 (uint16)
Byte 12-13: 股票代码索引 (uint16)
Byte 14:   订单类型 (0x4C='L' 限价, 0x4D='M' 市价)

OUCH有个坑:它的“订单编号”是客户端自己生成的。如果你在FPGA里生成编号,一定要保证不重复。我曾经见过一个团队,因为FPGA复位后编号计数器没清零,导致生成了重复订单……交易所直接拒单,策略停了整整一个交易日。

4.1.3 FIX——金融界的“通用语”

FIX协议就更有意思了。它是个文本协议,用“标签=值”的方式组织数据。

比如一条FIX行情消息可能是这样的:

8=FIX.4.2|9=78|35=W|49=EXCHANGE|56=CLIENT|55=AAPL|44=150.25|38=1000|10=123|

你看,每个字段前面都有个数字标签。35=W表示这是“市场数据快照”消息,55=AAPL表示股票代码,44=150.25表示价格。

FIX的好处是灵活,坏处也是灵活——解析起来太慢了。在FPGA里解析FIX,我个人的经验是:先做“标签定位”,再做“值提取”。别想着一次性解析完所有字段,那会浪费大量LUT资源。

我的建议: 如果条件允许,优先用ITCH/OUCH。FIX虽然通用,但在FPGA里解析效率低。我见过一个团队,为了兼容FIX,把FPGA的时钟频率从200MHz降到了150MHz——得不偿失。

4.2 硬件解析引擎设计

好了,协议看完了。接下来聊聊怎么在FPGA里“翻译”这些协议。

硬件解析引擎,说白了就是一个状态机+数据通路的组合。我一般把它分成三级流水线:

4.2.1 第一级:帧同步

这一级负责找到消息的“边界”。

对于ITCH来说,每条消息前面有个长度字段。对于FIX来说,每条消息以“8=FIX”开头,以“10=xxx”结尾。

帧同步的逻辑很简单:

  • 检测起始标志
  • 读取长度字段
  • 计数到长度值,确认结束

但要注意:网络包可能被拆分成多个TCP段。我在项目中遇到过,一个ITCH消息被拆成了两个TCP包。如果帧同步逻辑没处理好,就会解析出“半条消息”。

我的做法是:在帧同步模块里加一个“重组缓冲区”。收到不完整的消息时,先存着,等后续数据来了再拼起来。

4.2.2 第二级:字段提取

这一级负责从消息里“抠”出各个字段。

对于固定长度的协议(ITCH/OUCH),这步很简单——按偏移量读就行。对于变长协议(FIX),就需要“标签匹配”了。

我常用的方法是:并行比较器阵列。比如,我需要提取“价格”字段(标签44),就同时比较8个字节,看哪个字节是“44=”。找到了,就从这个位置往后读。

代码示例(Verilog风格):

// 并行标签匹配示例
always @(posedge clk) begin
  for (i = 0; i < 8; i = i + 1) begin
    if (data_in[i*8 +: 8] == "4" && 
        data_in[i*8+8 +: 8] == "4" &&
        data_in[i*8+16 +: 8] == "=") begin
      tag_found <= 1;
      tag_position <= i;
    end
  end
end

嗯,这里要注意:并行比较器很耗资源。如果你要提取10个字段,每个字段用8路并行比较,那就是80个比较器。所以,我一般只提取“关键字段”——价格、数量、买卖方向。其他字段(比如“下单员ID”)直接透传,不做解析。

4.2.3 第三级:协议转换

这一级把解析出来的字段,转成内部统一的“总线格式”。

我习惯用这样的内部格式:

// 内部行情总线格式
typedef struct packed {
  logic [31:0] price;      // 价格,定点数
  logic [31:0] quantity;   // 数量
  logic [1:0]  side;       // 买卖方向:00=买,01=卖
  logic [15:0] symbol_id;  // 股票代码索引
  logic [63:0] timestamp;  // 时间戳,纳秒
  logic [7:0]  msg_type;   // 消息类型
} market_data_t;

这样,不管底层是ITCH、OUCH还是FIX,到了FPGA内部,都是统一的格式。后面的策略模块、风控模块,就不用关心“数据是从哪个交易所来的”了。

核心思想: 硬件解析引擎的“三化”——帧同步化、字段提取化、协议统一化。做到这三点,你的行情解析模块就能“以不变应万变”。

4.3 数据包分类与过滤

行情数据来了,不是每条都要处理。比如,你只交易AAPL和MSFT,那其他股票的消息就可以直接扔掉。

数据包分类与过滤,就是在FPGA里做“快速筛选”。

4.3.1 分类:按股票代码分桶

我常用的方法是哈希表+桶

具体做法:

  1. 维护一个“关注股票列表”,比如256只股票
  2. 对每只股票,分配一个“桶编号”(0~255)
  3. 收到行情消息后,提取股票代码,查哈希表,得到桶编号
  4. 如果桶编号有效,就把消息发到对应的处理通道

哈希表可以用BRAM实现。我一般用直接映射的方式:股票代码的低8位作为桶编号。这样查表只需要一个时钟周期。

但要注意:哈希冲突。如果两只股票的代码低8位相同,就会冲突。我的做法是:在软件初始化时,检查所有关注的股票,确保没有冲突。如果有,就调整哈希函数。

4.3.2 过滤:按消息类型筛选

不是所有消息类型都需要处理。比如,ITCH里的“系统事件”消息(类型'S'),对交易策略来说就没用。

过滤逻辑很简单:

  • 维护一个“允许的消息类型”位图
  • 收到消息后,用消息类型作为索引,查位图
  • 如果位图对应位为1,放行;否则丢弃

这个位图可以用一个32位的寄存器实现。ITCH有20多种消息类型,32位足够了。

避坑指南: 我曾经在过滤逻辑里犯过一个错——把“成交确认”消息(类型'E')给过滤掉了。结果策略以为订单没成交,又下了一笔……嗯,那天我写了一个下午的复盘报告。所以,过滤逻辑一定要和策略团队反复确认,别自己拍脑袋。

4.4 多路行情合并

做高频交易的,通常不会只连一个交易所。比如,你可能同时连了纳斯达克和纽交所。这两家的行情数据,需要合并成一路“统一行情”。

多路行情合并,核心要解决两个问题:时序对齐冲突处理

4.4.1 时序对齐

不同交易所的行情数据,到达FPGA的时间不一样。可能纳斯达克的AAPL行情先到,纽交所的AAPL行情后到。但它们的“时间戳”是交易所给的,不是FPGA收到的时刻。

所以,合并时不能按“到达时间”排序,要按“交易所时间戳”排序。

我的做法是:每个交易所维护一个FIFO。收到行情消息后,先存到对应的FIFO里。然后,比较所有FIFO的“队首消息”的时间戳,把时间戳最小的那条发出去。

这个比较逻辑,可以用一个多路比较器实现。比如,有4路行情,就同时比较4个时间戳,选出最小的。

4.4.2 冲突处理

如果两个交易所同时报出同一只股票的价格,怎么办?

这取决于你的策略。有的策略用“最优价”(取最高买价、最低卖价),有的策略用“加权平均价”。

我一般这样处理:

  • 对于“买一价”,取所有交易所中的最大值
  • 对于“卖一价”,取所有交易所中的最小值
  • 对于“成交量”,直接累加

这样处理的好处是:保守。你永远不会因为合并逻辑而“看到”一个比实际更好的价格。

一个小技巧: 多路合并时,可以给每个交易所加一个“优先级”。比如,纳斯达克的行情优先级高,纽交所的优先级低。当两个交易所的时间戳完全相同时,优先发纳斯达克的消息。这样可以避免“死锁”——两个FIFO的队首时间戳相同,互相等待。

4.5 本章知识体系总览

说了这么多,我画了一张图,帮你把这一章的知识点串起来。

市场数据解析——知识体系总览 行情数据输入 ITCH / OUCH / FIX 协议 硬件解析引擎(三级流水线) 帧同步 → 字段提取 → 协议转换 数据包分类与过滤 哈希表分桶(按股票代码) | 位图过滤(按消息类型) 多路行情合并 时序对齐(按交易所时间戳) | 冲突处理(最优价/加权价)

这张图展示了行情数据从“输入”到“输出”的完整流程。你想想看,每一级都做一点“减法”——解析引擎把协议转成统一格式,分类过滤把无关数据扔掉,多路合并把多路变一路。最终,FPGA内部看到的是一条干净、统一、实时的行情流。


好了,这一章的内容就到这儿。行情解析是硬件加速交易系统的“地基”。地基没打好,上面的策略再牛也白搭。下一章,咱们聊聊订单管理——怎么把“交易信号”变成“交易所能理解的订单”。

专注资料整理