4、金融数据获取:REST API基础、使用CCXT库获取交易所行情、WebSocket实时数据流、数据缓存策略
做市系统说白了就是跟数据打交道。你策略再牛,拿不到行情数据也是白搭。这一章我们聊聊金融数据怎么拿,怎么存,怎么让它又快又稳。
我个人习惯把数据获取分成三层:REST API 做基础查询,WebSocket 做实时推送,缓存层 做性能兜底。这三层缺一不可。
核心观点:数据获取不是简单的"发请求-收响应",而是一个需要精心设计的管道系统。管道任何一个环节出问题,你的做市策略都会跟着遭殃。
4.1 REST API 基础
REST API 是啥?说白了就是 HTTP 请求。你给交易所服务器发个 GET 请求,它给你返回 JSON 数据。简单、直接、容易理解。
但这里有个坑——频率限制。我在项目中遇到过,刚开始写代码时疯狂请求,结果 IP 直接被交易所封了半小时。嗯,那半小时我的策略完全瞎了。
常见的 REST API 端点包括:
GET /api/v3/ticker/price— 获取最新价格GET /api/v3/depth— 获取订单簿深度GET /api/v3/klines— 获取K线数据GET /api/v3/trades— 获取近期成交记录
你想想看,这些接口返回的数据结构其实大同小异。但不同交易所的字段名、限频规则、认证方式都不一样。这就是为什么我们需要 CCXT 这样的统一接口库。
小技巧:写 REST 客户端时,一定要加重试机制。网络抖动是常态,不是异常。我一般用指数退避策略,第一次失败等1秒,第二次等2秒,第三次等4秒,最多重试3次。
4.2 使用 CCXT 库获取交易所行情
CCXT 是我最常用的加密货币交易所统一接口库。它支持 100+ 交易所,API 风格一致。你换交易所只需要改一行代码,不用重写整个数据获取模块。
安装很简单:
pip install ccxt
来看一个获取 Binance 行情的小例子:
import ccxt
# 初始化交易所对象
exchange = ccxt.binance({
'rateLimit': 1200, # 限频控制,毫秒
'enableRateLimit': True, # 自动限频
})
# 获取 BTC/USDT 的最新 ticker
ticker = exchange.fetch_ticker('BTC/USDT')
print(f"最新价: {ticker['last']}")
print(f"24h涨跌: {ticker['percentage']}%")
# 获取订单簿深度
orderbook = exchange.fetch_order_book('BTC/USDT', limit=10)
print(f"买一价: {orderbook['bids'][0][0]}, 量: {orderbook['bids'][0][1]}")
print(f"卖一价: {orderbook['asks'][0][0]}, 量: {orderbook['asks'][0][1]}")
为什么推荐 CCXT?因为它帮你处理了:
- 认证签名 — 私有接口的 HMAC 签名自动完成
- 限频控制 — 内置请求队列,不会触发封禁
- 数据标准化 — 不同交易所返回统一格式的数据
- 错误处理 — 网络异常、交易所维护等场景都有对应异常类
注意:CCXT 的 fetch_ticker 返回的是字典,但不同交易所的字段可能略有差异。比如有些交易所返回 'last',有些返回 'close'。建议在代码里做一层适配,统一成自己系统的内部格式。
4.3 WebSocket 实时数据流
REST API 有个致命问题——延迟。你轮询一次需要几百毫秒,这期间价格可能已经变了好几次。做市交易对延迟极其敏感,所以必须上 WebSocket。
WebSocket 是长连接,交易所主动推送数据给你。延迟通常在 10-50ms 级别,比 REST 快一个数量级。
我曾经踩过一个坑:WebSocket 连接断了,程序没检测到,结果策略一直用旧数据做市,亏了不少。后来我加了个心跳检测,每 5 秒检查一次连接状态,断了就自动重连。
来看一个用 websockets 库连接 Binance 的例子:
import asyncio
import websockets
import json
async def subscribe_depth(symbol='btcusdt'):
uri = "wss://stream.binance.com:9443/ws"
async with websockets.connect(uri) as ws:
# 订阅订单簿深度数据
subscribe_msg = {
"method": "SUBSCRIBE",
"params": [f"{symbol}@depth20@100ms"],
"id": 1
}
await ws.send(json.dumps(subscribe_msg))
while True:
data = await ws.recv()
depth = json.loads(data)
# 处理深度数据...
print(f"收到深度更新: {depth['bids'][:3]}")
# 运行
asyncio.run(subscribe_depth())
WebSocket 数据流的核心要点:
- 订阅管理 — 连接建立后先发订阅消息,告诉交易所你要哪些数据
- 心跳保活 — 定期发送 ping 帧,防止连接被断开
- 断线重连 — 必须实现自动重连逻辑,网络不可能永远稳定
- 数据去重 — 重连后可能收到重复数据,需要做去重处理
经验之谈:WebSocket 收到的数据是增量更新的。比如订单簿,交易所只推送变化的部分,你需要自己维护一个本地订单簿快照。我习惯用红黑树或者跳表来维护,查询和更新都是 O(log n)。
4.4 数据缓存策略
数据缓存是容易被忽视但极其重要的一环。你想想看,如果 WebSocket 断了,REST API 又限频,你的策略拿什么做决策?
缓存策略分三层:
| 层级 | 存储位置 | 适用数据 | 过期时间 |
|---|---|---|---|
| L1 内存缓存 | Python 字典 / Redis | 最新价格、订单簿快照 | 秒级 |
| L2 本地缓存 | SQLite / 文件 | K线数据、历史成交 | 分钟级 |
| L3 持久化存储 | PostgreSQL / InfluxDB | 全量历史数据 | 永久 |
我个人习惯用 Redis 做 L1 缓存。为什么?因为它快,而且支持数据结构丰富。比如订单簿可以用 Redis 的 Sorted Set 来存,价格作为 score,数量作为 value,查询和更新都很快。
来看一个简单的缓存实现思路:
import redis
import json
class MarketDataCache:
def __init__(self):
self.redis = redis.Redis(host='localhost', port=6379, db=0)
def update_ticker(self, symbol, ticker):
# 缓存最新 ticker,过期时间 5 秒
key = f"ticker:{symbol}"
self.redis.setex(key, 5, json.dumps(ticker))
def get_ticker(self, symbol):
key = f"ticker:{symbol}"
data = self.redis.get(key)
if data:
return json.loads(data)
return None
def update_depth(self, symbol, depth):
# 用 Sorted Set 存储订单簿
bid_key = f"depth:{symbol}:bids"
ask_key = f"depth:{symbol}:asks"
# 清空旧数据
self.redis.delete(bid_key, ask_key)
# 写入新数据
for price, qty in depth['bids']:
self.redis.zadd(bid_key, {price: qty})
for price, qty in depth['asks']:
self.redis.zadd(ask_key, {price: qty})
# 设置过期时间 10 秒
self.redis.expire(bid_key, 10)
self.redis.expire(ask_key, 10)
避坑指南:缓存一定要设置过期时间!我曾经犯过一个错误——缓存永不过期,结果交易所数据变了,我的策略还在用旧数据做市。那天的回撤我现在还记得。过期时间要根据数据更新频率来定,ticker 可以短一点(1-5秒),K线可以长一点(几分钟)。
本章小结
数据获取是做市系统的地基。REST API 负责基础查询,WebSocket 负责实时推送,缓存层负责性能和容错。这三者配合好了,你的策略才能稳定运行。
最后分享一个我自己的数据获取架构图,你可以参考这个思路来设计:
公众号:蓝海资料掘金营,微信deep3321