第二章:交易所数据源解析——WebSocket连接、REST API快照、增量数据流

做量化交易,第一道坎就是怎么把交易所的数据拿过来。

你可能觉得,不就是连个API嘛,有什么难的?

嗯,我刚开始也这么想。直到有一次,我的订单簿在盘中突然出现了一个巨大的价差缺口——买单和卖单之间差了整整两个tick。当时我冷汗都下来了,赶紧查日志,发现是增量数据丢了一条。

从那以后,我对数据源解析这件事,再也不敢掉以轻心。

2.1 数据源的整体架构

交易所的数据源,说白了就两种模式:

  • REST API:你主动去问,交易所给你一个快照。适合拿全量数据,但实时性差。
  • WebSocket:交易所主动推给你。适合拿增量数据,实时性好。

实际生产中,我们通常把两者结合起来用。我习惯叫它「快照+增量」模式。

核心思路:先用REST拿一个完整的订单簿快照,然后通过WebSocket接收后续的增量更新,在本地重建并维护一个实时订单簿。

为什么不能只用WebSocket?

因为WebSocket连接可能会断。断连之后,你本地维护的订单簿就和交易所的不一致了。这时候就需要重新拿一次快照,再继续接增量。

交易所数据源架构:快照 + 增量模式 交易所 REST API 快照 请求快照 WebSocket 增量 推送增量 本地订单簿重建 快照 + 增量 → 实时订单簿 全量数据 实时流 策略使用

2.2 WebSocket连接:实时性的基石

WebSocket这东西,说白了就是一个长连接。建立之后,交易所会持续往你这边推数据。

我个人习惯用 websocket-client 这个库,Python环境下很稳定。

import websocket
import json

def on_message(ws, message):
    data = json.loads(message)
    # 处理增量数据
    process_increment(data)

def on_error(ws, error):
    print(f"WebSocket错误: {error}")

def on_close(ws, close_status_code, close_msg):
    print("WebSocket连接已关闭")
    # 自动重连逻辑
    reconnect()

def on_open(ws):
    print("WebSocket连接已建立")
    # 订阅订单簿增量
    subscribe_msg = {
        "op": "subscribe",
        "args": ["orderbook.BTC-USDT"]
    }
    ws.send(json.dumps(subscribe_msg))

ws = websocket.WebSocketApp(
    "wss://api.exchange.com/ws",
    on_open=on_open,
    on_message=on_message,
    on_error=on_error,
    on_close=on_close
)
ws.run_forever()

避坑指南:我曾经在生产环境遇到过WebSocket连接被防火墙无故断开的情况。后来加了心跳检测和自动重连机制,才彻底解决。建议每30秒发一次ping,如果60秒内没收到pong,就主动重连。

2.3 REST API快照:重建的锚点

快照就是某一时刻订单簿的全量数据。你想想看,如果没有快照,只靠增量,万一中间丢了一条,后面的数据就全歪了。

所以快照的作用,就是给你一个「锚点」。

import requests

def get_orderbook_snapshot(symbol="BTC-USDT"):
    url = f"https://api.exchange.com/v1/orderbook/{symbol}"
    params = {"depth": 100}  # 取前100档
    
    resp = requests.get(url, params=params, timeout=5)
    data = resp.json()
    
    # 快照结构示例
    snapshot = {
        "bids": data["bids"],  # [[价格, 数量], ...]
        "asks": data["asks"],
        "timestamp": data["ts"],
        "sequence": data["seq"]  # 序列号,用于对齐增量
    }
    return snapshot

注意:快照请求不要太频繁。我见过有人每秒请求一次快照,结果被交易所封了IP。一般每5-10分钟请求一次就够了,或者只在断连后重建时请求。

2.4 增量数据流:实时更新的血液

增量数据,就是交易所告诉你「刚才发生了什么变化」。

比如有人下了一笔买单,交易所会推一条消息:

