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 价格-数量模型
价格和数量,是订单簿里最核心的两个维度。它们之间的关系,我总结为三点:
- 价格优先:买单价格高的优先成交,卖单价格低的优先成交。
- 时间优先:同价格下,先来的订单优先成交。
- 数量累积:同一价格上的所有订单数量累加,形成深度。
代码实现上,我习惯用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 核心数据结构关系图
下面这张图,展示了订单簿、报价、订单、时间戳和序列号之间的关系。你可以把它当作整个章节的「地图」。
这张图把整个章节的知识点串起来了。订单簿是核心容器,深度和广度是它的两个维度,报价和订单是它处理的对象,价格-数量模型是它的核心逻辑,时间戳和序列号是它的秩序保证。缺了任何一个,这个系统都跑不起来。
好了,这一章的内容就到这里。数据结构设计是报价引擎的根基,根基不牢,地动山摇。下一章咱们聊聊内存管理,看看怎么让订单簿跑得更快。