3. 实时数据源接入:WebSocket、Kafka、REST API,数据解析与缓存策略

做量化交易,最怕什么?

怕行情来了,你的数据还没到。或者更惨——数据到了,你解析错了。

今天咱们聊聊实时数据源接入。这个话题我做了快十年,踩过的坑能写一本小册子。WebSocket、Kafka、REST API,这三种方式我都在生产环境里用过。各有各的脾气,也各有各的坑。

核心观点:实时数据接入不是简单的「连上就行」。数据解析的准确性、缓存策略的合理性,直接决定了你的策略能不能赚钱。

3.1 三种数据源接入方式

先说说这三种方式。我个人习惯把它们分成两类:推模式拉模式

方式 模式 延迟 适用场景
WebSocket 毫秒级 实时行情、订单簿
Kafka 推/拉 亚毫秒级 内部数据总线、多消费者
REST API 秒级 历史数据、非实时查询

3.1.1 WebSocket:实时行情的首选

WebSocket 说白了就是一条长连接。服务器有数据了,直接推给你。不用你一遍遍去问「有数据了吗?」。

我在项目中遇到过一个问题:某交易所的 WebSocket 连接,每 30 秒会发一个 ping 帧。如果你没及时回复 pong,连接就被断开了。嗯,这个坑我踩过,当时策略跑得好好的,突然断连了,行情停了 3 分钟。

import asyncio
import websockets
import json

async def subscribe_market_data():
    uri = "wss://api.exchange.com/ws"
    async with websockets.connect(uri) as ws:
        # 订阅 BTC/USDT 的 ticker
        subscribe_msg = {
            "type": "subscribe",
            "channel": "ticker",
            "symbol": "BTC/USDT"
        }
        await ws.send(json.dumps(subscribe_msg))
        
        while True:
            try:
                data = await asyncio.wait_for(ws.recv(), timeout=10)
                # 解析数据
                tick = json.loads(data)
                process_tick(tick)
            except asyncio.TimeoutError:
                # 发送心跳
                await ws.send(json.dumps({"type": "ping"}))

避坑指南:我曾经因为没处理心跳超时,导致生产环境断连了 15 分钟。后来我加了一个 watchdog 机制:如果 5 秒内没收到任何数据,主动重连。

3.1.2 Kafka:内部数据总线的王者

Kafka 适合做内部的数据分发。你想想看,一个行情数据进来,可能要同时喂给风控模块、策略模块、日志模块。用 WebSocket 一个个推?太累了。

Kafka 的玩法是:生产者把数据扔到 topic 里,消费者自己去拉。我建议把行情数据按品种分区,比如 BTC 一个分区,ETH 一个分区。这样消费的时候可以并行处理。

from kafka import KafkaConsumer, KafkaProducer
import json

# 生产者:把行情数据发到 Kafka
producer = KafkaProducer(
    bootstrap_servers=['localhost:9092'],
    value_serializer=lambda v: json.dumps(v).encode('utf-8')
)

def publish_tick(tick_data):
    producer.send('market_data', 
                  key=tick_data['symbol'].encode(),
                  value=tick_data)

# 消费者:策略模块消费行情
consumer = KafkaConsumer(
    'market_data',
    bootstrap_servers=['localhost:9092'],
    group_id='strategy_group',
    auto_offset_reset='latest'
)

for msg in consumer:
    tick = json.loads(msg.value)
    # 处理行情
    update_strategy(tick)

注意:Kafka 的消费者组要设计好。同一个组内的消费者,一个分区只能被一个消费者消费。如果你有 4 个分区,最多只能有 4 个消费者并行处理。

3.1.3 REST API:历史数据的救星

REST API 适合拉取历史数据。比如你要回测,需要过去一年的分钟线。用 WebSocket 等?等到猴年马月。

但 REST API 有个问题:限流。交易所通常限制每秒多少次请求。我见过有人写了个 for 循环去拉数据,结果被交易所封了 IP。

import requests
import time
from ratelimit import limits, sleep_and_retry

