第三章:订单管理模块设计

订单管理模块,说白了就是做市商系统的心脏。你想想看,所有策略的决策最终都要落地成订单,而订单怎么来、怎么走、怎么成交,全看这个模块的设计。我个人习惯把订单管理拆成四个核心部分:生命周期、数据结构、撮合引擎、路由策略。咱们一个一个聊。

3.1 订单生命周期:从生到死的完整轨迹

一个订单从创建到最终消亡,中间会经历好几个状态。我在项目中遇到过不少新手,上来就搞个「已提交」「已成交」「已撤销」三个状态完事——结果上线第一天就出问题。

为什么?因为真实场景里,订单可能部分成交、可能被拒绝、可能因风控被冻结。你想想看,状态机设计不严谨,资金对账就会乱套。

我建议至少定义以下状态:

状态 含义 触发条件
Pending 待处理 订单刚创建,尚未进入撮合队列
Open 已挂单 订单进入订单簿,等待撮合
PartiallyFilled 部分成交 撮合引擎匹配了部分数量
Filled 完全成交 剩余数量为0
Cancelled 已撤销 用户主动撤单或超时
Rejected 已拒绝 风控校验不通过或参数非法
注意: 状态转换必须是单向的。比如一个订单从 Open 变成 PartiallyFilled 后,不能再回到 Open。我曾经见过一个系统因为状态回退导致重复成交,差点造成百万级亏损。

3.2 订单簿数据结构:价格优先、时间优先

订单簿的核心就八个字:价格优先,时间优先。说白了,买单出价高的排前面,卖单出价低的排前面,价格一样就看谁先来。

数据结构怎么选?我推荐用跳表(Skip List)或者红黑树。为什么不用数组?因为订单簿需要频繁插入和删除,数组的 O(n) 复杂度扛不住高频场景。

下面是我常用的订单簿结构定义:

// 价格档位节点
struct PriceLevel {
    double price;           // 价格
    uint64_t totalQty;      // 该价格总挂单量
    std::list<Order> orders; // 同价格下的订单队列(FIFO)
};

// 订单簿
class OrderBook {
    std::map<double, PriceLevel, std::greater<>> bids; // 买单,价格降序
    std::map<double, PriceLevel, std::less<>> asks;    // 卖单,价格升序
    std::unordered_map<uint64_t, Order> orderMap;       // 订单ID快速查找
};
经验之谈: 用 map 而不是 unordered_map 来存价格档位,因为我们需要按顺序遍历。订单ID的查找才用哈希表,O(1) 速度。

3.3 撮合引擎核心算法:锁与无锁之争

撮合引擎是性能瓶颈的重灾区。我见过两种主流方案:

  1. 单线程事件循环:简单可靠,没有锁竞争。适合大部分场景。
  2. 无锁队列 + 多线程:性能极致,但调试起来想哭。我建议非万不得已别碰。

核心算法其实不复杂:

void match(Order& takerOrder) {
    while (takerOrder.remainingQty > 0) {
        // 找到对手方最优价格
        PriceLevel* bestLevel = (takerOrder.side == BUY) 
            ? getBestAsk() : getBestBid();
        
        if (!bestLevel || !isPriceMatch(takerOrder, bestLevel->price)) {
            break; // 没有可撮合的订单
        }

        // 按FIFO顺序撮合
        auto& orderList = bestLevel->orders;
        while (!orderList.empty() && takerOrder.remainingQty > 0) {
            Order& makerOrder = orderList.front();
            uint64_t tradeQty = std::min(takerOrder.remainingQty, 
                                         makerOrder.remainingQty);
            // 生成成交记录
            generateTrade(takerOrder, makerOrder, tradeQty);
            // 更新数量
            takerOrder.remainingQty -= tradeQty;
            makerOrder.remainingQty -= tradeQty;
            if (makerOrder.remainingQty == 0) {
                orderList.pop_front();
            }
        }
        // 如果价格档位空了,清理掉
        if (orderList.empty()) {
            removePriceLevel(bestLevel);
        }
    }
}
核心要点: 撮合时一定要先检查价格是否匹配。比如买单价格必须 ≥ 卖单价格,否则直接退出循环。这个判断看似简单,但我在代码审查时发现过有人写反了符号,导致系统一直撮合不存在的订单。

3.4 订单路由策略:把订单送到该去的地方

做市商通常会在多个交易所同时挂单。订单路由就是决定「这个订单该发到哪个交易所」。我常用的策略有三种:

策略 适用场景 优缺点
Smart Order Routing (SOR) 流动性分散的场景 自动选择最优价格,但延迟较高
固定路由 主交易所流动性充足 延迟最低,但灵活性差
比例路由 需要分散风险 按比例分配,但可能错过最优价格

我个人习惯用 SOR 加一层缓存。为什么?因为每次路由都去查各交易所的实时行情,延迟太高。我会在本地维护一个「价格快照」,每 10 毫秒更新一次。路由时直接读快照,速度能快一个数量级。

避坑指南: 我曾经因为快照更新不及时,导致订单发到了已经涨价的交易所,白白损失了几千块。后来我加了一个「价格偏差阈值」——如果快照价格和实际价格偏差超过 0.1%,就强制重新查询。

3.5 整体架构图

下面这张图展示了订单管理模块的核心流程。从订单创建到最终成交或撤销,每一步都有对应的处理逻辑。

订单管理模块核心流程 订单创建 风控校验 订单拒绝 订单簿挂单 撮合引擎 成交 / 部分成交 / 未成交

嗯,这张图把整个流程串起来了。你想想看,从订单创建开始,先过风控——这是第一道防线。过了风控才能进订单簿,然后撮合引擎开始工作。如果风控没通过,直接拒绝,不给任何机会。

个人建议: 风控校验一定要放在订单簿之前。我见过有人把风控放在撮合之后,结果订单都成交了才发现资金不足——那场面,简直灾难。

好了,订单管理模块的核心内容就这些。记住,数据结构选对、状态机设计严谨、撮合算法高效、路由策略灵活,这四个点做好了,你的订单模块就稳了。

专注资料整理