第1章:WebSocket实时数据流——连接建立与心跳维持
做量化交易,最怕什么?
行情断流了,你还在那儿傻等。等发现的时候,仓位已经爆了。
我刚开始对接交易所API时,就吃过这个亏。那时候用的是轮询,每秒拉一次深度数据。结果有一次交易所网关抖动,连续5秒没返回数据,我的策略还在按旧价格下单……嗯,后果你们能想象。
从那以后,我彻底转向了WebSocket。今天咱们就聊聊,怎么把这套实时数据流玩明白。
1.1 WebSocket连接建立——别小看握手这一步
WebSocket本质上是一个TCP长连接。客户端先发一个HTTP Upgrade请求,服务端同意后,双方就建立了全双工通信通道。
听起来简单?但坑不少。
核心要点:连接建立时,必须校验服务端返回的HTTP状态码是否为101。如果不是,说明握手失败。
我见过不少新手,直接用第三方库的connect方法,连返回码都不看。结果连上了个假的代理服务器,数据全是错的。
代码示例(Python):
import websocket
import json
def on_open(ws):
print("连接已建立")
# 发送订阅消息
subscribe_msg = {
"op": "subscribe",
"args": ["btcusdt@depth20", "btcusdt@trade"]
}
ws.send(json.dumps(subscribe_msg))
def on_error(ws, error):
print(f"连接错误: {error}")
def on_close(ws, close_status_code, close_msg):
print(f"连接关闭: {close_status_code} - {close_msg}")
def on_message(ws, message):
data = json.loads(message)
print(f"收到数据: {data}")
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()
个人经验:我习惯在on_open回调里立即发送订阅消息,而不是等几秒再发。因为有些交易所的连接超时时间很短,你不发消息,它就把你踢了。
1.2 心跳维持——你不发,它就断
WebSocket连接建立后,不是永久有效的。网络中间设备(比如防火墙、NAT网关)会定期清理空闲连接。
所以,你需要定期发送心跳包,告诉服务端:“我还活着”。
不同交易所的心跳机制不一样。我整理了一个表格:
| 交易所 | 心跳方式 | 间隔时间 | 注意事项 |
|---|---|---|---|
| Binance | 发送ping帧(WebSocket原生) | 3分钟 | 服务端回复pong帧,无需额外处理 |
| OKX | 发送ping消息(JSON格式) | 15秒 | 必须等待pong响应,否则重连 |
| Bybit | 发送ping帧 | 20秒 | 如果连续3次无响应,主动断开重连 |
| Bitget | 发送ping消息 | 30秒 | 服务端可能不回复,需要自己计时 |
我曾经踩过的坑:有一次对接某小交易所,文档说心跳间隔是30秒。结果我设了30秒,连接总是断。后来抓包发现,他们的NAT设备15秒就清理空闲连接了。所以,我建议心跳间隔不要超过20秒,保守一点设10秒。
心跳代码实现:
import time
import threading
class HeartbeatManager:
def __init__(self, ws, interval=10):
self.ws = ws
self.interval = interval
self._running = False
self._thread = None
def start(self):
self._running = True
self._thread = threading.Thread(target=self._heartbeat_loop)
self._thread.daemon = True
self._thread.start()
def stop(self):
self._running = False
if self._thread:
self._thread.join(timeout=1)
def _heartbeat_loop(self):
while self._running:
try:
# 发送ping帧
self.ws.ping()
time.sleep(self.interval)
except Exception as e:
print(f"心跳发送失败: {e}")
break
1.3 订阅/取消订阅频道——别一股脑全订
订阅频道,说白了就是告诉交易所:“我要看哪些数据”。
但这里有个原则:按需订阅。你想想看,如果你把所有的交易对、所有的深度、所有的K线都订了,带宽和内存都扛不住。
我一般这样设计订阅策略:
- 深度数据:只订阅当前持仓和监控列表的交易对
- 交易数据:只订阅活跃交易对,冷门币种不订阅
- K线数据:只订阅策略需要的周期,比如1分钟和5分钟
订阅消息格式(以Binance为例):
{
"method": "SUBSCRIBE",
"params": [
"btcusdt@depth20",
"btcusdt@trade",
"btcusdt@kline_1m"
],
"id": 1
}
取消订阅:
{
"method": "UNSUBSCRIBE",
"params": [
"btcusdt@depth20"
],
"id": 2
}
我的习惯:每次订阅或取消订阅后,我都会检查服务端返回的确认消息。如果返回了错误码,说明订阅失败,需要重试。我曾经遇到过因为订阅参数写错了一个字母,结果一整天没收到数据……
1.4 数据帧解析——深度、交易、K线
数据来了,你得能看懂。不同交易所的数据格式大同小异,但细节上有差异。
深度数据(Order Book):
{
"e": "depthUpdate", // 事件类型
"E": 1672515782136, // 事件时间
"s": "BTCUSDT", // 交易对
"b": [ // 买单(bids)
["29500.00", "1.5"],
["29499.00", "2.0"]
],
"a": [ // 卖单(asks)
["29501.00", "0.8"],
["29502.00", "1.2"]
]
}
交易数据(Trade):
{
"e": "trade",
"E": 1672515782136,
"s": "BTCUSDT",
"p": "29500.00", // 价格
"q": "0.5", // 数量
"T": 1672515782136, // 交易时间
"m": true // 是否主动卖出
}
K线数据(Candlestick):
{
"e": "kline",
"E": 1672515782136,
"s": "BTCUSDT",
"k": {
"t": 1672515780000, // 开盘时间
"o": "29500.00", // 开盘价
"h": "29550.00", // 最高价
"l": "29450.00", // 最低价
"c": "29520.00", // 收盘价
"v": "1000.5" // 成交量
}
}
注意:深度数据是增量更新的。也就是说,每次推送的不是全量数据,而是变化的部分。你需要维护一个本地的Order Book,根据增量数据更新它。我刚开始没注意这点,直接覆盖了本地数据,结果深度全是错的。
1.5 重连机制设计——断了怎么办?
连接一定会断。网络抖动、交易所重启、你的服务器重启……原因太多了。
所以,重连机制是必须的。我设计重连时,遵循这几个原则:
- 指数退避:第一次重连等1秒,第二次等2秒,第三次等4秒……最大不超过60秒
- 重置计数器:如果成功连接并稳定运行超过5分钟,重置退避计数器
- 恢复订阅:重连后,自动重新订阅之前的所有频道
- 数据校验:重连后,检查数据的时间戳,如果发现数据断层,主动请求补数据
重连代码示例:
import time
import random
class ReconnectManager:
def __init__(self, max_retries=10, base_delay=1, max_delay=60):
self.max_retries = max_retries
self.base_delay = base_delay
self.max_delay = max_delay
self.retry_count = 0
self.stable_time = 0
def get_delay(self):
# 指数退避 + 随机抖动
delay = min(self.base_delay * (2 ** self.retry_count), self.max_delay)
jitter = random.uniform(0, 0.5)
return delay + jitter
def should_reconnect(self):
return self.retry_count < self.max_retries
def on_connect_success(self):
self.retry_count = 0
self.stable_time = time.time()
def on_connect_failure(self):
self.retry_count += 1
delay = self.get_delay()
print(f"第{self.retry_count}次重连,等待{delay:.2f}秒")
time.sleep(delay)
def check_stability(self):
# 如果稳定运行超过5分钟,重置计数器
if time.time() - self.stable_time > 300:
self.retry_count = 0
核心思想:重连不是简单的“断了就重连”,而是要有策略、有状态、有恢复。我见过有人写死循环重连,结果把交易所的网关打挂了……
知识体系总览
下面这张图,是我对本章内容的总结。你可以把它当作一个检查清单:
最后说一句:WebSocket对接,说白了就是“建连、保活、收数据、断重连”这四步。每一步都有坑,但只要你按照上面的方法一步步来,基本不会出大问题。
我见过太多人,代码写得很漂亮,但一上线就崩。为什么?因为没考虑网络异常。记住:在量化交易里,网络是不可靠的,你的代码必须可靠。
公众号:蓝海数据掘金营,微信deep3321