第十八章:多交易所支持
做市系统做到一定规模,你一定会面临一个问题:要不要接入多个交易所?
我个人习惯是,从一开始就留好接口。别等业务来了再改架构,那会儿改起来真要命。我在项目中见过太多团队,先只接一个交易所,后来要加第二个,结果发现代码里到处都是交易所的硬编码逻辑,改得想哭。
统一接口抽象层
说白了,就是给所有交易所戴上一副「通用面具」。不管底层是币安、OKX 还是 Coinbase,上层代码看到的接口都一样。
核心思路:定义一套标准接口,每个交易所实现自己的适配器。
// 统一交易所接口
public interface ExchangeAdapter {
// 行情接口
OrderBook getOrderBook(String symbol);
Ticker getTicker(String symbol);
// 交易接口
String placeOrder(Order order);
boolean cancelOrder(String orderId);
OrderStatus getOrderStatus(String orderId);
// 账户接口
Balance getBalance(String asset);
List<Balance> getAllBalances();
}
每个交易所写一个实现类。比如 BinanceAdapter、OkxAdapter。这样上层策略代码完全不用改,换交易所就像换插头一样简单。
我的经验:接口设计时,参数尽量用自定义对象,别用原始类型。比如 Order 对象里包含价格、数量、方向、类型等字段。这样后续加字段不影响已有实现。
交易所差异处理
你以为接口统一了就万事大吉?太天真了。交易所之间的差异,藏在细节里。
常见的差异点:
- 精度不同:有的交易所价格支持8位小数,有的只支持2位。我曾经因为精度问题,挂单一直失败,查了半天才发现是小数点位数不对。
- 订单类型:有的支持 IOC(立即成交否则取消),有的只支持 GTC(普通限价单)。
- 费率结构:Maker 和 Taker 费率不同,VIP 等级不同,甚至有的交易所对特定币种有优惠。
- 限频规则:有的每秒允许100次请求,有的只有10次。不注意这个,API 会被封。
- 时间戳格式:有的用毫秒,有的用微秒,有的甚至用字符串。
避坑指南:我曾经在对接一个新交易所时,没仔细看文档,直接用 UTC 时间戳。结果对方要求的是本地时间戳,所有订单都报错。后来我养成了一个习惯:每个交易所适配器里,单独写一个时间转换函数。
处理这些差异,我建议用「配置驱动」的方式。把差异点提取成配置文件,而不是硬编码在代码里。
// 交易所配置示例
{
"exchange": "binance",
"symbols": {
"BTCUSDT": {
"pricePrecision": 2,
"qtyPrecision": 5,
"minNotional": 10.0
}
},
"rateLimit": {
"perSecond": 10,
"perMinute": 1200
},
"fee": {
"maker": 0.001,
"taker": 0.001
}
}
套利机会识别
多交易所最大的价值是什么?套利。
同一时刻,不同交易所的同一币种价格可能不一样。比如币安上 BTC 卖 50000,OKX 上卖 50010。这10块钱的差价就是套利空间。
套利的核心逻辑:
- 实时获取所有交易所的行情数据
- 计算价差:
价差 = 最高买价 - 最低卖价 - 判断是否覆盖成本:价差 > 手续费 + 滑点 + 转账成本
- 如果有利可图,同时在低价交易所买入,高价交易所卖出
// 套利机会检测
public class ArbitrageDetector {
public ArbitrageOpportunity findOpportunity(Map<String, OrderBook> orderBooks) {
// 找出所有交易所的最优买卖价
ExchangePrice bestBid = findBestBid(orderBooks);
ExchangePrice bestAsk = findBestAsk(orderBooks);
double spread = bestBid.price - bestAsk.price;
double cost = calculateCost(bestAsk.exchange, bestBid.exchange);
if (spread > cost) {
return new ArbitrageOpportunity(
bestAsk.exchange, // 买入交易所
bestBid.exchange, // 卖出交易所
spread - cost // 预期利润
);
}
return null;
}
}
注意:套利不是看到价差就冲。要考虑滑点、延迟、转账时间。我曾经看到一个价差,等程序执行完,价差已经消失了,反而亏了手续费。
路由逻辑
路由逻辑解决的是「这个订单该发到哪个交易所」的问题。
简单场景:你只做单一交易所的做市,不需要路由。但如果你同时做多个交易所,就需要一个智能路由器。
路由策略:
- 流动性优先:哪个交易所深度好,就往哪发。适合大额订单。
- 费率优先:哪个交易所手续费低,就往哪发。适合高频小单。
- 延迟优先:哪个交易所响应快,就往哪发。适合抢单策略。
- 智能路由:综合评估流动性、费率、延迟、当前持仓等因素,动态选择最优交易所。
// 路由决策
public class OrderRouter {
private List<ExchangeAdapter> exchanges;
private RoutingStrategy strategy;
public ExchangeAdapter route(Order order) {
// 根据策略选择最优交易所
return strategy.select(order, exchanges);
}
}
// 策略接口
public interface RoutingStrategy {
ExchangeAdapter select(Order order, List<ExchangeAdapter> exchanges);
}
我个人习惯用「加权评分」的方式做路由。给每个交易所打分,分数最高的胜出。
// 加权评分路由
public class WeightedRouting implements RoutingStrategy {
@Override
public ExchangeAdapter select(Order order, List<ExchangeAdapter> exchanges) {
ExchangeAdapter best = null;
double bestScore = Double.MIN_VALUE;
for (ExchangeAdapter exchange : exchanges) {
double score = 0;
score += exchange.getLiquidityScore(order) * 0.4; // 流动性权重40%
score += exchange.getFeeScore(order) * 0.3; // 费率权重30%
score += exchange.getLatencyScore() * 0.2; // 延迟权重20%
score += exchange.getBalanceScore(order) * 0.1; // 余额权重10%
if (score > bestScore) {
bestScore = score;
best = exchange;
}
}
return best;
}
}
我的建议:路由逻辑一定要可配置。不同策略、不同权重,最好能通过配置文件动态调整。别写死在代码里,否则每次改策略都要重新部署。
整体架构图
下面这张图展示了多交易所支持的整体架构。从底层交易所到上层策略,每一层各司其职。
这张图里,从上到下依次是:策略层决定「做什么」,路由层决定「去哪做」,统一接口层屏蔽「怎么做」,适配器层处理「差异在哪」。套利检测模块横跨所有交易所,实时监控价差。
总结一下:多交易所支持不是简单的「再加一个交易所」。它需要一套完整的架构设计:统一接口、差异处理、套利检测、智能路由。每一步都有坑,但踩过去之后,你的系统会变得非常强大。
嗯,这套架构我用了好几年,在多个项目里验证过。虽然一开始搭建时多花了一些时间,但后续加交易所、改策略都非常轻松。值得投入。
交易系统化学习资料 微信Strategy888888