暗池交易核心数据结构:订单簿模型与冰山订单
各位同学,今天我们来聊聊暗池交易里最核心的数据结构——订单簿模型。说实话,这个模型在传统交易所里已经非常成熟了,但到了暗池环境里,情况就变得微妙起来。
我个人习惯把订单簿比作一个「多层停车场」。每一层代表一个价格,停着的车就是订单。但暗池里的停车场,有些车是「隐形」的——你只能看到一部分,甚至完全看不到。这就是我们今天要重点讲的冰山订单。
一、订单簿模型的基本结构
先看最基础的订单簿。我在项目中遇到过不少团队,上来就搞复杂的数据结构,结果性能一塌糊涂。其实核心就两个维度:价格和数量。
订单簿通常维护两个队列:
- 买单队列(Bid Queue):按价格从高到低排序
- 卖单队列(Ask Queue):按价格从低到高排序
每个价格档位(Price Level)上,挂着的订单按时间优先排列。嗯,这里要注意:暗池里虽然看不到完整深度,但内部撮合时,价格优先、时间优先的规则依然有效。
下面是一个典型的订单簿数据结构定义:
// 订单簿核心结构
struct OrderBook {
map<double, PriceLevel> bids; // 买单,key为价格
map<double, PriceLevel> asks; // 卖单,key为价格
uint64_t sequence_number; // 序列号,用于事件排序
timestamp_t last_update_time; // 最后更新时间
};
// 单个价格档位
struct PriceLevel {
double price; // 价格
uint64_t total_visible_qty; // 可见总量
uint64_t total_hidden_qty; // 隐藏总量(冰山订单专用)
list<Order> orders; // 该价格下的订单列表
};
// 订单实体
struct Order {
string order_id; // 订单ID
string client_id; // 客户ID
double price; // 价格
uint64_t visible_qty; // 可见数量
uint64_t hidden_qty; // 隐藏数量
uint64_t filled_qty; // 已成交数量
OrderSide side; // 买卖方向
OrderType type; // 订单类型
timestamp_t entry_time; // 进入时间
};
你想想看,为什么要把可见数量和隐藏数量分开存?说白了,就是为了快速计算市场深度。暗池虽然不公开深度,但内部系统需要知道每个价格档位的真实流动性。
二、冰山订单的数据结构设计
冰山订单(Iceberg Order),也叫隐藏订单。它的特点是:只暴露一部分数量给市场,剩下的藏在「水下」。我记得第一次做暗池系统时,就被这个设计坑过一次——当时没处理好隐藏数量的更新逻辑,导致撮合时出现了负库存。
冰山订单的核心字段定义如下:
| 字段名 | 类型 | 说明 |
|---|---|---|
| order_id | string | 订单唯一标识 |
| price | double | 订单价格 |
| total_qty | uint64_t | 订单总数量(可见+隐藏) |
| visible_qty | uint64_t | 当前可见数量,对外展示的部分 |
| hidden_qty | uint64_t | 当前隐藏数量,水下部分 |
| peak_size | uint64_t | 每次暴露的峰值大小 |
| remaining_qty | uint64_t | 剩余未成交总量 |
| status | enum | 订单状态:ACTIVE, PARTIAL, FILLED, CANCELLED |
这里有个关键点:visible_qty 和 hidden_qty 是动态变化的。每次成交后,可见部分减少,当可见部分消耗完,系统会从隐藏部分「补充」一个新的可见峰值。
核心逻辑:冰山订单的可见数量 = min(peak_size, remaining_qty)。每次成交后,如果 visible_qty 降为 0,就从 hidden_qty 中取出 peak_size 补充到 visible_qty。
三、隐藏数量与可见数量的字段定义
这两个字段的设计,我建议你特别注意一下。曾经有个项目,因为把 visible_qty 和 hidden_qty 定义成了 int32,结果在极端行情下溢出了——那场面,啧啧。
字段定义规范:
// 推荐使用 uint64_t,确保不会溢出
struct IcebergOrder {
uint64_t visible_qty; // 可见数量,单位:股/手
uint64_t hidden_qty; // 隐藏数量,单位:股/手
// 辅助方法
uint64_t get_total_qty() const { return visible_qty + hidden_qty; }
bool is_fully_visible() const { return hidden_qty == 0; }
bool is_fully_hidden() const { return visible_qty == 0; }
// 成交后更新
void on_trade(uint64_t trade_qty) {
if (trade_qty > visible_qty) {
// 这种情况理论上不应该发生
// 但我见过bug导致这里出问题
trade_qty = visible_qty;
}
visible_qty -= trade_qty;
remaining_qty -= trade_qty;
// 如果可见部分耗尽,从隐藏部分补充
if (visible_qty == 0 && hidden_qty > 0) {
uint64_t replenish = min(peak_size, hidden_qty);
visible_qty = replenish;
hidden_qty -= replenish;
}
}
};
避坑指南:我曾经在测试环境发现,当冰山订单的 peak_size 设置得比最小交易单位还小时,系统会陷入死循环——每次补充后立即成交,然后又补充。解决方案是:peak_size 必须大于等于最小交易单位(lot size)。
四、订单簿的存储与更新策略
暗池交易对延迟极其敏感。我个人习惯用内存数据库来存储订单簿,比如 Redis 或者自研的内存引擎。磁盘存储只做归档和审计用。
更新策略上,有几个原则:
- 增量更新:只发送变化的部分,不发送全量快照
- 事件驱动:每个操作都生成一个事件,方便回放和审计
- 无锁设计:使用 CAS(Compare-And-Swap)或者无锁队列
下面是一个简单的订单簿更新流程:
// 订单簿更新事件
enum EventType {
ADD_ORDER,
CANCEL_ORDER,
TRADE_EXECUTION,
ORDER_MODIFY
};
struct OrderBookEvent {
EventType type;
string order_id;
double price;
uint64_t qty;
uint64_t visible_qty; // 更新后的可见数量
uint64_t hidden_qty; // 更新后的隐藏数量
timestamp_t timestamp;
};
五、知识体系结构图
下面这张图,是我自己画的一个结构梳理。你可以看到暗池订单簿的核心模块和它们之间的关系:
特别注意:冰山订单的隐藏数量在数据库层面必须加密存储。我曾经在审计时发现,某个系统直接把隐藏数量明文写进了日志文件——这等于把底牌亮给了所有人。暗池的核心价值就是信息不透明,隐藏数量泄露会直接破坏市场公平性。
好了,以上就是暗池交易中订单簿模型和冰山订单的数据结构设计。说白了,核心就三件事:价格队列怎么组织、可见隐藏怎么切分、更新逻辑怎么保证一致性。把这些想清楚,你的订单簿模块就稳了。
公众号:蓝海数据掘金营,微信deep3321