数据源接入:WebSocket实时行情接入、REST API历史数据获取、数据清洗与对齐

做价差回归策略,数据是命根子。这话一点都不夸张。

我见过太多人,策略逻辑写得漂漂亮亮,结果一上线就亏钱。为什么?数据源没处理好。要么实时行情断流了,要么历史数据有坑,要么两个合约的时间戳对不上。今天咱们就把这块硬骨头啃下来。

实时行情接入:WebSocket 才是王道

做实时监控,千万别用 REST API 去轮询。为什么?延迟高、浪费带宽、还容易被交易所封IP。

我个人习惯,所有实时数据都用 WebSocket。它就像一根管道,交易所那边有数据变化,直接推过来,我们这边被动接收就行。

核心要点:WebSocket 建立一次连接,后续数据全走这条通道。延迟通常在 10ms 以内,比 REST 轮询快一个数量级。

下面是我常用的 WebSocket 接入模板。以币安为例,订阅两个合约的深度数据:

import websocket
import json
import threading

class RealTimeFeed:
    def __init__(self, symbols):
        self.symbols = symbols
        self.ws = None
        self.latest_data = {}
        
    def on_message(self, ws, message):
        data = json.loads(message)
        # 解析数据,提取买一卖一价
        symbol = data['s']
        bid = float(data['b'])
        ask = float(data['a'])
        self.latest_data[symbol] = {
            'bid': bid,
            'ask': ask,
            'mid': (bid + ask) / 2,
            'timestamp': data['E']
        }
        
    def on_error(self, ws, error):
        print(f"WebSocket 出错了: {error}")
        # 自动重连逻辑
        self.reconnect()
        
    def start(self):
        url = "wss://stream.binance.com:9443/ws"
        streams = "/".join([f"{s.lower()}@bookTicker" for s in self.symbols])
        full_url = f"{url}/{streams}"
        
        self.ws = websocket.WebSocketApp(
            full_url,
            on_message=self.on_message,
            on_error=self.on_error
        )
        
        # 跑在独立线程里
        wst = threading.Thread(target=self.ws.run_forever)
        wst.daemon = True
        wst.start()

# 使用示例
feed = RealTimeFeed(['BTCUSDT', 'ETHUSDT'])
feed.start()

避坑指南:我曾经在项目里忘了加自动重连,结果半夜交易所重启,我的策略傻等了一晚上数据。记住,WebSocket 一定要有重连机制,最好加上指数退避。

历史数据获取:REST API 的优雅用法

实时数据有了,但策略回测、参数优化还得靠历史数据。REST API 虽然慢,但胜在稳定、数据全。

这里有个坑:很多交易所的 REST API 有频率限制。你想想看,如果一次请求只能拿 500 根 K 线,要拿三年数据得发多少请求?

我的做法是分片拉取,然后本地缓存:

import requests
import time
from datetime import datetime, timedelta

def fetch_historical_klines(symbol, interval, start_time, end_time):
    """
    分片拉取历史K线数据
    """
    base_url = "https://api.binance.com/api/v3/klines"
    limit = 500  # 每次最多500根
    
    all_klines = []
    current_start = start_time
    
    while current_start < end_time:
        params = {
            'symbol': symbol,
            'interval': interval,
            'startTime': int(current_start.timestamp() * 1000),
            'limit': limit
        }
        
        resp = requests.get(base_url, params=params)
        if resp.status_code != 200:
            print(f"请求失败: {resp.status_code}")
            time.sleep(1)
            continue
            
        klines = resp.json()
        if not klines:
            break
            
        all_klines.extend(klines)
        
        # 更新起始时间
        last_kline_time = klines[-1][0]
        current_start = datetime.fromtimestamp(last_kline_time / 1000)
        
        # 礼貌性等待
        time.sleep(0.1)
        
    return all_klines

# 获取最近30天的1小时K线
end = datetime.now()
start = end - timedelta(days=30)
data = fetch_historical_klines('BTCUSDT', '1h', start, end)

注意:不同交易所的 K 线数据结构不一样。币安返回的是数组,OKX 返回的是对象。写代码前一定先看文档,别想当然。

数据清洗与对齐:最容易被忽视的环节

数据拿到手了,你以为就能直接用了吗?太天真了。

我遇到过的情况:两个交易所的 BTC 永续合约,一个用 UTC 时间,一个用本地时间。直接算价差?算出来的全是错的。

数据清洗,说白了就三件事:

  1. 去重:WebSocket 偶尔会推送重复数据,用时间戳去重
  2. 补缺失:某个时间点数据没到,用前一个值填充
  3. 对齐时间戳:把两个合约的数据对齐到同一个时间轴上

下面是我常用的对齐函数:

import pandas as pd
import numpy as np

def align_spread_data(data1, data2, freq='1s'):
    """
    将两个合约的数据对齐到同一时间轴
    """
    # 转成 DataFrame
    df1 = pd.DataFrame(data1).set_index('timestamp')
    df2 = pd.DataFrame(data2).set_index('timestamp')
    
    # 重采样到固定频率
    df1_resampled = df1['mid'].resample(freq).last().ffill()
    df2_resampled = df2['mid'].resample(freq).last().ffill()
    
    # 合并
    aligned = pd.DataFrame({
        'symbol1': df1_resampled,
        'symbol2': df2_resampled
    })
    
    # 计算价差
    aligned['spread'] = aligned['symbol1'] - aligned['symbol2']
    
    # 去掉两边有空值的行
    aligned = aligned.dropna()
    
    return aligned

# 使用示例
aligned_data = align_spread_data(
    feed.latest_data.get('BTCUSDT'),
    feed.latest_data.get('ETHUSDT'),
    freq='1s'
)

整体架构:一张图说清楚

上面讲了三个模块,它们怎么配合?我画了张图,你看一眼就明白了:

数据源接入整体架构 WebSocket 实时行情 延迟 < 10ms REST API 历史数据 分片拉取 + 本地缓存 数据清洗与对齐模块 去重 → 补缺失 → 时间戳对齐 → 价差计算 对齐后的价差数据流 供策略引擎消费 数据源层 处理层 输出层

你看,整个流程其实很清晰。左边是实时数据,右边是历史数据,中间经过清洗对齐,最后输出干净的价差序列。

我的经验:数据对齐这一步,千万别用循环去逐条比对。用 pandas 的 resample 和 merge,一行代码顶你写 50 行循环。性能差几十倍。

实战中的几个坑

最后分享几个我踩过的坑,你遇到了能少走弯路:

  • 时间戳单位不一致:有的交易所用毫秒,有的用微秒,还有的用秒。统一转成毫秒再处理。
  • 节假日数据缺失:某些合约在节假日不交易,数据会断档。用 ffill 填充时要小心,别把几小时前的价格当成当前价。
  • WebSocket 断线重连:别只重连一次。我一般重连 5 次,每次间隔递增,还不行就发告警。
  • REST API 限频:有些交易所的限频是每分钟 1200 次,算好你的请求量,别超了被拉黑。

数据源这块,说白了就是「接进来、洗干净、对齐好」。这三步做好了,你的策略就成功了一半。剩下的,就是策略逻辑的事了。