3、核心数据结构设计:Order类设计、OrderBook类设计、价格队列与时间优先、Level 2 数据模型

好,咱们直接进入正题。数据结构是订单簿的骨架,骨架歪了,后面再怎么优化也白搭。我个人习惯把这一章叫做「地基工程」,因为后面所有的高频策略、风控逻辑,都踩在这几个类上面。

3.1 Order类设计——最基础的积木

一个订单对象,说白了就是一笔交易的「身份证」。它需要记录谁、在什么价格、想买还是想卖、要多少、什么时候来的。

我在项目中遇到过最坑的事:有人把订单ID设计成自增整数,结果回测时发现不同交易所的订单ID居然冲突了。从那以后,我强制要求订单ID必须是全局唯一的字符串,比如 exchange_timestamp_seq 这种格式。

核心字段清单:

  • order_id:全局唯一字符串,别用自增ID
  • symbol:交易对,比如 BTC/USDT
  • side:买或卖,用枚举,别用 0/1 魔法数字
  • price:价格,用整数表示(避免浮点误差)
  • quantity:原始数量
  • filled_quantity:已成交数量
  • timestamp:纳秒级时间戳,用于时间优先排序
  • status:NEW, PARTIALLY_FILLED, FILLED, CANCELED
// 伪代码示例
class Order {
    String orderId;
    String symbol;
    Side side;          // BUY or SELL
    long price;         // 用 long 存储,单位是 tick 的倍数
    long quantity;
    long filledQty;
    long timestampNano; // System.nanoTime()
    OrderStatus status;
}

嗯,这里要注意:价格和数量一定要用整数。你想想看,浮点数在比较大小的时候有多坑?0.1 + 0.2 不等于 0.3 这种问题,在订单簿里就是灾难。我建议把所有价格都乘以最小价格精度,转成 long 来存。

3.2 OrderBook类设计——订单簿的心脏

OrderBook 类负责管理所有订单。它内部维护两个「价格队列」:一个买单队列(Bid),一个卖单队列(Ask)。

说白了,OrderBook 就干三件事:加订单、删订单、改订单。但怎么加、怎么删,决定了你的系统能扛多大并发。

我的设计原则:

  • TreeMapSkipList 来维护价格层级,保证价格有序
  • 每个价格层级下挂一个 LinkedListPriorityQueue,按时间排序
  • HashMap 做订单ID到订单对象的映射,实现 O(1) 查找
class OrderBook {
    String symbol;
    TreeMap<Long, PriceLevel> bids; // 价格降序
    TreeMap<Long, PriceLevel> asks; // 价格升序
    Map<String, Order> orderMap;    // orderId -> Order

    void addOrder(Order order);
    void cancelOrder(String orderId);
    Order getOrder(String orderId);
    List<Order> getOrdersAtPrice(long price, Side side);
}

我曾经犯过一个错:直接用 ArrayList 来存价格队列,每次插入都做二分查找。结果在回测 100 万笔订单时,性能直接崩了。后来换成 TreeMap,速度提升了 10 倍不止。数据结构选型,真的差很多。

3.3 价格队列与时间优先——公平的基石

交易所的撮合规则,绝大多数都是「价格优先、时间优先」。什么意思?

  • 价格优先:买单出价高的先成交,卖单出价低的先成交
  • 时间优先:同样价格,先来的先成交

实现起来其实不复杂。每个价格层级维护一个队列,新订单插到队尾。撮合时从队头开始匹配。

注意:时间戳的精度非常关键。如果只用毫秒,同一毫秒内来的订单顺序就无法保证。我建议用 System.nanoTime() 或者全局递增序列号。我在对接某交易所时,发现他们用微秒时间戳,结果同一微秒内居然有 3 笔订单,最后靠序列号才排好序。

class PriceLevel {
    long price;
    LinkedList<Order> orders; // 按到达时间排序

    void addOrder(Order order) {
        orders.addLast(order); // 插到队尾
    }

    Order getHeadOrder() {
        return orders.peekFirst(); // 取队头
    }

    void removeOrder(String orderId) {
        // 从队列中移除指定订单
    }
}

3.4 Level 2 数据模型——深度数据的真相

Level 2 数据,也叫市场深度数据。它不只是显示最优买卖价,而是展示每个价格层级上的挂单量。

我个人习惯把 Level 2 数据抽象成两个数组:bids[][]asks[][]。每个元素是 [price, quantity] 对。

价格层级 买单价格 买单数量 卖单价格 卖单数量
第1层 100.50 1,000 100.55 800
第2层 100.45 2,000 100.60 1,500
第3层 100.40 3,500 100.65 2,200

为什么需要 Level 2?因为只看最优价,你根本不知道市场的真实压力。比如买单最优价只有 100 手,但第二层挂了 10 万手,这说明有大资金在下面托底。这种信息,做策略时非常有用。

Level 2 快照 vs 增量更新:

  • 快照:全量推送,数据量大,适合初始化
  • 增量:只推送变化的部分,比如某个价格层级的数量变了,或者新增/删除了一个层级

我建议两者结合:启动时拿快照,之后用增量更新。这样可以大幅减少带宽消耗。我曾经帮一个团队优化过,从全量推送改成增量推送后,带宽占用降低了 80%。

3.5 核心数据流图

下面这张图展示了订单从进入系统到更新 Level 2 数据的完整流程。我习惯用这种图来跟团队对齐思路,避免各写各的。

订单簿事件驱动数据流 外部订单事件 事件处理器 Order 管理 价格队列更新 Level 2 数据生成 深度快照 / 增量推送

你看,整个流程其实很清晰:外部事件进来,先经过处理器,然后同时更新订单状态和价格队列,最后生成 Level 2 数据推出去。每个环节都是独立的,方便后面做并行优化。

避坑指南:我曾经在 Level 2 增量更新时,忘记处理「价格层级消失」的情况。比如某个价格上的所有订单都撤了,这个层级应该从深度数据中移除。结果我的系统一直保留着空层级,导致前端显示异常。后来加了个检查:每次更新后,如果某个价格层级的数量为 0,就把它从 TreeMap 中删掉。

好了,核心数据结构就聊这么多。记住一句话:数据结构选对了,后面的事情就顺了。Order 类要轻量,OrderBook 要高效,价格队列要公平,Level 2 要准确。把这四点吃透,订单簿的骨架就稳了。


专注资料整理