4、交易所API对接:REST API与WebSocket API的区别、API密钥管理与安全、限频与重试机制

做市商系统跟交易所打交道,说白了就两条路:REST API 和 WebSocket API。这两兄弟性格完全不同,用错了地方,轻则延迟爆炸,重则直接被交易所封号。我刚开始做量化那会儿,就吃过这个亏——全用 REST 去抢行情,结果还没成交,价格已经变了两轮。

4.1 REST API vs WebSocket API:到底该用谁?

先看一张对比图,帮你快速建立直觉:

REST API 请求-响应模式 HTTP 协议 每次请求独立连接 适合:下单、查询账户 延迟:50~500ms 频率:低(通常 ≤10次/秒) 典型用途 • 下单/撤单 • 查询余额/持仓 • 获取历史K线 WebSocket API 推送-订阅模式 WebSocket 协议 长连接,全双工通信 适合:行情、订单更新 延迟:1~10ms 频率:高(无上限) 典型用途 • 实时深度行情 • 成交/委托推送 • 账户变动通知 延迟差距

4.2 什么时候用 REST?什么时候用 WebSocket?

我个人习惯这样分:「查一次」用 REST,「一直查」用 WebSocket

  • 下单、撤单、查余额——这些操作频率低,用 REST 完全够。你想想看,一秒下个几单,REST 的延迟完全能接受。
  • 深度行情、成交推送——这些必须用 WebSocket。我在项目中遇到过,用 REST 轮询深度,每 200ms 拉一次,结果交易所直接给我限频了,而且拿到的数据永远慢半拍。

核心原则:做市商策略中,行情和成交推送必须走 WebSocket,交易指令走 REST。混用?可以,但千万别反过来。

4.3 API 密钥管理与安全

密钥泄露,等于把交易所账户的钥匙交给别人。我见过不止一个团队,因为密钥硬编码在代码里,被实习生 push 到 GitHub 上,几分钟内账户就被洗劫一空。

4.3.1 密钥存储:别偷懒

  • 环境变量:最基础的做法。用 os.environ 读取,不要写死在代码里。
  • 配置文件加密:用 cryptography 库加密配置文件,运行时解密。
  • 密钥管理服务:生产环境建议用 HashiCorp Vault 或 AWS Secrets Manager。
# 错误示范:硬编码
api_key = "abc123def456"  # 千万别这么干!

# 正确做法:环境变量
import os
api_key = os.environ.get("EXCHANGE_API_KEY")
api_secret = os.environ.get("EXCHANGE_API_SECRET")

4.3.2 签名机制:每个请求都要「验明正身」

交易所通常要求对请求进行 HMAC-SHA256 签名。嗯,这里要注意:签名参数必须包含时间戳和随机数,防止重放攻击。

import time
import hmac
import hashlib
import requests

def sign_request(api_secret, method, path, params):
    timestamp = str(int(time.time() * 1000))
    # 拼接签名内容
    message = timestamp + method + path + params
    signature = hmac.new(
        api_secret.encode(),
        message.encode(),
        hashlib.sha256
    ).hexdigest()
    return timestamp, signature

# 示例:发送签名后的请求
timestamp, sig = sign_request(api_secret, "POST", "/api/v1/order", "symbol=BTCUSDT&side=BUY")
headers = {
    "X-API-KEY": api_key,
    "X-TIMESTAMP": timestamp,
    "X-SIGNATURE": sig
}
response = requests.post("https://api.exchange.com/api/v1/order", headers=headers)

⚠️ 我曾经踩过的坑:时间戳必须用 UTC 毫秒,别用本地时间。有一次服务器时间跟交易所差了 2 秒,所有请求都被拒了。排查了半小时才发现是时区问题。

4.4 限频与重试机制

交易所不是你家开的,每个 API 都有频率限制。做市商策略每秒可能发几十个请求,一不小心就被封了。

4.4.1 限频策略:别硬闯

限频类型 说明 应对策略
IP 限频 每个 IP 每秒/每分钟允许的请求数 使用多个 IP 或代理
API Key 限频 每个密钥的请求配额 控制请求速率,使用令牌桶算法
权重限频 不同接口消耗不同权重 优先使用低权重接口

我建议用 令牌桶算法 来控制请求速率。说白了就是:桶里有一定数量的令牌,每发一个请求消耗一个令牌,令牌按固定速率补充。

import time
import threading

