第二十三讲:订单流与套利——跨交易所订单流差异与盘口价差套利

套利,说白了就是赚「信息差」的钱。在量化交易里,这个信息差往往藏在订单流里。

我个人习惯把套利分成两类:一类是统计套利,靠数学模型找相关性;另一类是盘口套利,靠订单流的速度差和价差。今天咱们聊的是后者——跨交易所的订单流差异,以及怎么用Python抓这种机会。

核心逻辑:同一资产在不同交易所的订单流不可能完全同步。谁先捕捉到这种「不同步」,谁就能吃到价差。

23.1 跨交易所订单流差异的本质

为什么会有差异?原因其实很简单:

  • 物理延迟:交易所服务器分布在不同的数据中心,网络传输有快有慢
  • 撮合机制不同:有的用连续竞价,有的用集合竞价,订单簿更新频率不一样
  • 流动性分布:大单砸在一个交易所,另一个交易所可能还没反应过来

我在项目中遇到过最典型的情况:比特币在币安突然出现一笔500BTC的卖单,价格瞬间砸了0.3%。但OKX那边订单簿还没更新,价差瞬间拉大到0.5%以上。嗯,这就是套利窗口。

避坑指南:我曾经以为这种窗口能持续好几秒,结果回测发现大部分窗口不到200毫秒就消失了。所以,别想着手动操作,必须用程序。

23.2 盘口价差套利的数学模型

套利信号其实就是一个条件判断:

# 伪代码逻辑
if (交易所A的卖一价 - 交易所B的买一价) > 手续费 + 滑点成本:
    触发套利信号

但实际没那么简单。你想想看,价差大不一定能成交,还要考虑:

  • 深度够不够:卖一挂了10个币,你想吃100个,那成交价就不是卖一了
  • 对手盘速度:你下单的同时,别人也在抢
  • 结算风险:跨所转账需要时间,万一价差反转了呢?

所以我一般用这个公式来算「有效套利空间」:

有效价差 = (卖一价 - 买一价) - 手续费率 * 2 - 滑点预估 - 风险缓冲

滑点预估怎么算?我习惯用订单簿前5档的加权平均价差。说白了,就是模拟一下你吃单进去,实际成交价会偏离多少。

23.3 Python实现套利信号

下面是我常用的一个套利信号检测模块。代码不长,但核心逻辑都在里面了。

import asyncio
import aiohttp
import json
from decimal import Decimal, ROUND_DOWN

class ArbitrageDetector:
    """
    跨交易所套利信号检测器
    支持多交易所订单流实时对比
    """
    
    def __init__(self, fee_rate=0.001, slippage=0.0005, buffer=0.0002):
        self.fee_rate = fee_rate          # 手续费率
        self.slippage = slippage          # 滑点预估
        self.buffer = buffer              # 风险缓冲
        self.orderbooks = {}              # 存储各交易所订单簿
        
    async def fetch_orderbook(self, exchange, symbol):
        """异步获取订单簿数据"""
        # 这里以币安和OKX为例
        urls = {
            'binance': f'https://api.binance.com/api/v3/depth?symbol={symbol}&limit=10',
            'okx': f'https://www.okx.com/api/v5/market/books?instId={symbol}&sz=10'
        }
        
        async with aiohttp.ClientSession() as session:
            async with session.get(urls[exchange]) as resp:
                data = await resp.json()
                self.orderbooks[exchange] = self._parse_orderbook(data, exchange)
    
    def _parse_orderbook(self, data, exchange):
        """解析不同交易所的订单簿格式"""
        if exchange == 'binance':
            return {
                'bids': [[Decimal(p), Decimal(q)] for p, q in data['bids']],
                'asks': [[Decimal(p), Decimal(q)] for p, q in data['asks']]
            }
        elif exchange == 'okx':
            return {
                'bids': [[Decimal(item[0]), Decimal(item[1])] for item in data['data'][0]['bids']],
                'asks': [[Decimal(item[0]), Decimal(item[1])] for item in data['data'][0]['asks']]
            }
    
    def calculate_effective_spread(self, ex_a, ex_b):
        """计算有效套利价差"""
        if ex_a not in self.orderbooks or ex_b not in self.orderbooks:
            return None
        
        # 交易所A的卖一价 vs 交易所B的买一价
        ask_a = self.orderbooks[ex_a]['asks'][0][0]
        bid_b = self.orderbooks[ex_b]['bids'][0][0]
        
        # 原始价差
        raw_spread = ask_a - bid_b
        
        # 扣除成本
        cost = (ask_a + bid_b) * (self.fee_rate * 2 + self.slippage + self.buffer)
        effective_spread = raw_spread - cost
        
        return float(effective_spread)
    
    def detect_signal(self, ex_a='binance', ex_b='okx', threshold=0.0):
        """检测套利信号"""
        spread = self.calculate_effective_spread(ex_a, ex_b)
        
        if spread is None:
            return None
        
        if spread > threshold:
            return {
                'signal': 'BUY_EX_B_SELL_EX_A',
                'spread': spread,
                'ex_a_ask': self.orderbooks[ex_a]['asks'][0][0],
                'ex_b_bid': self.orderbooks[ex_b]['bids'][0][0],
                'timestamp': asyncio.get_event_loop().time()
            }
        elif spread < -threshold:
            return {
                'signal': 'BUY_EX_A_SELL_EX_B',
                'spread': abs(spread),
                'ex_a_bid': self.orderbooks[ex_a]['bids'][0][0],
                'ex_b_ask': self.orderbooks[ex_b]['asks'][0][0],
                'timestamp': asyncio.get_event_loop().time()
            }
        return None

