4、订单流数据采集:交易所API对接

数据采集是整个订单流系统的「水源」。水源不干净,后面再好的分析引擎也是白搭。我在这个环节踩过的坑,比后面所有环节加起来都多。

说白了,交易所API对接就三件事:连得上、收得全、存得住。但每一件都有不少门道。

4.1 REST vs WebSocket:什么时候用哪个?

先说说这两个协议。很多人一上来就问「哪个更好」,其实不存在更好,只有更合适。

特性 REST WebSocket
连接方式 请求-响应 长连接,全双工
延迟 50-200ms(含网络) 1-10ms
适用场景 查询账户、下单、获取历史数据 实时行情、订单簿增量
资源消耗 低(用完即断) 高(需维持心跳)

我个人习惯:REST做「查」和「写」,WebSocket做「收」。比如查询持仓余额用REST,订阅逐笔成交用WebSocket。这样分工明确,出了问题也好排查。

核心原则:REST负责「一次性操作」,WebSocket负责「持续订阅」。千万别用REST去轮询行情,那是在给自己挖坑。

4.2 实时行情数据订阅

行情订阅,说白了就是告诉交易所:「我要看哪些品种的哪些数据,有更新就推给我。」

以币安的WebSocket接口为例,订阅深度行情是这样的:

// 伪代码示例 - 订阅BTC/USDT的订单簿增量
ws.connect("wss://stream.binance.com:9443/ws")
ws.send({
  "method": "SUBSCRIBE",
  "params": [
    "btcusdt@depth20@100ms",  // 20档深度,100ms快照
    "btcusdt@trade"           // 逐笔成交
  ],
  "id": 1
})

嗯,这里要注意:订阅的频道不是越多越好。我曾经有个项目,一口气订阅了50个交易对的所有频道,结果本地带宽被打满,消息处理队列直接爆了。后来我学乖了,只订阅真正需要的品种,而且分批次订阅。

我的经验:订阅数量控制在10个交易对以内,每个交易对最多3个频道(深度、成交、K线)。如果确实需要更多,考虑多开几个连接,分散压力。

4.3 订单簿增量更新处理

订单簿是高频交易的核心。全量快照动辄几百KB,每秒拉一次根本不现实。所以交易所都支持增量更新——只告诉你「变了什么」,而不是「全部是什么」。

增量更新的逻辑其实很简单:

  1. 获取初始快照:第一次连接时,拉一次全量订单簿
  2. 维护本地副本:在内存里维护一个OrderBook对象
  3. 应用增量:收到增量消息后,更新本地副本
  4. 校验一致性:每隔一段时间,用快照校验本地数据是否正确

我曾经遇到过一个经典问题:增量消息丢了,导致本地订单簿和交易所不一致。后来我加了一个「校验机制」——每1000条增量后,主动拉一次快照做对比。如果发现偏差,就重建本地订单簿。

避坑指南:增量消息的序号(sequence ID)一定要校验。如果发现序号不连续,立即丢弃当前增量,重新拉取全量快照。别问我怎么知道的——有一次我忽略了序号校验,结果策略根据错误的订单簿下了单,亏了六位数。

4.4 数据缓存策略

数据到了本地,不能直接扔给策略用。为什么?因为网络抖动、交易所限流、本地处理延迟,都会导致数据「断流」或「堆积」。缓存层就是用来平滑这些问题的。

我常用的缓存策略分三层:

层级 存储介质 用途 保留时间
L1 内存(RingBuffer) 最新行情,供策略实时消费 秒级
L2 Redis 分钟级K线、订单簿快照 小时级
L3 时序数据库(InfluxDB/TDengine) 历史数据,用于回测和分析 永久

L1缓存我用的是RingBuffer,说白了就是一个固定大小的循环数组。写入O(1),读取O(1),没有GC压力。我习惯把RingBuffer大小设为2的幂次,比如65536,这样取模运算可以用位运算加速。

// RingBuffer 核心逻辑(伪代码)
class RingBuffer {
  constructor(size) {
    this.buffer = new Array(size)
    this.mask = size - 1  // size必须是2的幂
    this.writeIndex = 0
  }
  
  write(data) {
    this.buffer[this.writeIndex & this.mask] = data
    this.writeIndex++
  }
  
  read(fromIndex) {
    return this.buffer[fromIndex & this.mask]
  }
}

一个小技巧:L2缓存用Redis的Stream数据结构,天然支持消息持久化和消费者组。这样即使策略重启,也能从断点处继续消费,不会丢数据。

4.5 整体架构图

说了这么多,画张图把整个流程串起来。你想想看,数据从交易所出来,经过采集层、缓存层,最后到策略层,每一步都有它的职责。

订单流数据采集架构 交易所数据源 REST API(查询/下单) WebSocket(实时行情) 增量更新(订单簿) 数据采集层 连接管理(心跳/重连) 消息解析(JSON/Protobuf) 增量合并(OrderBook) 数据缓存层 L1: RingBuffer(内存) L2: Redis Stream L3: 时序数据库 策略消费层 实时策略 / 回测引擎 / 风控模块

这张图我画得比较简洁,但每个环节都有坑。比如采集层的「重连机制」,我见过有人用指数退避,结果交易所恢复后半小时才连上;也有人用固定间隔,结果把交易所网关打挂了。我个人习惯用「抖动退避」——基础间隔1秒,每次翻倍,再加一个随机偏移,既不会太快也不会太慢。

最后说一句:数据采集是整个系统的基石。这块做得稳,后面策略怎么写都顺手。这块偷懒了,后面天天修数据。我见过太多团队在采集层省功夫,最后花十倍时间填坑。嗯,别问我怎么知道的。

专注资料整理