4、撮合核心算法:价格优先时间优先原则、FIFO队列原理、撮合匹配逻辑
好,咱们今天聊点硬核的。撮合系统的心脏,说白了就是那套匹配逻辑。你想想看,交易所每秒要处理成千上万笔订单,凭什么给张三成交,不给李四?凭什么张三的订单排前面?这里头就是「价格优先、时间优先」这八个字在起作用。
我个人习惯把撮合算法拆成三块来看:排队规则、队列结构、匹配引擎。这三块搞明白了,整个撮合系统的骨架就搭起来了。
4.1 价格优先、时间优先原则
这个原则,说白了就是两条铁律:
- 价格优先:买单出价高的排前面,卖单出价低的排前面。
- 时间优先:价格一样的情况下,谁先来谁先成交。
嗯,听起来很简单对吧?但我在项目中遇到过一个问题:当价格相同的时候,时间戳的精度怎么定?微秒级够不够?纳秒级会不会太浪费资源?
我建议你直接用硬件时间戳,精度至少到微秒。为什么?因为软件时间戳在极端情况下会有抖动,而FPGA的时钟计数器是确定性的。你想想看,如果两个订单在同一个时钟周期到达,时间戳一样怎么办?这时候就得靠「端口优先级」或者「FIFO入队顺序」来二次裁决了。
核心要点:
- 价格优先是「硬约束」,不可违反
- 时间优先是「软约束」,在价格相同时生效
- 时间戳精度决定了系统的公平性下限
4.2 FIFO队列原理
FIFO(First In First Out),先进先出。这玩意儿在FPGA里太常见了,但用在撮合系统里,有几个坑你得注意。
先看一个最简单的FIFO实现:
module fifo_simple #(
parameter DEPTH = 256,
parameter WIDTH = 64
)(
input clk,
input rst_n,
input wr_en,
input [WIDTH-1:0] wr_data,
output reg [WIDTH-1:0] rd_data,
input rd_en,
output reg full,
output reg empty
);
reg [WIDTH-1:0] mem [0:DEPTH-1];
reg [7:0] wr_ptr, rd_ptr;
reg [8:0] count; // 多一位用于判断满/空
// 写操作
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr_ptr <= 0;
count <= 0;
end else if (wr_en && !full) begin
mem[wr_ptr] <= wr_data;
wr_ptr <= wr_ptr + 1;
count <= count + 1;
end
end
// 读操作
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rd_ptr <= 0;
end else if (rd_en && !empty) begin
rd_data <= mem[rd_ptr];
rd_ptr <= rd_ptr + 1;
count <= count - 1;
end
end
// 满/空标志
assign full = (count == DEPTH);
assign empty = (count == 0);
endmodule
这个代码看着简单,但有个问题:读和写不能同时操作同一个地址。我曾经在调试一个撮合系统时,发现订单莫名其妙丢失了,查了两天才发现是FIFO的读写冲突导致的。后来我改用双端口RAM + 独立读写指针,才彻底解决。
避坑指南:
我曾经在FIFO深度设置上吃过亏。深度设小了,订单高峰期直接溢出;设大了,资源浪费。我的经验是:深度取最大并发订单数的1.5倍,再留20%余量。
4.3 撮合匹配逻辑
匹配逻辑是撮合系统的「大脑」。它要干的事很简单:把买单和卖单对上号。但实现起来,细节多得吓人。
咱们先画个流程图,看看整个匹配过程长什么样:
这个流程看着简单,但实际实现时,有几个关键点:
- 价格比较器:需要同时比较买单的最高价和卖单的最低价。我建议用并行比较器,一个时钟周期出结果。
- 数量匹配:价格匹配了,数量不一定匹配。这时候要处理「部分成交」的情况。
- 队列更新:成交后,队列头部的订单数量要扣减,如果数量归零则出队。
注意:部分成交的订单,剩余数量要重新插入队列。但插入位置不是队尾,而是根据价格优先原则重新排序。这个逻辑在FPGA里实现起来有点绕,我建议用「插入排序」的思想,每来一个新订单就扫描一次队列。
4.4 实战中的性能考量
好了,理论讲完了,咱们聊聊实际项目中怎么优化性能。
| 优化项 | 传统做法 | FPGA优化做法 | 性能提升 |
|---|---|---|---|
| 价格比较 | 串行比较 | 并行比较器树 | 5x |
| 队列查找 | 遍历查找 | 内容寻址存储器(CAM) | 10x |
| 匹配逻辑 | 状态机串行处理 | 流水线并行处理 | 8x |
| 时间戳生成 | 软件打戳 | 硬件计数器 | 100x |
嗯,看到这个表你可能会问:CAM是什么?说白了就是「按内容查找」的存储器。你给它一个价格,它直接告诉你这个价格在队列的哪个位置。我在一个高频交易项目里用过,查找延迟从10个时钟周期降到了1个时钟周期。
我的经验:如果你用的是Xilinx的FPGA,可以用BRAM搭一个简单的CAM。如果是Altera的,可以用MLAB。别自己用LUT去搭,资源消耗太大,划不来。
4.5 一个完整的匹配状态机
最后,咱们看一个简单的匹配状态机代码。这个代码只处理「价格匹配」和「数量匹配」两个核心逻辑,实际项目里还要加上超时、撤单等处理。
module match_fsm (
input clk,
input rst_n,
input [31:0] buy_price,
input [31:0] sell_price,
input [31:0] buy_qty,
input [31:0] sell_qty,
output reg match_done,
output reg [31:0] match_price,
output reg [31:0] match_qty
);
typedef enum {IDLE, COMPARE, EXECUTE, DONE} state_t;
state_t state, next_state;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
state <= IDLE;
else
state <= next_state;
end
always @(*) begin
next_state = state;
case (state)
IDLE: begin
if (buy_price >= sell_price)
next_state = COMPARE;
end
COMPARE: begin
// 价格匹配,比较数量
next_state = EXECUTE;
end
EXECUTE: begin
// 取较小数量成交
match_qty = (buy_qty < sell_qty) ? buy_qty : sell_qty;
match_price = (buy_price + sell_price) >> 1; // 取中间价
next_state = DONE;
end
DONE: begin
match_done = 1;
next_state = IDLE;
end
endcase
end
endmodule
这个状态机看着简单,但实际项目中,COMPARE状态要处理的事情很多:检查是否有未成交的挂单、判断是否触发熔断、记录成交日志等等。我建议你把COMPARE状态拆成多个子状态,用流水线的方式处理,这样吞吐量能翻倍。
好了,关于撮合核心算法,今天就聊这么多。记住一句话:价格优先是铁律,时间优先是规矩,FIFO是基础,匹配逻辑是灵魂。把这四样东西吃透了,你就能设计出一个高性能的撮合系统。
公众号:蓝海资料掘金营,微信deep3321