行情接口:WebSocket实时行情接入、REST API快照获取、数据解析与缓存

做量化交易,行情接口就是你的眼睛。

眼睛不好使,再牛的策略也是白搭。我个人习惯把行情接入分成三个层次:实时流、快照、本地缓存。今天咱们就把这三层彻底讲透。

一、为什么需要两套接口?

你想想看,WebSocket和REST API,一个像水龙头,一个像照相机。

WebSocket是长连接,数据主动推过来。适合做盘口实时更新,毫秒级延迟。但有个问题——万一断线重连,中间的数据就丢了。

REST API是一次性请求,拿到的是当前时刻的全量快照。适合做初始化、补数据、校验。但频繁请求会被交易所封IP。

所以实战中,我都是两者结合:

  • 启动时,先拉一次REST快照,拿到基准数据
  • 然后开启WebSocket,接收增量更新
  • 本地维护一个订单簿,用增量去更新快照

核心原则:WebSocket负责「快」,REST负责「准」。快准结合,才是合格的行情系统。

二、WebSocket实时行情接入

先说说WebSocket。这东西其实不复杂,就是一个全双工的TCP连接。

我早期踩过一个坑——直接用浏览器那套ws库,结果在Python异步环境里各种报错。后来换了websockets库,配合asyncio,才算稳定下来。

2.1 连接与心跳

交易所一般要求你定时发心跳包。比如每5秒发一个ping,服务器回一个pong。如果连续几次没收到,就该重连了。

import asyncio
import websockets
import json

async def subscribe_depth(symbol):
    uri = "wss://api.example.com/ws"
    async with websockets.connect(uri) as ws:
        # 订阅深度数据
        sub_msg = {
            "op": "subscribe",
            "args": [f"depth.{symbol}"]
        }
        await ws.send(json.dumps(sub_msg))
        
        while True:
            try:
                data = await asyncio.wait_for(ws.recv(), timeout=10)
                # 解析数据...
                process_depth(data)
            except asyncio.TimeoutError:
                # 超时了,发个ping
                await ws.send("ping")

我的经验:心跳超时设成10秒比较合理。太短了容易误判,太长了断线了都不知道。

2.2 数据格式与解析

交易所返回的深度数据,通常是这样的:

{
    "e": "depthUpdate",
    "s": "BTCUSDT",
    "b": [["50000.00", "1.5"], ["49999.00", "2.0"]],
    "a": [["50001.00", "0.8"], ["50002.00", "1.2"]]
}

b是买单(bids),a是卖单(asks)。每个数组里,第一个是价格,第二个是数量。

注意:这里给的是增量数据,不是全量。你需要用这些增量去更新本地的订单簿。

三、REST API快照获取

WebSocket启动之前,必须先拿一次快照。不然你都不知道当前盘口长什么样。

3.1 请求快照

import requests

def get_snapshot(symbol):
    url = f"https://api.example.com/api/v1/depth?symbol={symbol}&limit=100"
    resp = requests.get(url)
    data = resp.json()
    return data

limit参数一般可以设20、50、100。我建议设100,够用又不至于数据量太大。

注意:REST接口有频率限制。比如每秒最多5次。如果你同时监控几十个币种,记得加个限流器。我曾经因为没限流,被交易所封了IP整整一天。

3.2 快照与增量的合并

拿到快照后,怎么和WebSocket的增量合并?

说白了,就是维护一个本地的订单簿字典:

class OrderBook:
    def __init__(self, snapshot):
        self.bids = {price: qty for price, qty in snapshot['b']}
        self.asks = {price: qty for price, qty in snapshot['a']}
    
    def update(self, delta):
        # 更新买单
        for price, qty in delta['b']:
            if float(qty) == 0:
                self.bids.pop(price, None)
            else:
                self.bids[price] = float(qty)
        # 更新卖单
        for price, qty in delta['a']:
            if float(qty) == 0:
                self.asks.pop(price, None)
            else:
                self.asks[price] = float(qty)

注意:如果某个价格的数量变成0,说明这一档已经撤单了,要删掉。

四、数据解析与缓存

数据拿到手,不能直接用。得解析成你策略能吃的格式。

4.1 解析成标准结构

我个人习惯把盘口数据转成这样的结构:

{
    "symbol": "BTCUSDT",
    "timestamp": 1700000000000,
    "bids": [
        {"price": 50000.0, "qty": 1.5},
        {"price": 49999.0, "qty": 2.0}
    ],
    "asks": [
        {"price": 50001.0, "qty": 0.8},
        {"price": 50002.0, "qty": 1.2}
    ]
}

这样策略模块直接拿来用,不用关心底层是WebSocket还是REST。

4.2 本地缓存策略

缓存不是随便存一下就行。要考虑几个问题:

  • 内存占用:一个订单簿可能几千档,几十个币种同时监控,内存压力不小
  • 更新频率:毫秒级更新,不能每次都写磁盘
  • 断线恢复:程序重启后,能不能从缓存恢复?

我的做法是:

  • 内存里用字典维护最新订单簿
  • 每5秒把快照序列化后存到Redis
  • 程序启动时,先从Redis恢复,再拉REST快照做校验

避坑指南:Redis的key要带时间戳。比如orderbook:BTCUSDT:1700000000。这样即使缓存过期,也能找到最近的一份快照。

五、整体架构图

下面这张图,是我做行情系统时的标准架构。你看一眼就明白了。

交易所 WebSocket 实时流 REST API 快照 数据解析模块 本地缓存 (Redis) 本地订单簿 策略模块 实时数据流 快照数据 缓存数据

你看,整个流程很清晰:交易所同时提供WebSocket和REST两路数据,经过解析模块处理后,一路进缓存做持久化,一路进本地订单簿供策略实时使用。

六、实战中的几个坑

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

  1. WebSocket断线重连——一定要加指数退避。第一次等1秒,第二次2秒,第三次4秒...不然服务器会认为你在攻击它。
  2. 数据时间戳——交易所给的时间戳是服务器时间,和你本地时间可能有偏差。我习惯用交易所的时间戳,不用本地时间。
  3. 内存泄漏——订单簿如果只增不减,内存迟早爆掉。记得定期清理过期数据,比如只保留最近100档。
  4. 日志记录——每笔行情都打日志?别傻了,一秒几千条,磁盘直接写爆。我一般只记录异常情况和关键快照。

重要提醒:千万别在生产环境直接打印原始WebSocket报文。我见过有人这么干,结果一天下来日志文件几十个G,服务器直接挂了。

好了,行情接口这块就聊到这儿。记住一句话:WebSocket管快,REST管准,缓存管稳。三者配合,你的交易系统才能跑得又稳又快。


专注资料整理