协议抽象层设计:定义统一数据模型与适配器模式

各位同学,今天我们来聊聊协议抽象层。说白了,就是怎么把各个交易所乱七八糟的行情数据,变成我们系统里统一、好用的东西。

我在做量化系统的时候,最头疼的就是对接新交易所。每个交易所的返回格式都不一样,有的用数组,有的用对象,字段名更是五花八门。你想想看,如果每个交易所都单独写一套处理逻辑,那代码得多臃肿?维护起来得多痛苦?

所以,我们需要一个协议抽象层。它的核心任务就两个:定义统一数据模型实现接口适配

1. 统一数据模型:给数据一个标准长相

我们先得定好,行情数据长什么样。我习惯把最核心的三个数据结构先固定下来:OrderBookTickerTrade

为什么要先定这三个?因为不管哪个交易所,最终用户关心的就是这三样东西:当前的挂单深度、最新的行情快照、以及每一笔成交记录。

1.1 OrderBook(订单簿)

订单簿就是买卖双方的挂单队列。我在项目中遇到过,有的交易所把买单和卖单放在同一个数组里,用方向字段区分;有的则直接分成两个数组。嗯,这里我们要统一。

// 统一订单簿模型
public class OrderBook {
    public String symbol;          // 交易对,如 "BTCUSDT"
    public long timestamp;         // 时间戳,毫秒
    public List<OrderBookLevel> bids;  // 买单,按价格降序
    public List<OrderBookLevel> asks;  // 卖单,按价格升序
}

public class OrderBookLevel {
    public double price;    // 价格
    public double quantity; // 数量
}

注意看,我特意把 bidsasks 分开,并且规定了排序方向。这样不管交易所原始数据怎么给,我们都能统一处理。

小技巧: 时间戳我统一用毫秒。有些交易所给的是秒,有些是微秒。我建议在适配层就转成毫秒,后面所有逻辑都按毫秒算,省得来回转换。

1.2 Ticker(行情快照)

Ticker 就是当前市场的一个快照。包括最新价、24小时涨跌、成交量这些。这个模型相对简单,但字段名一定要统一。

// 统一Ticker模型
public class Ticker {
    public String symbol;
    public long timestamp;
    public double lastPrice;      // 最新成交价
    public double openPrice;      // 24小时开盘价
    public double highPrice;      // 24小时最高价
    public double lowPrice;       // 24小时最低价
    public double volume;         // 24小时成交量
    public double quoteVolume;    // 24小时成交额
}

我曾经踩过一个坑:某个交易所的 volume 是基础币种的数量,另一个交易所却是计价币种的数量。后来我统一规定:volume 是基础币种数量,quoteVolume 是计价币种数量。这样就不会搞混了。

1.3 Trade(成交记录)

每一笔成交记录,我们关心的是价格、数量、方向和时间。

// 统一Trade模型
public class Trade {
    public String symbol;
    public long timestamp;
    public String tradeId;        // 成交ID,用于去重
    public double price;          // 成交价格
    public double quantity;       // 成交数量
    public boolean isBuyerMaker;  // true: 主动卖单, false: 主动买单
}
注意: tradeId 一定要保留。我在做回测系统时,就因为没去重,导致同一笔成交被计算了两次,策略收益直接翻倍了……嗯,后来查了好久才发现。

2. 接口抽象:定义契约

模型定好了,接下来就是接口。接口就是契约,规定了「你能做什么」,但不规定「你怎么做」。

// 行情数据源接口
public interface MarketDataSource {
    void subscribeOrderBook(String symbol, Consumer<OrderBook> callback);
    void subscribeTicker(String symbol, Consumer<Ticker> callback);
    void subscribeTrade(String symbol, Consumer<Trade> callback);
    void unsubscribe(String symbol);
}

这个接口很简单,就四个方法:订阅订单簿、订阅Ticker、订阅成交、取消订阅。每个订阅方法都接受一个回调函数,当数据到达时,回调函数会被调用。

