第1章:实时数据流接入

做市系统里,数据流就是血液。没有实时、干净的数据,再牛的策略也是白搭。这一章,咱们就聊聊怎么把行情数据从交易所拉回来,再洗干净,喂给我们的库存计算引擎。

1.1 WebSocket行情接入

我个人习惯,做市系统的行情接入首选WebSocket。为什么?因为它轻量、全双工,延迟低。比起轮询HTTP接口,WebSocket能省下不少带宽和CPU。

我记得刚开始做这个项目时,团队里有人提议用TCP长连接自己解析协议。我直接否了——没必要重复造轮子。交易所基本都支持WebSocket,咱们直接用现成的库就行。

1.1.1 连接管理

连接WebSocket,核心就三步:握手、订阅、心跳。

import asyncio
import websockets

async def connect_exchange():
    uri = "wss://api.exchange.com/ws"
    async with websockets.connect(uri) as ws:
        # 订阅行情
        subscribe_msg = {
            "type": "subscribe",
            "channel": "orderbook",
            "symbol": "BTC-USDT"
        }
        await ws.send(json.dumps(subscribe_msg))
        
        # 持续接收数据
        async for message in ws:
            process_message(message)

这里有个坑:交易所的WebSocket连接经常断。我遇到过最夸张的一次,某交易所一天断了七八次。所以,重连机制必须做。

避坑指南:我曾经因为没做指数退避重连,导致系统在行情剧烈波动时反复重连,把交易所API打挂了。后来加了随机延迟和最大重连次数限制,才稳定下来。

1.1.2 心跳保活

很多交易所要求客户端定期发心跳。一般是每15-30秒发一次。如果超过一定时间没收到心跳,服务器就会断开连接。

async def heartbeat(ws, interval=15):
    while True:
        await asyncio.sleep(interval)
        try:
            await ws.send(json.dumps({"type": "ping"}))
        except:
            break

1.2 交易所API对接

不同交易所的API风格差异很大。有的用RESTful,有的用WebSocket,还有的混合使用。我建议统一封装一层,屏蔽底层差异。

1.2.1 统一接口设计

我一般会定义一个抽象基类,所有交易所都继承它:

class ExchangeAPI(ABC):
    @abstractmethod
    async def subscribe_orderbook(self, symbol: str):
        pass
    
    @abstractmethod
    async def subscribe_trade(self, symbol: str):
        pass
    
    @abstractmethod
    async def get_account_info(self) -> dict:
        pass

这样,上层策略代码就不用关心具体连的是哪个交易所。换交易所时,只需要换一个实现类就行。

1.2.2 鉴权与签名

交易接口通常需要鉴权。常见的做法是API Key + Secret Key,加上时间戳和签名。签名算法各家不同,但核心逻辑差不多:

def sign_request(params: dict, secret: str) -> str:
    sorted_params = sorted(params.items())
    query_string = "&".join(f"{k}={v}" for k, v in sorted_params)
    signature = hmac.new(
        secret.encode(), 
        query_string.encode(), 
        hashlib.sha256
    ).hexdigest()
    return signature
小技巧:签名时一定要把时间戳带上,防止重放攻击。我习惯用毫秒级时间戳,精度够用。

1.3 数据格式解析

行情数据到了,但格式五花八门。JSON最常见,也有用Protocol Buffers的。解析速度直接影响系统延迟。

1.3.1 JSON解析优化

Python自带的json库够用,但性能一般。我推荐用orjson或ujson,速度能快3-5倍。

import orjson

def parse_orderbook(raw: bytes) -> dict:
    # orjson直接解析bytes,省去decode步骤
    data = orjson.loads(raw)
    return {
        "bids": [(float(p), float(q)) for p, q in data["bids"]],
        "asks": [(float(p), float(q)) for p, q in data["asks"]],
        "timestamp": data["ts"]
    }

你想想看,每秒几千条行情,每条省0.1毫秒,整体延迟就能降不少。

1.3.2 二进制协议解析

有些交易所用Protobuf或自定义二进制协议。解析起来麻烦点,但带宽占用小。我做过一个项目,从JSON切到Protobuf后,带宽节省了60%。

# 假设交易所用Protobuf定义行情
import market_pb2

def parse_protobuf(raw: bytes):
    orderbook = market_pb2.OrderBook()
    orderbook.ParseFromString(raw)
    return orderbook

1.4 数据清洗与校验

数据到了,但不能直接用。交易所偶尔会发脏数据——重复的、乱序的、甚至错误的。清洗这一步,说白了就是去伪存真。

1.4.1 去重

WebSocket偶尔会重发消息。我一般用序列号去重:

class Deduplicator:
    def __init__(self):
        self.seen = set()
        self.max_size = 10000
    
    def is_duplicate(self, seq_id: int) -> bool:
        if seq_id in self.seen:
            return True
        self.seen.add(seq_id)
        if len(self.seen) > self.max_size:
            # 清理旧数据
            self.seen.clear()
        return False

1.4.2 乱序处理

行情数据到达顺序可能和生成顺序不一致。我遇到过这种情况:先收到t+1时刻的数据,后收到t时刻的。如果不处理,库存计算会出错。

class OrderBookReconstructor:
    def __init__(self):
        self.buffer = []
        self.last_seq = 0
    
    def add_update(self, seq: int, update: dict):
        if seq <= self.last_seq:
            return  # 丢弃旧数据
        self.buffer.append((seq, update))
        self.buffer.sort(key=lambda x: x[0])  # 按序列号排序
        # 处理连续的数据
        while self.buffer and self.buffer[0][0] == self.last_seq + 1:
            _, update = self.buffer.pop(0)
            self.apply_update(update)
            self.last_seq += 1
核心要点:乱序处理的关键是维护一个有序缓冲区,按序列号顺序消费。缓冲区不能太大,否则延迟会变高。

1.4.3 数据校验

校验规则其实不复杂,但必须做:

  • 价格不能为负数或零
  • 数量不能为负数
  • 时间戳不能是未来的
  • 买卖盘口不能交叉(买一价 < 卖一价)
def validate_orderbook(data: dict) -> bool:
    if data["bids"][0][0] >= data["asks"][0][0]:
        return False  # 盘口交叉,数据异常
    if data["timestamp"] > time.time() * 1000 + 1000:
        return False  # 时间戳异常
    return True

1.5 整体架构

说了这么多,咱们看看整体数据流长什么样:

交易所 WebSocket 接入层 数据解析 (JSON/Protobuf) 数据清洗 (去重/校验) 库存计算引擎 实时数据流处理架构 数据从交易所出发,经过接入、解析、清洗,最终进入库存计算引擎

这张图展示了数据从交易所到库存计算引擎的完整路径。每一步都有它的作用,缺一不可。

1.6 性能优化要点

做市系统对延迟极其敏感。我总结了几条优化经验:

优化点 做法 效果
内存分配 预分配对象池,避免频繁GC 延迟降低30%
序列化 用orjson代替标准json 解析速度提升3倍
网络IO 使用异步IO,避免阻塞 吞吐量提升5倍
数据校验 轻量校验,不做复杂计算 延迟增加<1%
我的经验:性能优化要有的放矢。先做profiling,找到瓶颈再优化。别一开始就想着用C++重写,很多时候Python异步IO就够用了。

嗯,数据流接入这块就聊到这儿。说白了,核心就是三件事:连得上、读得懂、洗得净。把这三点做好,后面的库存计算才能稳如磐石。

专注资料整理