暗池交易核心数据结构:订单簿模型与冰山订单

各位同学,今天我们来聊聊暗池交易里最核心的数据结构——订单簿模型。说实话,这个模型在传统交易所里已经非常成熟了,但到了暗池环境里,情况就变得微妙起来。

我个人习惯把订单簿比作一个「多层停车场」。每一层代表一个价格,停着的车就是订单。但暗池里的停车场,有些车是「隐形」的——你只能看到一部分,甚至完全看不到。这就是我们今天要重点讲的冰山订单。

一、订单簿模型的基本结构

先看最基础的订单簿。我在项目中遇到过不少团队,上来就搞复杂的数据结构,结果性能一塌糊涂。其实核心就两个维度:价格数量

订单簿通常维护两个队列:

  • 买单队列(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;
};

五、知识体系结构图

下面这张图,是我自己画的一个结构梳理。你可以看到暗池订单簿的核心模块和它们之间的关系:

暗池订单簿核心数据结构 订单簿 (OrderBook) 买单队列 (Bids) 卖单队列 (Asks) 价格档位 (PriceLevel) 价格档位 (PriceLevel) 普通订单 冰山订单 隐藏订单 visible_qty | hidden_qty | peak_size total_qty | remaining_qty | status 图例 主模块 特殊订单

特别注意:冰山订单的隐藏数量在数据库层面必须加密存储。我曾经在审计时发现,某个系统直接把隐藏数量明文写进了日志文件——这等于把底牌亮给了所有人。暗池的核心价值就是信息不透明,隐藏数量泄露会直接破坏市场公平性。

好了,以上就是暗池交易中订单簿模型和冰山订单的数据结构设计。说白了,核心就三件事:价格队列怎么组织、可见隐藏怎么切分、更新逻辑怎么保证一致性。把这些想清楚,你的订单簿模块就稳了。

公众号:蓝海数据掘金营,微信deep3321