3、核心数据结构设计:Order类设计、OrderBook类设计、价格队列与时间优先、Level 2 数据模型
好,咱们直接进入正题。数据结构是订单簿的骨架,骨架歪了,后面再怎么优化也白搭。我个人习惯把这一章叫做「地基工程」,因为后面所有的高频策略、风控逻辑,都踩在这几个类上面。
3.1 Order类设计——最基础的积木
一个订单对象,说白了就是一笔交易的「身份证」。它需要记录谁、在什么价格、想买还是想卖、要多少、什么时候来的。
我在项目中遇到过最坑的事:有人把订单ID设计成自增整数,结果回测时发现不同交易所的订单ID居然冲突了。从那以后,我强制要求订单ID必须是全局唯一的字符串,比如 exchange_timestamp_seq 这种格式。
核心字段清单:
order_id:全局唯一字符串,别用自增IDsymbol:交易对,比如 BTC/USDTside:买或卖,用枚举,别用 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 就干三件事:加订单、删订单、改订单。但怎么加、怎么删,决定了你的系统能扛多大并发。
我的设计原则:
- 用
TreeMap或SkipList来维护价格层级,保证价格有序 - 每个价格层级下挂一个
LinkedList或PriorityQueue,按时间排序 - 用
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 数据的完整流程。我习惯用这种图来跟团队对齐思路,避免各写各的。
你看,整个流程其实很清晰:外部事件进来,先经过处理器,然后同时更新订单状态和价格队列,最后生成 Level 2 数据推出去。每个环节都是独立的,方便后面做并行优化。
避坑指南:我曾经在 Level 2 增量更新时,忘记处理「价格层级消失」的情况。比如某个价格上的所有订单都撤了,这个层级应该从深度数据中移除。结果我的系统一直保留着空层级,导致前端显示异常。后来加了个检查:每次更新后,如果某个价格层级的数量为 0,就把它从 TreeMap 中删掉。
好了,核心数据结构就聊这么多。记住一句话:数据结构选对了,后面的事情就顺了。Order 类要轻量,OrderBook 要高效,价格队列要公平,Level 2 要准确。把这四点吃透,订单簿的骨架就稳了。