2. 核心数据结构设计:订单簿的骨架与灵魂

做市商报价引擎,说白了就是个处理订单的机器。机器跑得快不快,稳不稳,全看数据结构设计得好不好。我见过不少团队,一上来就写业务逻辑,结果数据模型一塌糊涂,后面改得想哭。今天咱们就把这块地基打牢。

2.1 订单簿(OrderBook)的深度与广度

订单簿是什么?就是记录所有未成交订单的账本。它有两个核心维度:深度广度

  • 深度:指每个价格档位上的订单数量。比如买一价上有1000个ETH,买二价上有500个,这就是深度。
  • 广度:指价格档位的数量。从买一到买十,一共10个档位,这就是广度。

我个人习惯用红黑树来实现订单簿。为什么?因为订单簿的核心操作是插入、删除和查询最优价格,红黑树能做到O(log n)的复杂度。你想想看,如果每秒要处理几万笔订单,用链表肯定扛不住。

核心要点:订单簿的深度决定了市场流动性,广度决定了价格发现能力。做市商报价引擎必须同时支持这两者。

我在项目中遇到过一个问题:某个交易所的订单簿深度只有5档,结果大单一来,价格直接滑了3%。后来我们改成了20档,情况好多了。嗯,这里要注意,深度不是越深越好,太深了内存占用大,更新也慢。一般10-20档就够用了。

2.2 报价(Quote)与订单(Order)的字段定义

报价和订单,这两个概念容易混淆。我简单解释一下:

  • 报价(Quote):做市商主动挂出的买卖价格,是「承诺」。
  • 订单(Order):市场参与者提交的买卖请求,是「动作」。

它们的字段设计,我建议这样:

字段 报价(Quote) 订单(Order)
ID 唯一标识 唯一标识
方向 买/卖 买/卖
价格 报价价格 委托价格
数量 报价数量 委托数量
时间戳 报价生成时间 订单提交时间
状态 有效/撤销 待成交/部分成交/完全成交
类型 限价/市价 限价/市价/止损

你看,报价和订单的字段高度相似,但语义不同。报价是「我承诺以这个价格买卖」,订单是「我要以这个价格买卖」。做市商报价引擎里,报价是核心,订单是输入。

避坑指南:我曾经把报价和订单混用一个结构体,结果后来要区分「报价撤销」和「订单撤销」时,代码改得一团糟。建议从一开始就分开定义。

2.3 价格-数量模型

价格和数量,是订单簿里最核心的两个维度。它们之间的关系,我总结为三点:

  1. 价格优先:买单价格高的优先成交,卖单价格低的优先成交。
  2. 时间优先:同价格下,先来的订单优先成交。
  3. 数量累积:同一价格上的所有订单数量累加,形成深度。

代码实现上,我习惯用std::map来存储价格档位,每个档位里用std::list存储订单。这样既能快速找到最优价格,又能保证时间顺序。

// C++ 示例:价格-数量模型
struct PriceLevel {
    double price;
    int64_t total_quantity;  // 该价格上的总数量
    std::list<Order> orders; // 该价格上的订单列表
};

std::map<double, PriceLevel, std::greater<double>> bid_book;  // 买单簿,价格降序
std::map<double, PriceLevel, std::less<double>> ask_book;     // 卖单簿,价格升序

你想想看,为什么买单簿用降序,卖单簿用升序?因为买单的最高价在最前面,卖单的最低价在最前面,这样取最优价格就是取第一个元素,O(1)复杂度。

注意:价格用浮点数存储有精度问题。我建议用整数存储,比如把价格乘以10000,变成「基点」单位。这样既避免了浮点误差,又提高了比较效率。

2.4 时间戳与序列号设计

时间戳和序列号,是订单簿的「时间轴」。没有它们,订单的顺序就乱了。

时间戳:我建议用纳秒级的时间戳。为什么?因为微秒级在高频场景下已经不够用了。我记得有一次,两个订单在同一微秒内到达,结果谁先谁后根本分不清。后来改成纳秒级,问题就解决了。

序列号:时间戳虽然精确,但同一台机器上可能同时产生多个事件。这时候就需要序列号来保证全局有序。我习惯用原子递增的64位整数作为序列号。

// C++ 示例:时间戳与序列号
struct Timestamp {
    int64_t seconds;      // 秒
    int64_t nanoseconds;  // 纳秒
};

struct SequenceNumber {
    int64_t value;  // 原子递增
};

// 生成唯一标识
struct EventID {
    Timestamp ts;
    SequenceNumber seq;
};

这里有个小技巧:把时间戳和序列号合并成一个128位的ID,这样既能保证全局唯一,又能按时间排序。我在项目中就是这么做的,效果很好。

避坑指南:我曾经用系统时间作为序列号的一部分,结果发现系统时间回拨(比如NTP同步)会导致序列号重复。后来改用单调递增的计数器,彻底解决了这个问题。

2.5 核心数据结构关系图

下面这张图,展示了订单簿、报价、订单、时间戳和序列号之间的关系。你可以把它当作整个章节的「地图」。

订单簿核心数据结构关系图 订单簿 (OrderBook) 深度 (Depth) 广度 (Breadth) 报价 (Quote) 订单 (Order) 价格-数量模型 时间戳 序列号 包含 包含 生成 接收 依赖 记录 分配 核心思想:订单簿是容器,报价和订单是内容,时间戳和序列号是秩序 核心结构 维度属性 业务实体 核心模型 时间序列

这张图把整个章节的知识点串起来了。订单簿是核心容器,深度和广度是它的两个维度,报价和订单是它处理的对象,价格-数量模型是它的核心逻辑,时间戳和序列号是它的秩序保证。缺了任何一个,这个系统都跑不起来。

好了,这一章的内容就到这里。数据结构设计是报价引擎的根基,根基不牢,地动山摇。下一章咱们聊聊内存管理,看看怎么让订单簿跑得更快。


专注资料整理