协议抽象层设计:定义统一数据模型与适配器模式
各位同学,今天我们来聊聊协议抽象层。说白了,就是怎么把各个交易所乱七八糟的行情数据,变成我们系统里统一、好用的东西。
我在做量化系统的时候,最头疼的就是对接新交易所。每个交易所的返回格式都不一样,有的用数组,有的用对象,字段名更是五花八门。你想想看,如果每个交易所都单独写一套处理逻辑,那代码得多臃肿?维护起来得多痛苦?
所以,我们需要一个协议抽象层。它的核心任务就两个:定义统一数据模型和实现接口适配。
1. 统一数据模型:给数据一个标准长相
我们先得定好,行情数据长什么样。我习惯把最核心的三个数据结构先固定下来:OrderBook、Ticker、Trade。
为什么要先定这三个?因为不管哪个交易所,最终用户关心的就是这三样东西:当前的挂单深度、最新的行情快照、以及每一笔成交记录。
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; // 数量
}
注意看,我特意把 bids 和 asks 分开,并且规定了排序方向。这样不管交易所原始数据怎么给,我们都能统一处理。
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图,方便你理解。
从这张图你可以看到,整个架构分四层:
- 交易所层:各个交易所的原始数据源
- 适配器层:每个交易所一个适配器,负责数据转换
- 接口层:统一的
MarketDataSource接口 - 数据模型层:统一的
OrderBook、Ticker、Trade
数据从下往上流动,每一层只负责自己的事。适配器层是最累的,但也是最值得写的。因为一旦写好,后面加新交易所,你只需要再写一个适配器就行,上层代码完全不用动。
5. 总结
协议抽象层设计,说白了就是三件事:
- 定模型:把
OrderBook、Ticker、Trade的字段和类型固定下来 - 定接口:定义
MarketDataSource接口,规定能做什么 - 写适配器:每个交易所一个适配器,把脏活累活封装起来
这样做的好处很明显:业务代码只依赖接口和模型,不依赖具体交易所。以后加新交易所,就像插拔U盘一样简单。
嗯,这一章就到这里。记住,抽象层设计得好不好,直接决定了你的系统能走多远。别偷懒,把模型和接口定义清楚,后面会省很多事。