4、实时行情处理:行情订阅、数据清洗、Tick级数据对齐

做套利的人都知道一句话:行情是命,延迟是病

我做了这么多年跨交易所套利,见过太多策略本身没问题,最后死在行情处理上的案例。说白了,你算法再牛,如果拿到的Tick数据是脏的、乱的、不同步的,那一切都是白搭。

这一章,我们就来聊聊实时行情处理的核心三件事:怎么订阅、怎么清洗、怎么对齐

4.1 行情订阅:别一股脑全收

很多新手上来就想订阅所有交易所的所有品种。嗯,这里要注意——行情数据量比你想象的大得多。

以币安和OKX为例,光BTC/USDT一个交易对,高峰期每秒能产生几十甚至上百笔Tick。如果你同时订阅50个交易对,带宽和CPU直接拉满。

⚠️ 我曾经踩过的坑: 刚开始做的时候,我图省事,把所有交易对都订阅了。结果服务器带宽被打满,行情延迟从5ms飙到了200ms。套利策略直接变成亏钱机器。

我的建议是:只订阅你策略需要的交易对。比如你做BTC和ETH的价差套利,那就只订阅这两个币种在目标交易所的深度和成交数据。

订阅方式上,我个人习惯用WebSocket。RESTful轮询在Tick级场景下基本不可用,延迟太高。

// 伪代码示例:WebSocket订阅
ws.connect('wss://exchange1.com/ws')
ws.subscribe({
  channel: 'ticker',
  symbol: 'BTC/USDT'
})

ws.on('message', function(data) {
  // 收到行情数据
  processTick(data)
})

4.2 数据清洗:脏数据是隐形杀手

你以为交易所发过来的数据都是干净的吗?太天真了。

我在项目中遇到过好几次,交易所偶尔会推送一些异常数据:价格突然跳变、时间戳错乱、甚至重复推送同一笔成交。如果不做清洗,策略会做出错误决策。

数据清洗我一般做三件事:

  • 去重:同一笔成交ID出现两次,只保留第一次
  • 时间戳校验:如果当前Tick的时间戳比上一个还早,直接丢弃
  • 价格合理性检查:价格偏离最近N笔成交均值超过3个标准差,标记为异常

核心原则:宁可丢数据,不要用脏数据。

套利策略对数据质量极其敏感。一个错误的价格信号,可能让你在错误的方向上开仓。

def clean_tick(tick, last_tick):
    # 去重
    if tick['trade_id'] == last_tick['trade_id']:
        return None
    
    # 时间戳校验
    if tick['timestamp'] <= last_tick['timestamp']:
        return None
    
    # 价格合理性检查
    price_change = abs(tick['price'] - last_tick['price']) / last_tick['price']
    if price_change > 0.03:  # 超过3%
        log.warning(f"异常价格跳变: {tick}")
        return None
    
    return tick

4.3 Tick级数据对齐:套利的灵魂

好了,行情订阅了,数据也清洗了。接下来是最关键的一步:对齐

你想想看,交易所A和交易所B的Tick到达时间是不一样的。网络延迟、处理延迟、甚至交易所自身的撮合速度都不同。如果你拿交易所A在T1时刻的价格,去对比交易所B在T2时刻的价格,那算出来的价差毫无意义。

怎么做对齐?我常用的方法有两种:

  1. 时间戳对齐法:以统一的时间基准(比如NTP同步后的本地时间)为准,将两个交易所的Tick对齐到同一个时间窗口内。
  2. 事件驱动对齐法:当收到交易所A的Tick时,等待交易所B在极短时间窗口内(比如10ms)的最近一笔Tick,然后配对。

我个人更推荐第二种。为什么?因为时间戳对齐依赖交易所的时间戳精度,而不同交易所的时间戳可能偏差很大。事件驱动对齐更贴近实际交易场景。

💡 一个小技巧: 对齐窗口的大小不是固定的。我在项目中会根据网络延迟动态调整。如果网络波动大,窗口适当放宽;如果网络稳定,窗口缩到最小。

class TickAligner:
    def __init__(self, window_ms=10):
        self.window_ms = window_ms
        self.buffer = {'exchange_a': [], 'exchange_b': []}
    
    def on_tick(self, tick, exchange):
        self.buffer[exchange].append(tick)
        self._try_align()
    
    def _try_align(self):
        # 取两个交易所最近的一笔Tick
        a = self.buffer['exchange_a'][-1] if self.buffer['exchange_a'] else None
        b = self.buffer['exchange_b'][-1] if self.buffer['exchange_b'] else None
        
        if a and b:
            time_diff = abs(a['timestamp'] - b['timestamp'])
            if time_diff <= self.window_ms:
                # 对齐成功,计算价差
                spread = a['price'] - b['price']
                self.on_spread(spread)

4.4 整体架构:一张图说清楚

说了这么多,我们来看一张整体流程图。这是我做套利系统时常用的行情处理链路。

交易所 A 交易所 B 行情订阅 行情订阅 数据清洗 数据清洗 Tick 对齐引擎 对齐后的价差数据 图例: 数据源 订阅 清洗 对齐

从这张图你可以看到,整个链路是串行的。任何一个环节出问题,下游都会受影响。所以我一直强调:行情处理不是锦上添花,而是生死攸关

4.5 避坑指南

最后,分享几个我亲身踩过的坑:

  • 不要依赖交易所的时间戳。我曾经发现某交易所的时间戳比真实时间慢了整整2秒。从那以后,我所有对齐逻辑都基于本地NTP时间。
  • 行情断连要自动重连。WebSocket偶尔会断开,必须实现心跳检测和自动重连机制。我见过有人没做重连,策略在断连期间一直用旧数据交易,亏了十几万。
  • 数据清洗要记录日志。每次丢弃异常数据,都要记录原因。这样复盘时才能知道是交易所的问题还是自己代码的问题。

一句话总结: 行情处理的核心就三个字——快、准、稳。快是延迟低,准是数据干净,稳是系统可靠。三者缺一不可。

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