第四节:基础数据结构——Order、Trade、Quote、OrderBook 的设计与实现

做市商系统里,数据结构就是地基。地基不稳,上层再漂亮的策略也是白搭。今天咱们就把这四个核心类掰开揉碎讲清楚。

4.1 Order 类:订单的身份证

一个订单,说白了就是「谁在什么价格上想买/卖多少」。但实际项目中,字段远不止这些。

核心字段清单

  • order_id:全局唯一,我习惯用「时间戳+自增序列」生成
  • symbol:交易对,比如 BTC/USDT
  • side:BUY 或 SELL
  • price:价格,用整数表示(避免浮点精度问题)
  • quantity:原始数量
  • filled_quantity:已成交数量
  • status:NEW / PARTIALLY_FILLED / FILLED / CANCELED
  • timestamp:订单创建时间,纳秒级
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 数据结构关系图

下面这张图展示了四个类之间的协作关系:

Order 订单实体 orderId, price, qty Trade 成交记录 tradeId, price, qty Quote 最优报价 bid/ask price & size OrderBook 订单簿核心容器 bids: TreeMap<价格, 订单列表> asks: TreeMap<价格, 订单列表> 包含 生成 聚合 数据类 容器类

4.6 避坑指南与最佳实践

关于内存优化:一个 Order 对象如果包含 20 个字段,在 Java 中大约占用 200+ 字节。当订单簿有 100 万笔订单时,光 Order 对象就占 200MB。我建议用 long[] 数组存储订单字段,通过索引访问,能节省 60% 以上内存。

关于并发安全:订单簿会被多个线程同时读写(行情线程、交易线程、风控线程)。千万别用 synchronized 锁整个订单簿——性能会惨不忍睹。我推荐用读写锁(ReadWriteLock)或者无锁数据结构(如 Disruptor)。

最后说一句:这些数据结构看起来简单,但每个字段的选择、每个集合的选用,背后都是血泪教训。你把这些基础打牢了,后面做策略、做风控才能游刃有余。

蓝海数据掘金营,专注资料整理