暗池撮合引擎数据模型:规则的数据化表达

大家好,我是你们的老朋友。今天咱们聊聊暗池撮合引擎里最核心的部分——数据模型。

说实话,我在做这个课程之前,翻了不少资料。发现很多讲撮合引擎的文章,要么太理论,要么太偏代码。真正把「规则怎么变成数据」讲清楚的,不多。

嗯,今天我就把我这些年踩过的坑、总结的经验,掰开揉碎了讲给你听。

撮合规则的数据化:不只是「价格优先」那么简单

先问一个问题:暗池的撮合规则,和交易所的有什么不同?

表面上看,都是价格优先、时间优先。但暗池多了一个维度——隐藏优先

什么意思?

在暗池里,有些订单是「隐藏单」,只有部分数量暴露在市场上。撮合的时候,系统要优先处理这些隐藏单。为什么?因为隐藏单的流动性提供者,承担了更大的信息不对称风险。

我当年在做一个暗池项目时,就遇到过这个问题。一开始我们直接把交易所的撮合逻辑搬过来,结果隐藏单的成交率特别低。后来才发现,规则没数据化到位。

价格优先的数据化

价格优先,说白了就是「谁的出价更优,谁先成交」。在数据模型里,我们用一个字段来表示:

// 订单价格优先级
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。

核心要点: 撮合事件不仅要记录「发生了什么」,还要记录「为什么发生」。这是数据模型设计的一个基本原则。

撮合引擎的核心流程

说了这么多理论,咱们来看看实际流程。下面这张图,是我在项目中常用的撮合引擎数据流:

暗池撮合引擎数据流 订单入口 规则引擎(价格/时间/隐藏) 撮合队列(OrderBook) 撮合事件(Match Event) 事件存储 接收订单 规则匹配 生成事件 持久化

这个流程看起来简单,但每个环节都有坑。我重点说说规则引擎这一步。

规则引擎的实现细节

规则引擎的核心,就是根据价格、时间、隐藏优先级,给每个订单算出一个「综合优先级分数」。分数越高,越先被撮合。

我常用的计算公式是:

// 综合优先级分数
double priority_score = 
    price_score * 0.6 +      // 价格权重60%
    time_score * 0.2 +       // 时间权重20%
    hidden_score * 0.2;      // 隐藏权重20%

这个权重比例,是我在项目中反复调优的结果。你可以根据业务需求调整。但有一点要注意:权重的总和必须是1.0,否则排序会出问题。

避坑指南: 我曾经把权重设成整数,结果排序时出现了大量并列。后来改成浮点数,并保留两位小数,问题就解决了。嗯,细节决定成败。

撮合事件的生命周期

一个撮合事件,从生成到存储,经历了三个阶段:

  1. 生成阶段: 规则引擎匹配成功,生成Match Event
  2. 验证阶段: 检查事件的有效性(价格是否合理、数量是否足够等)
  3. 存储阶段: 写入事件存储,同时更新订单状态

这里有个容易被忽略的点:验证阶段。我见过不少系统,生成事件后直接存储,不做验证。结果出现了一些「不可能」的成交记录,比如成交价格比市场价高出一大截。

为什么会这样?

因为规则引擎的输入数据可能有问题。比如,订单的价格字段被篡改了,或者时间戳出现了异常。验证阶段就是最后一道防线。

验证逻辑示例

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