# 使用示例
async def main():
    detector = ArbitrageDetector(fee_rate=0.001, slippage=0.0003, buffer=0.0002)
    
    while True:
        # 同时获取两个交易所的订单簿
        await asyncio.gather(
            detector.fetch_orderbook('binance', 'BTCUSDT'),
            detector.fetch_orderbook('okx', 'BTC-USDT')
        )
        
        signal = detector.detect_signal(threshold=0.0)
        if signal:
            print(f"套利信号: {signal['signal']}, 价差: {signal['spread']:.4f}")
        
        await asyncio.sleep(0.1)  # 100ms轮询一次

if __name__ == '__main__':
    asyncio.run(main())

注意:这个代码只是信号检测,没有包含下单和风控逻辑。实际生产中,你还需要考虑:

  • 订单簿快照与增量更新的同步问题
  • WebSocket连接断线重连
  • 交易所API限频控制
  • 资金划转的延迟预估

23.4 订单流差异的可视化分析

光看数字不够直观。我习惯把两个交易所的订单簿画在一起,一眼就能看出哪里有机会。

下面这张图展示了跨交易所订单流的典型差异:

跨交易所订单簿差异对比 交易所A (卖盘密集) 交易所B (买盘密集) 价格 价差窗口 卖一价 A 买一价 B 套利方向 在A卖,在B买,赚取价差

从图上能清楚看到:交易所A的卖盘深度明显大于买盘,价格被压得较低;而交易所B正好相反,买盘厚实,价格偏高。这种结构性的订单流差异,就是套利机会的来源。

23.5 实战中的几个坑

讲几个我踩过的坑,希望能帮你省点学费:

  1. 别只看第一档:卖一和买一的价差再大,如果深度不够,你一进去就把价格打穿了。我建议至少看前5档的加权平均。
  2. 手续费是隐形杀手:有些交易所的maker费率很低,但taker费率很高。你以为是套利,结果手续费一扣,反而亏了。
  3. 网络延迟比你想象的严重:我曾经在AWS新加坡节点跑策略,结果发现信号出来的时候,交易所的撮合引擎已经先我一步了。后来我把服务器搬到了交易所的同一机房。
  4. 回测数据要小心:历史数据里的价差看起来很美,但那是「事后诸葛亮」。实际交易中,你的订单可能根本成交不了。

我的建议:刚开始做跨所套利,先选流动性好的品种(比如BTC、ETH),用最小仓位跑一段时间。别一上来就上杠杆,那是在给交易所送手续费。

23.6 总结

跨交易所订单流套利,说白了就是比谁快、谁准。快,靠的是低延迟的网络和高效的代码;准,靠的是对订单簿结构的深刻理解。

我个人觉得,这个策略最迷人的地方在于:它不预测价格涨跌,只赚「不同步」的钱。只要两个交易所还在物理上分离,这种机会就永远存在。

嗯,今天就聊到这儿。代码已经给了,剩下的就是你自己去跑一跑、调一调。记住,纸上得来终觉浅,绝知此事要躬行。


无相订单流研究社 微信Lucian808555