3. 数据获取:WebSocket实时行情订阅与REST API历史数据抓取

做价差套利,数据就是你的眼睛。没有数据,策略再好也是瞎蒙。

我个人习惯把数据获取分成两条线:实时行情历史数据。前者用来发现机会,后者用来回测验证。两条线缺一不可。

3.1 为什么需要两套数据接口?

你想想看,套利机会往往转瞬即逝。比如币安和OKX之间的价差,可能几秒钟就消失了。这时候你需要WebSocket——它像一根水管,源源不断地把最新价格推送给你。

但光有实时数据不够。你要回测策略,要分析历史价差规律,就得靠REST API去拉历史K线。我在项目中遇到过一个问题:只用WebSocket攒数据,结果程序重启后历史数据全丢了,回测根本没法做。

所以我的建议是:WebSocket负责“现在”,REST API负责“过去”。两者配合,才能构建完整的数据体系。

核心原则:实时数据用推(WebSocket),历史数据用拉(REST API)。不要混用,更不要只用一种。

3.2 WebSocket实时行情订阅

WebSocket说白了就是一条长连接。客户端连上服务器后,服务器有数据就主动推过来。你不用反复去问“有新数据吗?”,省带宽,也省时间。

3.2.1 连接与订阅

以币安为例,订阅BTC/USDT的深度行情,代码大概长这样:

import websocket
import json

def on_message(ws, message):
    data = json.loads(message)
    # 处理实时行情数据
    print(f"最新价: {data['c']}")

def on_error(ws, error):
    print(f"连接出错: {error}")

def on_close(ws, close_status_code, close_msg):
    print("连接已关闭")

def on_open(ws):
    # 订阅深度行情
    subscribe_msg = {
        "method": "SUBSCRIBE",
        "params": ["btcusdt@depth20"],
        "id": 1
    }
    ws.send(json.dumps(subscribe_msg))

websocket.enableTrace(False)
ws = websocket.WebSocketApp(
    "wss://stream.binance.com:9443/ws",
    on_open=on_open,
    on_message=on_message,
    on_error=on_error,
    on_close=on_close
)
ws.run_forever()

嗯,这里要注意:订阅的频道不要太多。我曾经一次性订阅了20个交易对的深度数据,结果本地带宽被打满,程序直接卡死。后来我改成只订阅价差最大的几个交易对,问题就解决了。

3.2.2 数据清洗与缓存

WebSocket推送的数据很原始。你需要做两件事:

  • 去重:同一笔成交可能推送多次,用时间戳+交易对做唯一键去重
  • 缓存:用队列暂存数据,防止处理速度跟不上推送速度
我的小技巧:用Python的deque做环形缓冲区,设置最大长度1000。这样既能缓存最新数据,又不会内存爆炸。

3.3 REST API历史数据抓取

历史数据用来做回测。没有历史数据,你根本不知道策略在过去的市场里表现如何。

3.3.1 请求频率限制

交易所都有频率限制。币安REST API的限制是每分钟1200次请求。你如果一股脑发请求,很快就会被封IP。

我建议的做法是:用时间窗口控制请求频率。比如每秒钟最多发20次请求,超了就等一秒再发。

import time
import requests

class RateLimiter:
    def __init__(self, max_calls, period):
        self.max_calls = max_calls
        self.period = period
        self.calls = []

    def wait_if_needed(self):
        now = time.time()
        # 移除过期的调用记录
        self.calls = [t for t in self.calls if now - t < self.period]
        if len(self.calls) >= self.max_calls:
            sleep_time = self.calls[0] + self.period - now
            if sleep_time > 0:
                time.sleep(sleep_time)
        self.calls.append(time.time())

limiter = RateLimiter(max_calls=20, period=1)

def fetch_klines(symbol, interval, limit=500):
    limiter.wait_if_needed()
    url = "https://api.binance.com/api/v3/klines"
    params = {
        "symbol": symbol,
        "interval": interval,
        "limit": limit
    }
    resp = requests.get(url, params=params)
    return resp.json()
踩坑提醒:我曾经没做频率限制,结果币安把我的IP封了24小时。那段时间策略完全停摆,损失不小。所以频率限制不是可选项,是必选项。

3.3.2 数据存储策略

历史数据拉下来后,存哪里?我个人推荐用本地CSV文件+数据库双保险。

  • CSV文件:方便人工查看和调试
  • SQLite/MySQL:方便程序快速查询和回测

存储结构建议这样:

字段 类型 说明
symbol VARCHAR 交易对,如BTCUSDT
open_time BIGINT 开盘时间戳(毫秒)
open DECIMAL 开盘价
high DECIMAL 最高价
low DECIMAL 最低价
close DECIMAL 收盘价
volume DECIMAL 成交量

3.4 数据获取整体架构

下面这张图是我自己项目里用的数据获取架构,你可以参考一下:

数据获取整体架构 交易所A 交易所B 交易所C 数据获取层 WebSocket实时订阅 + REST API历史抓取 频率控制 | 数据清洗 | 去重缓存 数据存储层 CSV文件(调试) + SQLite/MySQL(回测) 交易所 获取 存储

这个架构看起来简单,但实际跑起来坑不少。我刚开始做的时候,WebSocket断连了都不知道,等发现时已经错过了好几波行情。后来我加了心跳检测和自动重连机制,才算稳定下来。

3.5 避坑指南

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

  • WebSocket断连:交易所会定期断开空闲连接。一定要实现自动重连,不然数据流就断了
  • 数据时间对齐:不同交易所的K线时间戳可能差几毫秒。做价差计算时,一定要先对齐时间
  • 磁盘空间:历史数据越攒越多。我建议定期清理,只保留最近3个月的数据用于回测
我的经验:数据获取是整个套利系统的基础。基础不牢,地动山摇。花80%的精力把数据搞稳定了,后面策略开发会轻松很多。

专注资料整理