class TokenBucket:
    def __init__(self, rate, capacity):
        self.rate = rate          # 每秒补充的令牌数
        self.capacity = capacity  # 桶容量
        self.tokens = capacity
        self.last_refill = time.time()
        self.lock = threading.Lock()

    def consume(self, tokens=1):
        with self.lock:
            now = time.time()
            # 补充令牌
            elapsed = now - self.last_refill
            self.tokens = min(self.capacity, self.tokens + elapsed * self.rate)
            self.last_refill = now

            if self.tokens >= tokens:
                self.tokens -= tokens
                return True
            return False

# 使用示例:每秒最多 10 个请求
bucket = TokenBucket(rate=10, capacity=10)

def safe_request(url, headers):
    while not bucket.consume():
        time.sleep(0.05)  # 等待 50ms 再试
    return requests.get(url, headers=headers)

4.4.2 重试机制:优雅地处理失败

网络波动、交易所临时限频,这些都会导致请求失败。直接重试?不行,会加重问题。你需要一个 指数退避 + 随机抖动 的重试策略。

import time
import random
import requests

def retry_request(url, headers, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = requests.get(url, headers=headers, timeout=5)
            if response.status_code == 429:  # 限频
                wait = (2 ** attempt) + random.uniform(0, 1)
                print(f"限频了,等待 {wait:.2f} 秒后重试")
                time.sleep(wait)
                continue
            response.raise_for_status()
            return response.json()
        except requests.exceptions.RequestException as e:
            if attempt == max_retries - 1:
                raise e
            wait = (2 ** attempt) + random.uniform(0, 1)
            print(f"请求失败,{wait:.2f} 秒后重试: {e}")
            time.sleep(wait)
    return None

💡 个人经验:重试次数别超过 5 次。超过 5 次还失败,说明问题不在网络层,可能是密钥失效或交易所挂了。这时候应该报警,而不是继续重试。

4.5 WebSocket 连接管理:心跳与重连

WebSocket 是长连接,但网络不稳定时会断开。你需要自己维护心跳和重连逻辑。

import websocket
import json
import threading
import time

class WebSocketClient:
    def __init__(self, url, ping_interval=30):
        self.url = url
        self.ping_interval = ping_interval
        self.ws = None
        self.running = False

    def on_message(self, ws, message):
        data = json.loads(message)
        # 处理行情数据
        print(f"收到行情: {data}")

    def on_error(self, ws, error):
        print(f"WebSocket 错误: {error}")

    def on_close(self, ws, close_status_code, close_msg):
        print("连接关闭,准备重连...")
        self.reconnect()

    def on_open(self, ws):
        print("连接成功")
        # 订阅行情
        subscribe_msg = {
            "op": "subscribe",
            "channel": "depth",
            "symbol": "BTCUSDT"
        }
        ws.send(json.dumps(subscribe_msg))

    def connect(self):
        self.ws = websocket.WebSocketApp(
            self.url,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close,
            on_open=self.on_open
        )
        self.running = True
        # 启动心跳线程
        threading.Thread(target=self.heartbeat, daemon=True).start()
        self.ws.run_forever()

    def reconnect(self):
        if not self.running:
            return
        time.sleep(5)  # 等待 5 秒后重连
        print("正在重连...")
        self.connect()

    def heartbeat(self):
        while self.running:
            time.sleep(self.ping_interval)
            if self.ws and self.ws.sock:
                try:
                    self.ws.send(json.dumps({"op": "ping"}))
                except:
                    pass

    def close(self):
        self.running = False
        if self.ws:
            self.ws.close()

关键点:WebSocket 重连时,必须重新订阅所有频道。我见过有人重连后忘了订阅,结果行情停了半小时才发现。

4.6 实战建议:做市商系统的 API 架构

最后,给你一个我实际在用的架构参考:

  • 行情层:2~3 条 WebSocket 连接,分别订阅深度、成交、K线。每条连接独立心跳。
  • 交易层:REST API + 令牌桶限频。下单、撤单、查余额走 REST。
  • 监控层:WebSocket 连接状态监控,断线自动报警。
  • 重试层:所有 REST 请求统一走重试装饰器,指数退避 + 抖动。

嗯,API 对接这块,说白了就是「稳」字当头。宁可慢一点,也别让交易所把你封了。我见过太多团队,策略写得漂亮,结果 API 层一塌糊涂,上线第一天就被限频,血亏。


专注资料整理