2、行情数据接入:交易所API对接(REST/WebSocket)、行情数据格式解析(Level1/Level2)、行情数据缓存与分发、延迟优化策略

做市商系统里,行情数据就是你的眼睛。

眼睛要是瞎了,或者看东西慢了半拍,那交易策略再牛也没用。我见过太多团队,策略模型跑得飞起,结果行情接入环节就跪了——要么数据丢了,要么延迟高得离谱。说白了,行情接入是整个系统的地基,地基不稳,上面盖什么都是白搭。

2.1 交易所API对接:REST vs WebSocket

交易所给你两个接口:REST和WebSocket。怎么选?我直接说结论——两个都要用,但分工不同。

特性 REST WebSocket
连接方式 短连接,每次请求独立 长连接,全双工通信
延迟 高(50-200ms) 低(1-10ms)
适用场景 历史数据查询、盘前检查 实时行情订阅、订单簿更新
可靠性 天然可靠(HTTP状态码) 需自行处理断线重连

我个人习惯是:REST用来做初始化,WebSocket用来跑实时。举个例子,每天早上开盘前,我用REST拉一次全量的订单簿快照,然后通过WebSocket订阅增量更新。这样既保证了初始数据的完整性,又享受了WebSocket的低延迟。

核心原则:REST负责「对账」,WebSocket负责「速度」。两者互补,缺一不可。

REST API对接要点

REST对接其实没什么技术含量,但坑不少。我踩过最大的坑是——限频。有些交易所的REST接口每秒只允许1次请求,你一不小心超了,直接给你封IP。

解决方案?加本地限流器。我一般用令牌桶算法,控制请求速率在安全范围内。代码大概长这样:

import time
from collections import deque

class RateLimiter:
    def __init__(self, max_calls, period):
        self.max_calls = max_calls
        self.period = period
        self.calls = deque()
    
    def acquire(self):
        now = time.time()
        # 清理过期记录
        while self.calls and self.calls[0] < now - self.period:
            self.calls.popleft()
        if len(self.calls) >= self.max_calls:
            sleep_time = self.calls[0] + self.period - now
            if sleep_time > 0:
                time.sleep(sleep_time)
        self.calls.append(time.time())

WebSocket对接要点

WebSocket的坑更多。我记得有一次,交易所的WebSocket服务器半夜挂了,我的程序没检测到,结果第二天开盘发现行情数据断了整整4个小时。嗯,从那以后我学乖了——心跳检测+自动重连是标配。

我现在的做法是:

  • 每5秒发一次ping,3次没收到pong就判定断开
  • 断开后指数退避重连(1s, 2s, 4s, 8s...最大30s)
  • 重连成功后,重新订阅所有行情

注意:有些交易所的WebSocket有「空闲超时」机制,如果你长时间不发送任何消息,它会主动断开连接。所以即使没有数据要发,也要定时发个ping保活。

2.2 行情数据格式解析:Level1 vs Level2

行情数据分两个层级:Level1和Level2。说白了,Level1是「大众版」,Level2是「专业版」。

数据项 Level1 Level2
最新价
买卖五档
成交量
逐笔成交
买卖十档/全档
委托队列

做市商系统,我建议直接上Level2。为什么?因为Level1的数据太「糙」了。你想想看,Level1的买卖五档是快照数据,每秒更新一次,中间发生了什么你完全不知道。而Level2的逐笔成交和委托队列,能让你看到市场的微观结构——谁在买、谁在卖、挂单撤单的频率如何。

我在项目中遇到过一件事:某个币种突然暴跌,Level1显示卖一价格从10块直接跳到8块,中间发生了什么?完全不知道。但Level2的逐笔成交显示,有人在10块、9.5块、9块连续砸了三个大单,然后迅速撤单。这就是典型的「砸盘测试」——有人故意制造恐慌。如果你只有Level1,你根本看不出来。

解析性能优化

行情数据的解析,说白了就是「字节流→结构体」的转换。但别小看这一步,处理不好会成为性能瓶颈。

我常用的优化手段:

  • 零拷贝解析:直接操作字节数组,避免创建临时对象
  • 预分配对象池:行情对象频繁创建销毁,用对象池复用
  • 批量处理:攒一批数据再解析,减少系统调用
// 零拷贝解析示例(伪代码)
public class MarketDataParser {
    public void parse(byte[] data, int offset, MarketData out) {
        out.price = ByteBuffer.wrap(data, offset, 8).getDouble();
        out.volume = ByteBuffer.wrap(data, offset + 8, 8).getDouble();
        out.timestamp = ByteBuffer.wrap(data, offset + 16, 8).getLong();
        // 直接填充到预分配的对象中,不创建新对象
    }
}

2.3 行情数据缓存与分发

行情数据解析完了,接下来怎么办?直接扔给策略模块?千万别。你想想看,如果策略模块处理速度跟不上行情更新速度,数据就会积压,延迟越来越高。

正确的做法是:缓存+分发

我一般用两级缓存架构:

  • 一级缓存(内存):用环形缓冲区(RingBuffer),存储最近N条行情数据。读写无锁,性能极高。
  • 二级缓存(磁盘):用内存映射文件(MMAP),存储全量历史数据。用于回测和复盘。

分发的话,我推荐用发布-订阅模式。每个策略模块订阅自己需要的行情类型,缓存层负责推送。这样策略模块之间互不干扰,也方便动态增减策略。

小技巧:如果多个策略订阅同一个行情,缓存层只解析一次,然后广播给所有订阅者。避免重复解析浪费CPU。

2.4 延迟优化策略

延迟是做市商的命门。你的系统比别人慢1毫秒,可能就抢不到单。我总结了几条实战经验:

网络层优化

  • 就近部署:把服务器放在交易所机房或者同城IDC,物理距离决定光速延迟
  • 使用TCP_NODELAY:禁用Nagle算法,小包立即发送
  • 调整内核参数:增大TCP缓冲区,减少丢包重传

应用层优化

  • 绑定CPU核心:行情处理线程绑定到独立CPU核心,避免上下文切换
  • 使用大页内存:减少TLB miss,提升内存访问速度
  • 避免锁竞争:用无锁数据结构(如Disruptor)替代传统队列

协议层优化

  • 使用二进制协议:Protobuf或FlatBuffers比JSON快10倍以上
  • 合并小消息:把多个行情更新合并成一个数据包发送,减少网络开销

我的经验:延迟优化不是一蹴而就的。我曾经花了两周时间,把行情处理延迟从5ms降到了0.5ms。每一步优化只提升一点点,但累积起来效果惊人。关键是——先测量,再优化。没有数据支撑的优化都是瞎搞。

2.5 整体架构图

说了这么多,我画张图帮你理清思路。这是行情接入模块的整体架构:

行情数据接入模块架构图 交易所数据源 REST API(历史数据/快照) WebSocket(实时增量数据) 协议解析与数据标准化 Level1/Level2格式解析 统一内部数据结构 行情缓存与分发 一级缓存(环形缓冲区) 二级缓存(MMAP磁盘) 策略模块消费 做市策略 风控模块 监控告警

这张图从左到右,从上到下,就是行情数据从交易所到策略模块的完整链路。每一层都有明确的职责,层与层之间通过接口解耦。这样做的好处是——任何一层出了问题,都不会影响其他层

好了,行情接入这块就聊到这儿。记住一句话:行情数据是系统的血液,血管要通畅,心脏要有力。下一节我们聊聊订单管理,那又是另一番天地了。


公众号:蓝海资料掘金营,微信deep3321