数据采集实战:使用WebSocket订阅实时Tick流

好,咱们直接进入正题。

上一章我们聊了Tick数据长什么样,这一章就来真的——怎么把数据从交易所拉下来。我个人习惯用WebSocket,原因很简单:快,而且省资源。你想想看,如果用HTTP轮询,每秒请求几十次,服务器不封你才怪。

为什么选WebSocket?

说白了,WebSocket就是一条长连接。建立一次连接,后面数据自己推过来。不像HTTP,每次都要握手、发请求、等响应、再断开。我在项目中遇到过,用HTTP轮询1秒拉10次,带宽占用直接飙到3MB/s。换成WebSocket后,降到200KB/s不到。

核心优势:

  • 全双工通信:服务器可以主动推数据
  • 低延迟:建立连接后,数据延迟在毫秒级
  • 节省带宽:没有HTTP头部冗余

WebSocket连接的生命周期

嗯,这里要注意。WebSocket不是连上就完事了。它有几个关键状态:

  1. 连接建立:客户端发起握手,服务器返回101状态码
  2. 数据交换:双方可以随时发送消息
  3. 心跳维持:每隔一段时间发ping/pong,防止连接断开
  4. 连接关闭:任意一方发起关闭帧

我曾经踩过一个坑:连接建立后没做心跳检测,结果半夜网络波动,连接断了都不知道。第二天一看,数据停了6个小时。从那以后,我所有WebSocket客户端都加了心跳机制。

实战:用Python订阅Binance的Tick流

咱们拿币安(Binance)的公开WebSocket接口做例子。为什么选它?因为文档清晰,而且不需要API Key就能订阅实时数据。

import websocket
import json
import threading
import time

# 回调函数:收到消息时触发
def on_message(ws, message):
    data = json.loads(message)
    # 解析Tick数据
    if 'e' in data and data['e'] == '24hrTicker':
        symbol = data['s']
        price = data['c']
        volume = data['v']
        print(f"{symbol} 最新价: {price}, 成交量: {volume}")

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

# 连接关闭
def on_close(ws, close_status_code, close_msg):
    print("连接已关闭,尝试重连...")
    # 这里可以加自动重连逻辑

# 连接建立
def on_open(ws):
    print("连接成功!")
    # 订阅BTC/USDT的24小时Ticker
    subscribe_msg = {
        "method": "SUBSCRIBE",
        "params": ["btcusdt@ticker"],
        "id": 1
    }
    ws.send(json.dumps(subscribe_msg))

# 启动WebSocket
def start_websocket():
    ws_url = "wss://stream.binance.com:9443/ws"
    ws = websocket.WebSocketApp(ws_url,
                                on_open=on_open,
                                on_message=on_message,
                                on_error=on_error,
                                on_close=on_close)
    # 运行连接(阻塞)
    ws.run_forever()

# 启动
if __name__ == "__main__":
    start_websocket()

小技巧:如果你需要同时订阅多个交易对,可以在subscribe_msg的params列表里加多个参数,比如["btcusdt@ticker", "ethusdt@ticker"]。但注意,不要超过1024个,否则交易所会断开连接。

数据解析:从JSON到结构化数据

WebSocket收到的数据是JSON字符串。我们需要把它解析成Python对象。我个人习惯用dataclass来定义Tick数据结构:

from dataclasses import dataclass
from typing import Optional
import json

@dataclass
class TickData:
    symbol: str          # 交易对
    price: float         # 最新价
    volume: float        # 成交量
    high: float          # 24小时最高价
    low: float           # 24小时最低价
    open: float          # 24小时开盘价
    close: float         # 24小时收盘价
    timestamp: int       # 事件时间戳

def parse_tick(json_str: str) -> Optional[TickData]:
    try:
        data = json.loads(json_str)
        if 'e' not in data or data['e'] != '24hrTicker':
            return None
        
        return TickData(
            symbol=data['s'],
            price=float(data['c']),
            volume=float(data['v']),
            high=float(data['h']),
            low=float(data['l']),
            open=float(data['o']),
            close=float(data['c']),
            timestamp=data['E']
        )
    except Exception as e:
        print(f"解析失败: {e}")
        return None