{
    "type": "increment",
    "symbol": "BTC-USDT",
    "bids": [["50000.00", "1.5"]],  // 新增或更新的买单
    "asks": [],
    "sequence": 12345678
}

这里有个关键点:增量的更新方式是替换,不是累加

什么意思呢?如果某个价格档位已经有挂单了,增量里来了同价格的新数据,那就直接覆盖掉旧数据。如果数量为0,就表示这个档位被撤单了,要删掉。

2.5 订单簿重建:把碎片拼成完整画面

好了,现在我们有快照和增量了。怎么拼起来?

我一般用红黑树或者跳表来维护订单簿,但为了演示,这里用Python的字典加排序:

class OrderBook:
    def __init__(self):
        self.bids = {}  # 价格 -> 数量
        self.asks = {}
        self.last_seq = 0
    
    def apply_snapshot(self, snapshot):
        """应用快照"""
        self.bids.clear()
        self.asks.clear()
        
        for price, qty in snapshot["bids"]:
            self.bids[float(price)] = float(qty)
        for price, qty in snapshot["asks"]:
            self.asks[float(price)] = float(qty)
        
        self.last_seq = snapshot["sequence"]
        print(f"快照已应用,序列号: {self.last_seq}")
    
    def apply_increment(self, inc):
        """应用增量"""
        # 检查序列号连续性
        if inc["sequence"] != self.last_seq + 1:
            print(f"警告:序列号不连续!期望 {self.last_seq + 1},实际 {inc['sequence']}")
            # 这里应该触发重新获取快照
            return False
        
        # 处理买单增量
        for price, qty in inc.get("bids", []):
            price_f = float(price)
            qty_f = float(qty)
            if qty_f == 0:
                self.bids.pop(price_f, None)  # 删除
            else:
                self.bids[price_f] = qty_f  # 更新或新增
        
        # 处理卖单增量
        for price, qty in inc.get("asks", []):
            price_f = float(price)
            qty_f = float(qty)
            if qty_f == 0:
                self.asks.pop(price_f, None)
            else:
                self.asks[price_f] = qty_f
        
        self.last_seq = inc["sequence"]
        return True
    
    def get_top_n(self, n=10):
        """获取前n档买卖盘口"""
        sorted_bids = sorted(self.bids.items(), reverse=True)[:n]
        sorted_asks = sorted(self.asks.items())[:n]
        return sorted_bids, sorted_asks

关键检查点:每次应用增量前,一定要检查序列号的连续性。我曾经因为没做这个检查,导致订单簿数据错乱了整整3分钟,那笔损失至今记忆犹新。

2.6 实战中的几个坑

问题 表现 解决方案
序列号跳跃 增量丢失,订单簿偏差 检测到跳跃后立即重新获取快照
WebSocket断连 收不到增量 自动重连 + 重连后重新拿快照
快照与增量时序错乱 快照拿到后,增量已经包含了快照之前的变化 快照请求时记录时间戳,丢弃该时间戳之前的增量
数据延迟 增量到达时间晚于实际成交时间 在策略中引入时间戳比对,标记延迟数据

嗯,这里要注意一点:不同交易所的数据格式差异很大。有的用数组,有的用对象,有的序列号是字符串,有的是数字。我建议在解析层就统一成标准格式,不要让上层策略感知到这些差异。

说白了,数据源解析这件事,80%的工作都是在处理边界情况和异常恢复。正常流程写起来很简单,但真正考验功力的,是那些「万一」的情况。

我个人习惯在开发阶段就模拟各种异常场景:断网、数据乱序、重复推送、空数据……把这些都测一遍,上线后才睡得着觉。

一个小技巧:在本地维护一个「数据健康度」指标,比如每秒收到的增量条数、快照与增量的序列号差值。一旦指标异常,立刻报警。我靠这个提前发现过好几次交易所的API故障。


公众号:蓝海数据掘金营,微信deep3321