暗池撮合引擎数据模型:规则的数据化表达
大家好,我是你们的老朋友。今天咱们聊聊暗池撮合引擎里最核心的部分——数据模型。
说实话,我在做这个课程之前,翻了不少资料。发现很多讲撮合引擎的文章,要么太理论,要么太偏代码。真正把「规则怎么变成数据」讲清楚的,不多。
嗯,今天我就把我这些年踩过的坑、总结的经验,掰开揉碎了讲给你听。
撮合规则的数据化:不只是「价格优先」那么简单
先问一个问题:暗池的撮合规则,和交易所的有什么不同?
表面上看,都是价格优先、时间优先。但暗池多了一个维度——隐藏优先。
什么意思?
在暗池里,有些订单是「隐藏单」,只有部分数量暴露在市场上。撮合的时候,系统要优先处理这些隐藏单。为什么?因为隐藏单的流动性提供者,承担了更大的信息不对称风险。
我当年在做一个暗池项目时,就遇到过这个问题。一开始我们直接把交易所的撮合逻辑搬过来,结果隐藏单的成交率特别低。后来才发现,规则没数据化到位。
价格优先的数据化
价格优先,说白了就是「谁的出价更优,谁先成交」。在数据模型里,我们用一个字段来表示:
// 订单价格优先级
enum PricePriority {
BUY_HIGHEST_FIRST, // 买单:价高者优先
SELL_LOWEST_FIRST // 卖单:价低者优先
}
但这里有个坑:暗池里经常有「冰山订单」,只显示一部分数量。价格优先的规则,对冰山订单的隐藏部分同样适用。也就是说,隐藏部分的价格,也要参与排序。
注意: 我曾经见过一个系统,只对可见部分做价格排序,隐藏部分直接按时间排。结果导致隐藏单的成交价格严重偏离市场。这是个典型的「规则数据化不完整」的坑。
时间优先的数据化
时间优先,就是「先来后到」。在数据模型里,我们用一个时间戳字段:
// 订单到达时间
uint64_t arrival_time; // 纳秒级时间戳
你可能会问:为什么用纳秒?
因为在高频交易场景下,微秒级的差异就能决定谁先成交。我建议用系统时钟的纳秒时间,而不是网络时间。网络时间有延迟,不靠谱。
隐藏优先的数据化
这是暗池独有的规则。隐藏优先,意味着隐藏单在同等价格下,比普通单有更高的优先级。
数据化表示很简单:
// 订单可见性
enum Visibility {
VISIBLE, // 可见单
HIDDEN, // 完全隐藏单
ICEBERG // 冰山订单(部分可见)
}
// 隐藏优先级
uint8_t hidden_priority; // 0-255,值越大优先级越高
这里有个细节:隐藏优先级不是固定的。我习惯根据订单的隐藏比例动态计算。比如,隐藏比例越高,优先级越高。这样能激励更多流动性提供者使用隐藏单。
我的经验: 隐藏优先级的计算,最好放在订单进入撮合队列时完成,而不是每次撮合时实时计算。这样可以减少撮合引擎的计算压力。
撮合事件(Match Event)的数据结构设计
撮合事件,就是一笔成交的记录。它包含了这笔成交的所有信息。
我见过很多系统,撮合事件设计得特别简单,就几个字段。结果事后做审计、做风控的时候,发现信息不够用,还得去翻原始订单。这其实是个设计缺陷。
一个好的撮合事件,应该包含以下信息:
| 字段 | 类型 | 说明 |
|---|---|---|
| event_id | uint64_t | 全局唯一事件ID |
| match_time | uint64_t | 撮合时间(纳秒) |
| buy_order_id | uint64_t | 买单ID |
| sell_order_id | uint64_t | 卖单ID |
| match_price | double | 成交价格 |
| match_quantity | uint64_t | 成交数量 |
| buy_remaining | uint64_t | 买单剩余数量 |
| sell_remaining | uint64_t | 卖单剩余数量 |
| match_type | enum | 撮合类型(完全成交/部分成交) |
| priority_info | struct | 优先级信息(价格、时间、隐藏) |
你可能会问:为什么要把「剩余数量」也放进去?
因为这样可以避免事后去查订单状态。我当年做一个风控系统时,就因为没有记录剩余数量,导致风控逻辑需要频繁查询订单表,性能很差。后来加上这个字段,查询量直接降了80%。
优先级信息的数据结构
优先级信息是撮合事件里比较特殊的一个字段。它记录了这笔成交的优先级判定过程:
struct PriorityInfo {
double buy_price; // 买单价格
double sell_price; // 卖单价格
uint64_t buy_arrival_time; // 买单到达时间
uint64_t sell_arrival_time; // 卖单到达时间
uint8_t buy_hidden_priority; // 买单隐藏优先级
uint8_t sell_hidden_priority; // 卖单隐藏优先级
uint8_t final_priority; // 最终优先级(用于排序)
}
这个结构有什么用?
举个例子。假设有两个买单,价格相同,时间也相同。但一个是隐藏单,一个是可见单。按照规则,隐藏单优先。如果没有记录优先级信息,事后审计时根本不知道这笔成交为什么是A而不是B。
核心要点: 撮合事件不仅要记录「发生了什么」,还要记录「为什么发生」。这是数据模型设计的一个基本原则。
撮合引擎的核心流程
说了这么多理论,咱们来看看实际流程。下面这张图,是我在项目中常用的撮合引擎数据流:
这个流程看起来简单,但每个环节都有坑。我重点说说规则引擎这一步。
规则引擎的实现细节
规则引擎的核心,就是根据价格、时间、隐藏优先级,给每个订单算出一个「综合优先级分数」。分数越高,越先被撮合。
我常用的计算公式是:
// 综合优先级分数
double priority_score =
price_score * 0.6 + // 价格权重60%
time_score * 0.2 + // 时间权重20%
hidden_score * 0.2; // 隐藏权重20%
这个权重比例,是我在项目中反复调优的结果。你可以根据业务需求调整。但有一点要注意:权重的总和必须是1.0,否则排序会出问题。
避坑指南: 我曾经把权重设成整数,结果排序时出现了大量并列。后来改成浮点数,并保留两位小数,问题就解决了。嗯,细节决定成败。
撮合事件的生命周期
一个撮合事件,从生成到存储,经历了三个阶段:
- 生成阶段: 规则引擎匹配成功,生成Match Event
- 验证阶段: 检查事件的有效性(价格是否合理、数量是否足够等)
- 存储阶段: 写入事件存储,同时更新订单状态
这里有个容易被忽略的点:验证阶段。我见过不少系统,生成事件后直接存储,不做验证。结果出现了一些「不可能」的成交记录,比如成交价格比市场价高出一大截。
为什么会这样?
因为规则引擎的输入数据可能有问题。比如,订单的价格字段被篡改了,或者时间戳出现了异常。验证阶段就是最后一道防线。
验证逻辑示例
bool validate_match_event(const MatchEvent& event) {
// 检查价格是否在合理范围内
if (event.match_price <= 0 || event.match_price > 1000000) {
return false;
}
// 检查数量是否为正数
if (event.match_quantity <= 0) {
return false;
}
// 检查剩余数量是否合理
if (event.buy_remaining < 0 || event.sell_remaining < 0) {
return false;
}
return true;
}
这个验证逻辑看起来简单,但能挡住90%的异常事件。剩下的10%,需要更复杂的风控逻辑来处理。
存储设计的一点建议
最后说说存储。撮合事件的存储,我建议用时序数据库,比如InfluxDB或TimescaleDB。为什么?
- 撮合事件是典型的时序数据,按时间顺序写入
- 查询场景通常是按时间范围查询
- 时序数据库的写入性能远高于关系型数据库
当然,如果你对事务一致性要求很高,也可以用关系型数据库。但要做好分表分库的准备,因为撮合事件的数据量增长很快。
注意: 不要把所有撮合事件放在一张表里。我建议按天分表,比如match_events_20240101。这样查询和清理都方便。
好了,这一章的内容就到这里。暗池撮合引擎的数据模型,说白了就是「规则数据化」和「事件结构化」两个核心。把这两个点想清楚,剩下的就是细节了。
下一章,我们会聊聊订单簿(OrderBook)的数据结构设计。到时候见。
公众号:蓝海资料掘金营,微信deep3321