注意:交易所返回的字段名可能缩写得很厉害。比如Binance的"c"代表close price,"v"代表volume。一定要对照文档确认每个字段的含义。我刚开始做的时候,把"c"当成了change,结果算出来的收益率全是错的。

心跳与重连机制

WebSocket连接不是永久的。网络波动、服务器维护、防火墙超时,都可能导致连接断开。所以,一个健壮的客户端必须包含心跳和重连逻辑。

我曾经在实盘交易中遇到过:凌晨3点,交易所做了一次小版本升级,所有WebSocket连接被强制断开。我的客户端没有重连机制,结果策略在无数据的情况下运行了2小时,亏了6位数。从那以后,我写WebSocket客户端必加以下三样:

  • 心跳检测:每30秒发一次ping,如果60秒内没收到pong,判定连接断开
  • 自动重连:断开后等待5秒,然后重新建立连接
  • 数据连续性检查:重连后检查是否有数据缺失,如果有,从REST API补数据
class ReconnectingWebSocket:
    def __init__(self, url, max_retries=10):
        self.url = url
        self.max_retries = max_retries
        self.retry_count = 0
        self.ws = None
        
    def connect(self):
        self.ws = websocket.WebSocketApp(
            self.url,
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close
        )
        # 启动心跳线程
        self.heartbeat_thread = threading.Thread(target=self.heartbeat_loop)
        self.heartbeat_thread.daemon = True
        self.heartbeat_thread.start()
        
        # 阻塞运行
        self.ws.run_forever()
    
    def heartbeat_loop(self):
        while True:
            time.sleep(30)
            if self.ws and self.ws.sock:
                try:
                    self.ws.send(json.dumps({"method": "PING"}))
                except:
                    print("心跳发送失败")
                    break
    
    def on_close(self, ws, code, msg):
        print(f"连接关闭: {code} - {msg}")
        if self.retry_count < self.max_retries:
            wait_time = min(5 * (2 ** self.retry_count), 60)
            print(f"等待 {wait_time} 秒后重连...")
            time.sleep(wait_time)
            self.retry_count += 1
            self.connect()

数据存储:别让数据丢了

WebSocket收到的数据是流式的,如果不存下来,一旦程序重启,数据就没了。我建议至少做两级存储:

存储层级 用途 推荐方案
内存缓冲区 实时计算用,延迟最低 deque(固定长度)或RingBuffer
磁盘持久化 历史回测用,防止丢失 CSV文件或Parquet
数据库 长期存储,方便查询 InfluxDB(时序数据库)或ClickHouse

我个人习惯的做法是:内存里用deque存最近1000条Tick,同时每收到一条就追加写入CSV文件。这样既保证了实时计算的速度,又不会丢数据。

本章知识体系

下面这张图总结了WebSocket订阅Tick流的完整流程:

WebSocket订阅Tick流核心流程 1. 建立WebSocket连接 2. 发送订阅消息 3. 接收实时Tick数据 4. JSON解析为结构化数据 5. 内存缓冲 + 磁盘持久化 心跳检测(每30秒) 自动重连(指数退避) 异常捕获与日志记录

避坑指南

最后,分享几个我踩过的坑,希望能帮你省点时间:

  • 不要在生产环境用单线程:WebSocket的on_message回调是单线程的,如果处理逻辑太慢,会阻塞后续消息。建议用异步队列,把数据丢到另一个线程处理。
  • 注意数据乱序:WebSocket不保证消息顺序。如果对顺序敏感,每条消息带时间戳,自己排序。
  • 小心交易所的限流:有些交易所对WebSocket订阅数量有限制。比如Binance单个连接最多订阅1024个stream。超了会被断开。
  • 本地时间与服务器时间:Tick数据的时间戳是交易所服务器时间。不要用本地时间做计算,否则回测时会出问题。

我的建议:刚开始做的时候,先订阅一个交易对,把整个流程跑通。确认数据能正常接收、解析、存储后,再慢慢增加订阅数量。一口吃不成胖子,WebSocket调试起来比HTTP麻烦得多。

好了,这一章的内容就到这里。代码可以直接复制去跑,但记得把交易对换成你需要的。下一章我们会聊怎么用这些Tick数据做实时统计计算——嗯,那才是真正有意思的部分。


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