你可能会问:为什么不用 WebSocket 或者 REST 的细节?因为接口是抽象的,底层用什么协议,那是实现类的事。我们只关心「能订阅数据」这个能力。

3. 适配器模式:把脏活累活封装起来

好了,模型和接口都定义好了。现在问题来了:每个交易所的数据格式都不一样,怎么把它们转换成我们的统一模型?

答案就是适配器模式。每个交易所写一个适配器,负责把交易所的原始数据,转换成我们的统一模型。

// Binance适配器示例
public class BinanceAdapter implements MarketDataSource {
    private WebSocketClient client;
    
    @Override
    public void subscribeOrderBook(String symbol, Consumer<OrderBook> callback) {
        // 订阅Binance的WebSocket频道
        client.subscribe(symbol.toLowerCase() + "@depth20", message -> {
            // 解析Binance的原始JSON
            JSONObject json = new JSONObject(message);
            
            // 转换成统一模型
            OrderBook book = new OrderBook();
            book.symbol = symbol;
            book.timestamp = json.getLong("E");
            
            // 解析买单
            book.bids = new ArrayList<>();
            JSONArray bidsArray = json.getJSONArray("bids");
            for (int i = 0; i < bidsArray.length(); i++) {
                JSONArray level = bidsArray.getJSONArray(i);
                OrderBookLevel l = new OrderBookLevel();
                l.price = level.getDouble(0);
                l.quantity = level.getDouble(1);
                book.bids.add(l);
            }
            
            // 解析卖单(类似)
            // ...
            
            // 回调
            callback.accept(book);
        });
    }
    
    // 其他方法类似...
}

你看,适配器里全是脏活累活:解析JSON、处理时间戳、排序、去重……但这些细节都被封装在适配器内部了。上层业务代码根本不用关心这些。

核心思想: 适配器模式让「变化」和「不变」分离。交易所的数据格式是变化的,但我们的统一模型和业务逻辑是不变的。适配器就是中间的转换层。

4. 整体架构图

下面这张图,展示了整个协议抽象层的架构。我画了张SVG图,方便你理解。

协议抽象层架构图 Binance WebSocket / REST OKX WebSocket / REST Coinbase WebSocket / REST BinanceAdapter 解析JSON → 统一模型 OKXAdapter 解析JSON → 统一模型 CoinbaseAdapter 解析JSON → 统一模型 MarketDataSource 接口 subscribeOrderBook / subscribeTicker / subscribeTrade 统一数据模型 OrderBook | Ticker | Trade 字段统一、类型统一、语义统一 交易所 适配器 接口 数据模型

从这张图你可以看到,整个架构分四层:

  • 交易所层:各个交易所的原始数据源
  • 适配器层:每个交易所一个适配器,负责数据转换
  • 接口层:统一的 MarketDataSource 接口
  • 数据模型层:统一的 OrderBookTickerTrade

数据从下往上流动,每一层只负责自己的事。适配器层是最累的,但也是最值得写的。因为一旦写好,后面加新交易所,你只需要再写一个适配器就行,上层代码完全不用动。

我的经验: 写适配器的时候,一定要把「解析」和「转换」分开。解析是把原始数据变成中间对象,转换是把中间对象变成统一模型。这样如果交易所改了字段名,你只需要改解析部分,转换部分不用动。

5. 总结

协议抽象层设计,说白了就是三件事:

  1. 定模型:把 OrderBookTickerTrade 的字段和类型固定下来
  2. 定接口:定义 MarketDataSource 接口,规定能做什么
  3. 写适配器:每个交易所一个适配器,把脏活累活封装起来

这样做的好处很明显:业务代码只依赖接口和模型,不依赖具体交易所。以后加新交易所,就像插拔U盘一样简单。

嗯,这一章就到这里。记住,抽象层设计得好不好,直接决定了你的系统能走多远。别偷懒,把模型和接口定义清楚,后面会省很多事。

专注资料整理