一、订单簿结构:交易系统的核心数据结构
订单簿,说白了就是一张实时更新的买卖挂单表。我刚开始接触交易系统时,觉得这东西不就是个价格排序列表吗?后来才发现,它的设计直接决定了撮合引擎的性能天花板。
1.1 订单簿的基本组成
一个标准的订单簿包含两部分:
- 买单队列(Bid Queue):按价格从高到低排列
- 卖单队列(Ask Queue):按价格从低到高排列
每个价格档位上,可能挂着一堆订单。这些订单按到达时间排队,也就是所谓的「价格优先、时间优先」原则。
核心要点:订单簿的本质是一个双向优先队列。买单侧价格越高越优先,卖单侧价格越低越优先。
1.2 我在项目中踩过的坑
我曾经在一个FPGA加速项目中,直接用数组实现订单簿。结果呢?当订单量达到10万笔时,插入操作耗时飙升到微秒级。对于高频交易来说,这简直是灾难。
后来我改用跳表(Skip List)结构,插入和查询都稳定在纳秒级。你想想看,同样的数据量,性能差了三个数量级。
1.3 订单簿的硬件友好设计
FPGA实现订单簿,有几个关键点:
- 并行比较器:用硬件并行查找价格档位,比软件二分查找快得多
- 双端口RAM:同时支持读写操作,避免流水线停顿
- 流水线设计:把插入、删除、查询操作拆成多级流水
我的建议:刚开始做FPGA订单簿时,别追求全并行。先做一个简单的单周期操作版本,跑通了再优化。我见过太多人一上来就想做全流水,结果调试了三个月还没跑起来。
二、撮合引擎原理:从订单到成交的完整链路
撮合引擎,就是订单簿的消费者。它负责把买单和卖单匹配起来,生成成交记录。
2.1 撮合的基本流程
- 接收订单:从网络或API接口拿到新订单
- 价格检查:判断能否与对手方队列成交
- 数量匹配:按价格优先顺序逐笔撮合
- 生成成交:输出成交价格、数量、时间戳
- 更新订单簿:删除已成交订单,插入未成交部分
嗯,这里要注意:撮合引擎必须保证原子性。要么全部成交,要么全部不成交,不能出现部分成交后系统崩溃的情况。
2.2 连续竞价与集合竞价
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 连续竞价 | 订单实时匹配,立即成交 | 正常交易时段 |
| 集合竞价 | 收集一段时间订单,统一撮合 | 开盘、收盘、临时停牌 |
我记得有一次做期货交易所项目,客户要求同时支持两种模式。FPGA实现时,我用了状态机切换,连续竞价用流水线,集合竞价用批处理。两种模式共用一套订单簿存储,节省了不少资源。
2.3 撮合引擎的FPGA实现要点
避坑指南:我曾经在撮合引擎里用了全局复位信号,结果每次复位都会把订单簿清空。生产环境下一复位,所有挂单全没了,客户差点投诉到证监会。后来我改成只复位控制逻辑,数据存储用独立电源域。
FPGA撮合引擎的核心是状态机设计:
- IDLE:等待新订单
- PARSE:解析订单类型和参数
- MATCH:执行撮合逻辑
- UPDATE:更新订单簿
- OUTPUT:发送成交结果
每个状态控制在1-2个时钟周期内完成,这样才能达到纳秒级的处理延迟。
三、市场数据协议:交易系统的通信语言
市场数据协议,就是交易所和交易者之间的「方言」。不同的交易所用不同的协议,但核心内容都差不多。
3.1 常见的市场数据协议
- FIX协议:金融信息交换协议,文本格式,可读性好但带宽利用率低
- Binary协议:二进制格式,带宽利用率高,解析速度快
- ITCH协议:纳斯达克专用,逐笔数据,信息量大
- OUCH协议:订单管理协议,用于提交和撤销订单
我个人习惯用Binary协议做FPGA加速。为什么?因为FPGA擅长处理固定长度的二进制数据包,解析起来比文本协议快得多。
3.2 协议解析的硬件实现
FPGA解析市场数据协议,通常分三步:
- 帧同步:找到数据包的起始位置
- 头部解析:提取消息类型、序列号、时间戳
- 负载解析:根据消息类型解析具体字段
关键点:协议解析必须处理异常情况。比如数据包损坏、序列号跳跃、重复包等。我见过一个系统,因为没处理重复包,导致同一笔订单被撮合了两次,最后亏了几百万。
3.3 市场数据的压缩与解压缩
交易所每秒发送几百万条市场数据,带宽压力很大。常用的压缩方法有:
- 增量编码:只传输价格变化量,不传全量
- 字典压缩:用短编码代替长字符串
- 快照+增量:定期发送全量快照,中间只传增量
FPGA实现解压缩时,我建议用查找表(LUT)做字典解码,比用BRAM快一个时钟周期。别小看这一个周期,在高频交易里,1纳秒的延迟可能就是几百万的利润差距。
四、交易指令类型:订单的「身份证」
交易指令类型,决定了订单怎么被处理。不同的指令类型,在FPGA里对应不同的处理逻辑。
4.1 基础指令类型
| 指令类型 | 说明 | FPGA处理特点 |
|---|---|---|
| 市价单 | 立即以当前最优价格成交 | 直接匹配对手方最优档位 |
| 限价单 | 指定价格,不达到不成交 | 需要插入订单簿对应档位 |
| 止损单 | 价格触发后转为市价单 | 需要监控价格变化 |
| 冰山单 | 只显示部分数量,隐藏真实量 | 需要维护隐藏队列 |
4.2 高级指令类型
除了基础指令,还有几种高级类型:
- FOK(Fill or Kill):要么全部成交,要么全部撤销
- IOC(Immediate or Cancel):立即成交剩余部分,未成交的撤销
- GTD(Good Till Date):指定有效期,过期自动撤销
我曾经在FPGA里实现FOK指令时,遇到一个难题:如何判断「能否全部成交」?如果按顺序逐笔检查,延迟太大。后来我用了并行求和器,同时计算所有对手方档位的总数量,一个时钟周期就出结果。
4.3 指令类型的FPGA编码
在FPGA里,指令类型通常用枚举编码:
// 指令类型编码
typedef enum logic [3:0] {
MARKET = 4'b0001,
LIMIT = 4'b0010,
STOP = 4'b0100,
ICEBERG = 4'b1000
} order_type_t;
// 指令标志位
typedef struct packed {
logic is_fok;
logic is_ioc;
logic is_gtd;
logic [31:0] expire_time;
} order_flags_t;
嗯,这里要注意:编码要留足余量。我见过有人用3位编码,结果后来交易所新增了两种指令类型,整个FPGA代码都得重写。建议至少用4位,预留扩展空间。
我的经验:指令类型的FPGA实现,核心是「分类处理」。用一个case语句区分指令类型,每种类型对应一个处理模块。这样代码清晰,也方便后续增加新类型。
知识体系总览
下面这张图,展示了本章四个核心概念之间的关系:
这张图你看懂了吗?订单簿是存储层,撮合引擎是处理层,指令类型定义了处理规则,市场数据协议负责对外通信。四者缺一不可。