第四节:基础数据结构——Order、Trade、Quote、OrderBook 的设计与实现
做市商系统里,数据结构就是地基。地基不稳,上层再漂亮的策略也是白搭。今天咱们就把这四个核心类掰开揉碎讲清楚。
4.1 Order 类:订单的身份证
一个订单,说白了就是「谁在什么价格上想买/卖多少」。但实际项目中,字段远不止这些。
核心字段清单
order_id:全局唯一,我习惯用「时间戳+自增序列」生成symbol:交易对,比如 BTC/USDTside:BUY 或 SELLprice:价格,用整数表示(避免浮点精度问题)quantity:原始数量filled_quantity:已成交数量status:NEW / PARTIALLY_FILLED / FILLED / CANCELEDtimestamp:订单创建时间,纳秒级
public class Order {
private final long orderId;
private final String symbol;
private final Side side;
private final long price; // 价格 * 1e8,用 long 存
private long quantity; // 数量 * 1e8
private long filledQuantity;
private OrderStatus status;
private final long timestamp;
// 构造函数、getter、setter 省略
}
嗯,这里要注意:价格和数量一定要用整数。我早期做外汇做市时,直接用 double 存价格,结果浮点误差导致订单簿对不上,排查了整整两天。从那以后,所有价格我都乘以精度因子转成 long。
4.2 Trade 类:成交的流水账
Trade 是订单匹配后的产物。它记录了一笔交易的所有细节。
个人习惯:我会在 Trade 里同时保存 taker_order_id 和 maker_order_id。这样后续做归因分析时,能快速定位是哪笔订单吃掉了哪笔挂单。
public class Trade {
private final long tradeId;
private final long takerOrderId;
private final long makerOrderId;
private final String symbol;
private final Side takerSide;
private final long price;
private final long quantity;
private final long timestamp;
// 省略构造和 getter
}
为什么要有 takerSide?因为做市商需要知道自己是主动吃单还是被动挂单。被动挂单有返佣,主动吃单要付手续费——这直接影响到你的盈亏计算。
4.3 Quote 类:报价的快照
Quote 不是订单,它是做市商对外发布的「最优买卖报价」。说白了,就是告诉市场:「我当前愿意在什么价位买,什么价位卖」。
| 字段 | 说明 |
|---|---|
| bid_price | 买一价 |
| bid_size | 买一量 |
| ask_price | 卖一价 |
| ask_size | 卖一量 |
| quote_id | 报价唯一标识 |
| timestamp | 报价生成时间 |
public class Quote {
private final long quoteId;
private final String symbol;
private final long bidPrice;
private final long bidSize;
private final long askPrice;
private final long askSize;
private final long timestamp;
// 校验逻辑:bid_price 必须小于 ask_price
public boolean isValid() {
return bidPrice < askPrice && bidSize > 0 && askSize > 0;
}
}
我曾经犯过一个低级错误:报价时 bid_price 和 ask_price 写反了,结果系统瞬间被套利机器人扫光。所以我现在每个 Quote 对象创建时都会做 isValid() 校验,算是给自己加个保险。
4.4 OrderBook 类:订单簿的核心
OrderBook 是报价引擎的心脏。它维护着当前市场所有未成交的限价单。
性能关键点:订单簿的增删改查必须在微秒级完成。我见过有人用 TreeMap 做订单簿,结果每秒 10 万笔订单时直接 OOM。
我个人推荐用「价格桶 + 链表」的结构:
- 每个价格对应一个桶(bucket),桶内按时间顺序排列订单
- 用
TreeMap<Long, List<Order>>存储买盘和卖盘 - 买盘按价格降序排列,卖盘按价格升序排列
public class OrderBook {
private final String symbol;
private final TreeMap<Long, List<Order>> bids; // 买盘,key 是价格
private final TreeMap<Long, List<Order>> asks; // 卖盘
private final Map<Long, Order> orderMap; // orderId -> Order 快速查找
public OrderBook(String symbol) {
this.symbol = symbol;
this.bids = new TreeMap<>(Comparator.reverseOrder());
this.asks = new TreeMap<>(Comparator.naturalOrder());
this.orderMap = new HashMap<>();
}
// 新增订单
public void addOrder(Order order) {
TreeMap<Long, List<Order>> book = order.getSide() == Side.BUY ? bids : asks;
book.computeIfAbsent(order.getPrice(), k -> new ArrayList<>()).add(order);
orderMap.put(order.getOrderId(), order);
}
// 撤销订单
public void cancelOrder(long orderId) {
Order order = orderMap.remove(orderId);
if (order != null) {
TreeMap<Long, List<Order>> book = order.getSide() == Side.BUY ? bids : asks;
List<Order> bucket = book.get(order.getPrice());
if (bucket != null) {
bucket.remove(order);
if (bucket.isEmpty()) {
book.remove(order.getPrice());
}
}
}
}
// 获取最优买卖价
public Quote getTopQuote() {
if (bids.isEmpty() || asks.isEmpty()) return null;
long bidPrice = bids.firstKey();
long askPrice = asks.firstKey();
long bidSize = bids.get(bidPrice).stream().mapToLong(Order::getRemainingQuantity).sum();
long askSize = asks.get(askPrice).stream().mapToLong(Order::getRemainingQuantity).sum();
return new Quote(0, symbol, bidPrice, bidSize, askPrice, askSize, System.nanoTime());
}
}
你想想看,为什么不用 ConcurrentHashMap?因为订单簿需要按价格排序取最优价,TreeMap 的红黑树结构天然支持这个需求。但要注意线程安全——实际生产环境我会用 ConcurrentSkipListMap 替代 TreeMap。
4.5 数据结构关系图
下面这张图展示了四个类之间的协作关系:
4.6 避坑指南与最佳实践
关于内存优化:一个 Order 对象如果包含 20 个字段,在 Java 中大约占用 200+ 字节。当订单簿有 100 万笔订单时,光 Order 对象就占 200MB。我建议用 long[] 数组存储订单字段,通过索引访问,能节省 60% 以上内存。
关于并发安全:订单簿会被多个线程同时读写(行情线程、交易线程、风控线程)。千万别用 synchronized 锁整个订单簿——性能会惨不忍睹。我推荐用读写锁(ReadWriteLock)或者无锁数据结构(如 Disruptor)。
最后说一句:这些数据结构看起来简单,但每个字段的选择、每个集合的选用,背后都是血泪教训。你把这些基础打牢了,后面做策略、做风控才能游刃有余。