2、订单簿数据获取:交易所API对接(REST/WebSocket)

做量化交易,第一道坎就是怎么把订单簿数据搞到手。

我刚开始做这个的时候,以为不就是调个接口嘛,简单。结果一上手就踩坑——数据对不上、时间戳乱跳、增量更新丢了几个事件,整个订单簿就歪了。嗯,今天咱们就把这块彻底讲透。

2.1 REST vs WebSocket:怎么选?

交易所通常提供两种接口:REST和WebSocket。说白了,一个是你主动去问,一个是交易所主动告诉你。

特性 REST WebSocket
数据获取方式 请求-响应 订阅-推送
延迟 高(100ms+) 低(1-10ms)
适合场景 历史数据、快照 实时行情、增量更新
连接开销 每次请求新建连接 长连接,持续推送

我个人习惯是:REST拿快照,WebSocket收增量。这样既保证了初始数据的完整性,又能拿到低延迟的实时更新。

小技巧: 启动时先用REST拉一次全量快照,然后订阅WebSocket增量流。这样本地订单簿就能快速对齐。

2.2 数据清洗与对齐

数据拿到手,别急着用。交易所返回的数据经常有坑。

我曾经遇到过一个情况:某交易所的卖一价突然跳了个负数。你想想看,价格还能是负的?明显是数据异常。所以清洗是必须的。

清洗规则我一般这么定:

  • 价格检查: 价格必须大于0,不能是NaN或Infinity
  • 数量检查: 数量必须大于等于0(有些交易所允许0表示撤单)
  • 时间戳检查: 时间戳不能是未来的,也不能比上次更新还早
  • 去重: 同一个价格位,如果连续收到相同数据,跳过
def clean_orderbook_update(data):
    """清洗订单簿增量数据"""
    cleaned = []
    for entry in data:
        price = float(entry['price'])
        size = float(entry['size'])
        # 价格异常直接扔掉
        if price <= 0 or price != price:  # NaN检查
            continue
        # 数量为0表示删除该价位
        if size < 0:
            continue
        cleaned.append({
            'price': price,
            'size': size,
            'side': entry['side'],
            'timestamp': entry['timestamp']
        })
    return cleaned
注意: 不同交易所的时间戳格式不一样。有的是毫秒,有的是微秒,还有的是字符串。对齐时一定要统一成一种格式,否则本地订单簿会乱套。

2.3 快照与增量更新

订单簿重建的核心逻辑就四个字:快照+增量

流程是这样的:

  1. 通过REST获取当前全量快照(bids和asks列表)
  2. 订阅WebSocket增量流,每条增量包含:价格、数量、方向(买/卖)
  3. 收到增量后,在本地订单簿中找到对应价格位,更新数量
  4. 如果数量为0,删除该价位

这里有个关键点:增量更新的顺序必须严格按时间戳执行。我见过有人因为多线程处理,导致增量乱序,订单簿直接崩了。

核心原则: 增量更新是幂等的。同一个增量重复应用两次,结果应该不变。这样才能应对网络重连后的数据补发。

2.4 本地订单簿重建

本地订单簿说白了就是一个有序字典。买盘按价格降序排列,卖盘按价格升序排列。

我一般用红黑树或者跳表来实现,但Python里直接用sortedcontainers库就够用了。

from sortedcontainers import SortedDict

class OrderBook:
    def __init__(self):
        self.bids = SortedDict(lambda k: -k)  # 买盘,价格降序
        self.asks = SortedDict()              # 卖盘,价格升序
    
    def apply_snapshot(self, snapshot):
        """应用全量快照"""
        self.bids.clear()
        self.asks.clear()
        for bid in snapshot['bids']:
            self.bids[float(bid[0])] = float(bid[1])
        for ask in snapshot['asks']:
            self.asks[float(ask[0])] = float(ask[1])
    
    def apply_update(self, update):
        """应用增量更新"""
        price = float(update['price'])
        size = float(update['size'])
        side = update['side']
        
        book = self.bids if side == 'buy' else self.asks
        
        if size == 0:
            # 删除该价位
            if price in book:
                del book[price]
        else:
            # 更新数量
            book[price] = size
    
    def get_top(self, n=10):
        """获取前n档行情"""
        top_bids = list(self.bids.items())[:n]
        top_asks = list(self.asks.items())[:n]
        return {'bids': top_bids, 'asks': top_asks}
避坑指南: 我曾经因为快照和增量之间的时间差,导致本地订单簿少了几个价位。解决办法是:收到快照后,把快照时间戳之后的所有增量重新应用一遍。这叫「快照对齐」。

2.5 整体架构图

下面这张图展示了订单簿数据获取的完整流程。从交易所到本地重建,每一步都不能少。

订单簿数据获取与重建流程 交易所 REST 全量快照 WebSocket 增量流 数据清洗 数据清洗 本地订单簿重建 实时订单簿数据

你看,整个流程其实不复杂。但每个环节都有细节要注意。数据清洗别偷懒,增量顺序别搞乱,快照对齐别忘记。做到这三点,本地订单簿基本就稳了。

总结一下: 订单簿数据获取的核心就是「REST拿快照,WebSocket收增量,本地做合并」。这套模式几乎所有交易所都通用,学会了就能举一反三。

公众号:蓝海资料掘金营,微信deep3321