@sleep_and_retry
@limits(calls=10, period=1)  # 每秒最多10次
def fetch_klines(symbol, interval, limit=500):
    url = f"https://api.exchange.com/v1/klines"
    params = {
        "symbol": symbol,
        "interval": interval,
        "limit": limit
    }
    resp = requests.get(url, params=params)
    if resp.status_code == 429:
        # 被限流了,等一会再试
        time.sleep(1)
        return fetch_klines(symbol, interval, limit)
    return resp.json()

3.2 数据解析策略

数据拿到了,怎么解析?

我个人习惯用 Protocol Buffers 或者 FlatBuffers。为什么?因为 JSON 解析太慢了。你想想看,每秒几千笔行情,每笔都做 json.loads,CPU 很快就满了。

格式 解析速度 数据大小 适用场景
JSON 调试、小规模
Protobuf 生产环境
FlatBuffers 极快 高频交易
# 使用 Protobuf 定义行情数据结构
syntax = "proto3";

message Tick {
    string symbol = 1;
    double price = 2;
    double volume = 3;
    int64 timestamp = 4;
    string exchange = 5;
}

# Python 中解析
import market_pb2

def parse_tick(raw_bytes):
    tick = market_pb2.Tick()
    tick.ParseFromString(raw_bytes)
    return tick

经验之谈:我曾经用 JSON 解析行情,CPU 占用率 70%。换成 Protobuf 后,直接降到 15%。这差距,你品,你细品。

3.3 缓存策略

数据解析完了,放哪?

我建议用 Redis 做缓存。为什么?因为快。而且 Redis 支持多种数据结构,非常适合存行情数据。

3.3.1 缓存什么?

  • 最新行情:用 Redis 的 String 类型,key 是 "tick:BTC/USDT",value 是序列化的行情数据
  • 订单簿:用 Redis 的 Sorted Set,score 是价格,member 是数量
  • K 线数据:用 Redis 的 List,左进右出,保持固定长度
import redis
import json

r = redis.Redis(host='localhost', port=6379, db=0)

# 缓存最新行情
def cache_tick(tick):
    key = f"tick:{tick['symbol']}"
    r.setex(key, 60, json.dumps(tick))  # 60秒过期

# 缓存订单簿
def cache_orderbook(symbol, bids, asks):
    key = f"orderbook:{symbol}"
    # 清空旧数据
    r.delete(key)
    # 添加买单
    for price, qty in bids:
        r.zadd(f"{key}:bids", {qty: price})
    # 添加卖单
    for price, qty in asks:
        r.zadd(f"{key}:asks", {qty: price})

3.3.2 缓存淘汰策略

缓存不能无限大。我建议用 LRU(最近最少使用)策略。Redis 默认就支持。

# 设置 Redis 最大内存和淘汰策略
# 在 redis.conf 中配置:
# maxmemory 1gb
# maxmemory-policy allkeys-lru

注意:缓存过期时间要设置合理。太短了,频繁查数据库;太长了,数据可能过时。我一般设 30-60 秒,具体看数据更新频率。

3.4 整体架构图

说了这么多,咱们画个图看看整体架构。

数据源层 WebSocket (实时行情) Kafka (内部数据总线) REST API (历史数据) 数据解析层 JSON 解析 Protobuf 解析 FlatBuffers 解析 缓存层 (Redis) 最新行情缓存 订单簿缓存 K线数据缓存 应用层 策略引擎 风控模块 日志系统

这张图展示了从数据源到应用的完整链路。数据从 WebSocket、Kafka、REST API 进来,经过解析层处理,缓存到 Redis,最后被策略引擎、风控模块等消费。

总结一下:

  • WebSocket 适合实时行情,记得处理心跳和重连
  • Kafka 适合内部数据分发,注意分区和消费者组设计
  • REST API 适合历史数据,注意限流
  • 数据解析用 Protobuf 或 FlatBuffers,别用 JSON 硬扛
  • 缓存用 Redis,设置合理的过期时间和淘汰策略

嗯,今天就聊到这。这些经验都是我用真金白银换来的,希望能帮你少走弯路。

蓝海数据掘金营,专注资料整理