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 整体架构图
说了这么多,我画张图帮你理清思路。这是行情接入模块的整体架构:
这张图从左到右,从上到下,就是行情数据从交易所到策略模块的完整链路。每一层都有明确的职责,层与层之间通过接口解耦。这样做的好处是——任何一层出了问题,都不会影响其他层。
好了,行情接入这块就聊到这儿。记住一句话:行情数据是系统的血液,血管要通畅,心脏要有力。下一节我们聊聊订单管理,那又是另一番天地了。
公众号:蓝海资料掘金营,微信deep3321