2024年双十一那天,我的加密货币套利机器人在线上跑了3小时后彻底哑火。行情数据像断了线的风筝,订单簿数据停在错误的时间戳上,价差套利机会眼睁睁从指缝溜走。那一刻我才意识到,WebSocket 断线重连不是"锦上添花",而是高频交易系统的"生死线"。

本文是我用血泪踩坑换来的实战经验,涵盖 Binance、Bybit、OKX、Deribit 四大主流交易所的 WebSocket 行情接入完整方案,包含可直接复制的 Python/Node.js 代码,以及常见报错的排查清单。

为什么你的 WebSocket 连接总是不稳定?

在深入代码之前,先理解问题的本质。加密货币交易所的 WebSocket 连接不稳定通常由以下原因造成:

标准断线重连架构设计

一个健壮的重连机制需要包含以下组件:

"""
WebSocket 行情重连管理器 - Python 实现
支持:Binance / Bybit / OKX / Deribit
"""

import asyncio
import json
import time
import random
from dataclasses import dataclass, field
from typing import Callable, Optional, Dict, Any
from enum import Enum
import websockets
from websockets.client import WebSocketClientProtocol

class ConnectionState(Enum):
    DISCONNECTED = "disconnected"
    CONNECTING = "connecting"
    CONNECTED = "connected"
    RECONNECTING = "reconnecting"

@dataclass
class ReconnectConfig:
    max_retries: int = 10
    base_delay: float = 1.0        # 基础重连延迟(秒)
    max_delay: float = 60.0         # 最大重连延迟(秒)
    exponential_base: float = 2.0   # 指数退避基数
    jitter: bool = True             # 是否添加随机抖动
    
@dataclass
class MarketDataHandler:
    """行情数据处理器基类"""
    def on_orderbook(self, exchange: str, data: Dict[str, Any]):
        """处理订单簿数据"""
        pass
    
    def on_trade(self, exchange: str, data: Dict[str, Any]):
        """处理成交数据"""
        pass
    
    def on_ticker(self, exchange: str, data: Dict[str, Any]):
        """处理行情 ticker"""
        pass

class MultiExchangeWebSocketManager:
    """
    多交易所 WebSocket 行情管理
    核心特性:
    - 指数退避重连 + 抖动
    - 自动心跳维持
    - 多交易所并行管理
    - 数据流丢失检测
    """
    
    def __init__(
        self, 
        config: ReconnectConfig = None,
        handler: MarketDataHandler = None
    ):
        self.config = config or ReconnectConfig()
        self.handler = handler or MarketDataHandler()
        self.connections: Dict[str, WebSocketClientProtocol] = {}
        self.states: Dict[str, ConnectionState] = {}
        self.retry_counts: Dict[str, int] = {}
        self.last_message_time: Dict[str, float] = {}
        self.subscriptions: Dict[str, list] = {}
        self._running = False
        
        # 交易所 WebSocket 端点配置
        self.endpoints = {
            "binance": "wss://stream.binance.com:9443/ws",
            "bybit": "wss://stream.bybit.com/v5/public/spot",
            "okx": "wss://ws.okx.com:8443/ws/v5/public",
            "deribit": "wss://www.deribit.com/ws/api/v2"
        }
        
        # 心跳配置(各交易所要求不同)
        self.ping_intervals = {
            "binance": 60,
            "bybit": 30,
            "okx": 20,
            "deribit": 10
        }
        
        # 数据超时阈值(秒)
        self.data_timeouts = {
            "binance": 10,
            "bybit": 15,
            "okx": 12,
            "deribit": 8
        }
    
    def _calculate_delay(self, retry_count: int) -> float:
        """计算重连延迟:指数退避 + 抖动"""
        delay = min(
            self.config.base_delay * (self.config.exponential_base ** retry_count),
            self.config.max_delay
        )
        if self.config.jitter:
            # 添加 ±25% 随机抖动,避免多客户端同时重连
            delay *= (0.75 + random.random() * 0.5)
        return delay
    
    async def connect(self, exchange: str, streams: list):
        """连接到指定交易所"""
        if exchange not in self.endpoints:
            raise ValueError(f"不支持的交易所: {exchange}")
        
        self.subscriptions[exchange] = streams
        self.states[exchange] = ConnectionState.DISCONNECTED
        await self._establish_connection(exchange)
    
    async def _establish_connection(self, exchange: str):
        """建立或重建连接"""
        self.states[exchange] = ConnectionState.CONNECTING
        
        try:
            endpoint = self.endpoints[exchange]
            async with websockets.connect(endpoint, ping_interval=None) as ws:
                self.connections[exchange] = ws
                self.states[exchange] = ConnectionState.CONNECTED
                self.retry_counts[exchange] = 0
                self.last_message_time[exchange] = time.time()
                
                # 发送订阅请求
                await self._subscribe(exchange, ws)
                
                # 启动心跳协程
                asyncio.create_task(self._heartbeat(exchange, ws))
                
                # 启动数据超时检测
                asyncio.create_task(self._monitor_data_timeout(exchange))
                
                # 启动消息接收循环
                await self._receive_loop(exchange, ws)
                
        except websockets.exceptions.ConnectionClosed as e:
            print(f"[{exchange}] 连接关闭: {e}")
            await self._handle_reconnect(exchange)
        except Exception as e:
            print(f"[{exchange}] 连接错误: {e}")
            await self._handle_reconnect(exchange)
    
    async def _subscribe(self, exchange: str, ws: WebSocketClientProtocol):
        """发送订阅请求(各交易所格式不同)"""
        streams = self.subscriptions.get(exchange, [])
        
        if exchange == "binance":
            # Binance: 直接在 URL 中指定 streams
            pass
        elif exchange == "bybit":
            await ws.send(json.dumps({
                "op": "subscribe",
                "args": streams
            }))
        elif exchange == "okx":
            await ws.send(json.dumps({
                "op": "subscribe",
                "args": [{"channel": s} for s in streams]
            }))
        elif exchange == "deribit":
            await ws.send(json.dumps({
                "method": "subscribe",
                "params": {"channels": streams}
            }))
    
    async def _heartbeat(self, exchange: str, ws: WebSocketClientProtocol):
        """维持心跳连接"""
        interval = self.ping_intervals.get(exchange, 30)
        try:
            while self.states.get(exchange) == ConnectionState.CONNECTED:
                await asyncio.sleep(interval)
                if self.states.get(exchange) == ConnectionState.CONNECTED:
                    await ws.ping()
        except Exception:
            pass
    
    async def _monitor_data_timeout(self, exchange: str):
        """检测数据流是否超时"""
        timeout = self.data_timeouts.get(exchange, 10)
        while self.states.get(exchange) == ConnectionState.CONNECTED:
            await asyncio.sleep(timeout / 2)
            
            last_time = self.last_message_time.get(exchange, 0)
            if time.time() - last_time > timeout:
                print(f"[{exchange}] 数据流超时 ({timeout}s),强制重连")
                await self._handle_reconnect(exchange)
                break
    
    async def _receive_loop(self, exchange: str, ws: WebSocketClientProtocol):
        """消息接收主循环"""
        async for message in ws:
            self.last_message_time[exchange] = time.time()
            
            try:
                data = json.loads(message)
                await self._process_message(exchange, data)
            except json.JSONDecodeError:
                print(f"[{exchange}] JSON 解析失败: {message[:100]}")
    
    async def _process_message(self, exchange: str, data: Dict[str, Any]):
        """处理接收到的消息"""
        # 根据消息类型分发到不同处理器
        msg_type = data.get("e") or data.get("type") or data.get("channel", "")
        
        if "depth" in str(msg_type).lower() or "orderbook" in str(data).lower():
            self.handler.on_orderbook(exchange, data)
        elif "trade" in str(msg_type).lower():
            self.handler.on_trade(exchange, data)
        elif "ticker" in str(msg_type).lower() or "bookTicker" in str(data).lower():
            self.handler.on_ticker(exchange, data)
    
    async def _handle_reconnect(self, exchange: str):
        """处理重连逻辑"""
        retry_count = self.retry_counts.get(exchange, 0)
        
        if retry_count >= self.config.max_retries:
            print(f"[{exchange}] 达到最大重试次数 ({self.config.max_retries}),放弃重连")
            self.states[exchange] = ConnectionState.DISCONNECTED
            return
        
        self.states[exchange] = ConnectionState.RECONNECTING
        delay = self._calculate_delay(retry_count)
        
        print(f"[{exchange}] {delay:.1f}秒后重连 (第 {retry_count + 1} 次)")
        await asyncio.sleep(delay)
        
        self.retry_counts[exchange] = retry_count + 1
        
        # 尝试重建连接
        try:
            await self._establish_connection(exchange)
        except Exception as e:
            print(f"[{exchange}] 重连失败: {e}")
            await self._handle_reconnect(exchange)
    
    async def start_all(self, exchanges: Dict[str, list]):
        """启动所有交易所连接"""
        self._running = True
        tasks = [
            self.connect(exchange, streams) 
            for exchange, streams in exchanges.items()
        ]
        await asyncio.gather(*tasks)
    
    async def stop(self):
        """停止所有连接"""
        self._running = False
        for exchange, ws in self.connections.items():
            await ws.close()
            self.states[exchange] = ConnectionState.DISCONNECTED


使用示例

if __name__ == "__main__": class MyHandler(MarketDataHandler): def on_orderbook(self, exchange: str, data: Dict[str, Any]): print(f"[{exchange}] 订单簿更新: {data.get('s', 'UNKNOWN')}") def on_trade(self, exchange: str, data: Dict[str, Any]): print(f"[{exchange}] 成交: 价格={data.get('p')}, 数量={data.get('q')}") async def main(): config = ReconnectConfig( max_retries=10, base_delay=2.0, max_delay=120.0, jitter=True ) manager = MultiExchangeWebSocketManager( config=config, handler=MyHandler() ) # 订阅配置 exchanges = { "binance": ["btcusdt@depth20@100ms", "btcusdt@trade"], "bybit": ["orderbook.50.BTCUSDT", "publicTrade.BTCUSDT"], "okx": ["books5:BTC-USDT", "trades:BTC-USDT"], "deribit": ["book.BTC-PERPETUAL", "trades.BTC-PERPETUAL"] } try: await manager.start_all(exchanges) except KeyboardInterrupt: await manager.stop() # 运行: asyncio.run(main())

Node.js 实现方案

如果你更习惯 JavaScript/TypeScript 生态,以下是 Node.js 原生实现:

/**
 * 多交易所 WebSocket 行情客户端 - Node.js 实现
 * 使用原生 WebSocket + async/await
 */

const EventEmitter = require('events');

class ExchangeWebSocket extends EventEmitter {
    constructor(exchange, config = {}) {
        super();
        this.exchange = exchange;
        this.config = {
            maxRetries: config.maxRetries || 10,
            baseDelay: config.baseDelay || 1000,
            maxDelay: config.maxDelay || 60000,
            pingInterval: config.pingInterval || 30000,
            dataTimeout: config.dataTimeout || 15000
        };
        
        this.ws = null;
        this.retryCount = 0;
        this.isRunning = false;
        this.lastMessageTime = 0;
        this.pingTimer = null;
        this.timeoutTimer = null;
        
        // 各交易所配置
        this.endpoints = {
            binance: 'wss://stream.binance.com:9443/ws',
            bybit: 'wss://stream.bybit.com/v5/public/spot',
            okx: 'wss://ws.okx.com:8443/ws/v5/public',
            deribit: 'wss://www.deribit.com/ws/api/v2'
        };
    }
    
    calculateDelay() {
        // 指数退避 + 抖动
        let delay = Math.min(
            this.config.baseDelay * Math.pow(2, this.retryCount),
            this.config.maxDelay
        );
        // 添加 ±25% 抖动
        delay *= (0.75 + Math.random() * 0.5);
        return delay;
    }
    
    async connect(streams = []) {
        return new Promise((resolve, reject) => {
            const endpoint = this.endpoints[this.exchange];
            if (!endpoint) {
                reject(new Error(不支持的交易所: ${this.exchange}));
                return;
            }
            
            console.log([${this.exchange}] 正在连接...);
            
            try {
                this.ws = new WebSocket(endpoint);
                
                this.ws.on('open', async () => {
                    console.log([${this.exchange}] 连接成功);
                    this.isRunning = true;
                    this.retryCount = 0;
                    this.lastMessageTime = Date.now();
                    
                    // 发送订阅
                    await this.subscribe(streams);
                    
                    // 启动心跳
                    this.startPing();
                    
                    // 启动超时检测
                    this.startTimeoutMonitor();
                    
                    this.emit('connected');
                    resolve();
                });
                
                this.ws.on('message', (data) => {
                    this.lastMessageTime = Date.now();
                    this.handleMessage(data);
                });
                
                this.ws.on('close', (code, reason) => {
                    console.log([${this.exchange}] 连接关闭: ${code} - ${reason});
                    this.cleanup();
                    this.handleReconnect(streams);
                });
                
                this.ws.on('error', (error) => {
                    console.error([${this.exchange}] 错误: ${error.message});
                    this.emit('error', error);
                });
                
            } catch (error) {
                reject(error);
            }
        });
    }
    
    async subscribe(streams) {
        if (!streams.length || this.ws.readyState !== WebSocket.OPEN) return;
        
        const message = this.buildSubscribeMessage(streams);
        this.ws.send(JSON.stringify(message));
        console.log([${this.exchange}] 订阅: ${JSON.stringify(streams)});
    }
    
    buildSubscribeMessage(streams) {
        // 各交易所订阅格式不同
        switch (this.exchange) {
            case 'bybit':
                return { op: 'subscribe', args: streams };
            case 'okx':
                return { op: 'subscribe', args: streams.map(s => ({ channel: s })) };
            case 'deribit':
                return { method: 'subscribe', params: { channels: streams } };
            default:
                return streams;
        }
    }
    
    handleMessage(data) {
        try {
            const message = JSON.parse(data);
            this.emit('message', message);
            
            // 路由到具体处理器
            if (message.e) {
                // Binance 格式
                this.emit(message.e, message);
            } else if (message.type) {
                this.emit(message.type, message);
            }
        } catch (e) {
            console.error([${this.exchange}] 消息解析失败);
        }
    }
    
    startPing() {
        this.pingTimer = setInterval(() => {
            if (this.ws && this.ws.readyState === WebSocket.OPEN) {
                this.ws.ping();
            }
        }, this.config.pingInterval);
    }
    
    startTimeoutMonitor() {
        this.timeoutTimer = setInterval(() => {
            const elapsed = Date.now() - this.lastMessageTime;
            if (elapsed > this.config.dataTimeout) {
                console.warn([${this.exchange}] 数据超时 (${elapsed}ms),重连中...);
                this.ws.close();
            }
        }, this.config.dataTimeout / 2);
    }
    
    cleanup() {
        this.isRunning = false;
        if (this.pingTimer) clearInterval(this.pingTimer);
        if (this.timeoutTimer) clearInterval(this.timeoutTimer);
    }
    
    async handleReconnect(streams) {
        if (!this.isRunning) return;
        if (this.retryCount >= this.config.maxRetries) {
            console.error([${this.exchange}] 达到最大重试次数,停止重连);
            this.emit('maxRetriesExceeded');
            return;
        }
        
        const delay = this.calculateDelay();
        console.log([${this.exchange}] ${Math.round(delay/1000)}秒后重连 (第 ${this.retryCount + 1} 次));
        
        await new Promise(resolve => setTimeout(resolve, delay));
        this.retryCount++;
        
        try {
            await this.connect(streams);
        } catch (error) {
            console.error([${this.exchange}] 重连失败: ${error.message});
        }
    }
    
    close() {
        this.isRunning = false;
        if (this.ws) {
            this.ws.close();
        }
        this.cleanup();
    }
}

// 使用示例
async function main() {
    const streams = {
        binance: ['btcusdt@depth20@100ms', 'btcusdt@trade'],
        bybit: ['orderbook.50.BTCUSDT', 'publicTrade.BTCUSDT'],
        okx: ['books5:BTC-USDT', 'trades:BTC-USDT']
    };
    
    const clients = {};
    
    for (const [exchange, params] of Object.entries(streams)) {
        const client = new ExchangeWebSocket(exchange, {
            maxRetries: 10,
            baseDelay: 2000,
            maxDelay: 120000
        });
        
        // 事件监听
        client.on('connected', () => {
            console.log(✓ ${exchange} 已连接);
        });
        
        client.on('depthUpdate', (data) => {
            // 处理订单簿更新
        });
        
        client.on('trade', (data) => {
            // 处理成交数据
        });
        
        client.on('error', (error) => {
            console.error(✗ ${exchange} 错误:, error.message);
        });
        
        client.on('maxRetriesExceeded', () => {
            console.error(✗ ${exchange} 重试次数耗尽);
        });
        
        clients[exchange] = client;
    }
    
    // 启动所有连接
    const connections = Object.entries(clients).map(
        ([exchange, client]) => client.connect(streams[exchange])
    );
    
    await Promise.allSettled(connections);
    
    // 优雅关闭
    process.on('SIGINT', () => {
        console.log('\n正在关闭所有连接...');
        Object.values(clients).forEach(client => client.close());
        process.exit(0);
    });
}

main().catch(console.error);

多交易所数据对齐策略

实战中最头疼的问题不是断线重连,而是多交易所数据的时间对齐。不同交易所的时钟可能存在毫秒级偏差,直接用各自的时间戳做跨交易所套利会出问题。

"""
多交易所数据时间对齐模块
核心思路:
1. 以某个交易所为基准(通常选择延迟最低的)
2. 其他交易所数据通过插值/外推对齐到基准时间戳
3. 使用滑动窗口平滑异常值
"""

import time
from collections import deque
from dataclasses import dataclass
from typing import Dict, Optional
import threading

@dataclass
class TimestampedData:
    exchange: str
    symbol: str
    price: float
    timestamp: float  # 交易所原始时间戳
    local_time: float  # 本地接收时间
    
@dataclass
class AlignedData:
    base_timestamp: float
    data: Dict[str, Optional[TimestampedData]]
    latency_ms: float  # 相对基准的延迟

class MultiExchangeSynchronizer:
    """
    多交易所行情数据时间同步器
    
    算法说明:
    - 使用滑动窗口记录每个交易所的数据到达时间差
    - 动态计算各交易所的时钟偏差
    - 基于偏差值对原始数据进行时间对齐
    """
    
    def __init__(self, base_exchange: str = "binance", window_size: int = 100):
        self.base_exchange = base_exchange
        self.window_size = window_size
        
        # 各交易所的时钟偏差(相对于基准交易所)
        self.clock_offsets: Dict[str, deque] = {
            "binance": deque(maxlen=window_size),
            "bybit": deque(maxlen=window_size),
            "okx": deque(maxlen=window_size),
            "deribit": deque(maxlen=window_size)
        }
        
        self.latest_data: Dict[str, Optional[TimestampedData]] = {
            "binance": None,
            "bybit": None,
            "okx": None,
            "deribit": None
        }
        
        self._lock = threading.Lock()
    
    def add_data(self, data: TimestampedData) -> Optional[AlignedData]:
        """添加新的行情数据,返回对齐后的数据(如果可对齐)"""
        with self._lock:
            exchange = data.exchange
            local_time = data.local_time
            
            # 更新最新数据
            self.latest_data[exchange] = data
            
            if exchange != self.base_exchange:
                # 计算时钟偏移
                # 假设:同一时刻,各交易所收到同一事件的本地时间应相近
                # 偏移 = 交易所时间戳 - 本地接收时间 + 基准偏移
                base_data = self.latest_data.get(self.base_exchange)
                if base_data:
                    # 粗略估计偏移(实际场景需要更复杂的算法)
                    offset = data.timestamp - (local_time - base_data.local_time + base_data.timestamp)
                    self.clock_offsets[exchange].append(offset)
            
            # 返回对齐数据
            return self._align()
    
    def _align(self) -> Optional[AlignedData]:
        """将所有交易所数据对齐到基准时间戳"""
        base_data = self.latest_data.get(self.base_exchange)
        if not base_data:
            return None
        
        base_time = base_data.timestamp
        
        aligned = {}
        max_latency = 0
        
        for exchange, data in self.latest_data.items():
            if data is None:
                aligned[exchange] = None
                continue
            
            # 计算时钟偏差的中位数
            offsets = list(self.clock_offsets.get(exchange, []))
            clock_correction = sum(offsets) / len(offsets) if offsets else 0
            
            # 对齐后的时间戳
            adjusted_time = data.timestamp - clock_correction
            latency = abs(adjusted_time - base_time) * 1000  # 毫秒
            
            aligned[exchange] = data
            max_latency = max(max_latency, latency)
        
        return AlignedData(
            base_timestamp=base_time,
            data=aligned,
            latency_ms=max_latency
        )
    
    def get_spread_opportunity(self, symbol: str, min_spread: float = 0.001) -> Optional[Dict]:
        """
        检测跨交易所套利机会
        要求:所有交易所数据已对齐,延迟小于阈值
        """
        with self._lock:
            aligned = self._align()
            if not aligned or aligned.latency_ms > 100:  # 超过100ms的对齐数据不可用
                return None
            
            prices = {}
            for exchange, data in aligned.data.items():
                if data and data.symbol == symbol:
                    prices[exchange] = data.price
            
            if len(prices) < 2:
                return None
            
            max_price = max(prices.values())
            min_price = min(prices.values())
            spread = (max_price - min_price) / min_price
            
            if spread >= min_spread:
                max_ex = [k for k, v in prices.items() if v == max_price][0]
                min_ex = [k for k, v in prices.items() if v == min_price][0]
                
                return {
                    "symbol": symbol,
                    "buy_exchange": min_ex,
                    "sell_exchange": max_ex,
                    "buy_price": prices[min_ex],
                    "sell_price": prices[max_ex],
                    "spread_bps": round(spread * 10000, 2),
                    "latency_ms": round(aligned.latency_ms, 2),
                    "timestamp": aligned.base_timestamp
                }
            
            return None

常见报错排查

错误 1:Connection Refused (10061/10060)

错误信息websockets.exceptions.ConnectionRefusedError: [WinError 10061]ConnectionResetError: [Errno 104]

原因分析

解决方案

# 检查 IP 是否被封禁
import requests

def check_ban_status():
    # Binance IP 检查
    try:
        r = requests.get("https://api.binance.com/wapi/v3/apiKey.html", 
                        headers={"X-MBX-APIKEY": "YOUR_KEY"})
        print(f"Binance API 状态: {r.status_code}")
    except Exception as e:
        print(f"可能已被封禁: {e}")

添加重试和备用端点

ENDPOINTS_BINANCE = [ "wss://stream.binance.com:9443/ws", "wss://stream.binance.com/ws", # 备用 "wss://stream.bnbchain.org/ws", # BNB Chain 端点 ]

使用健康检查选择最优端点

import asyncio import aiohttp async def health_check(endpoint): try: async with aiohttp.ClientSession() as session: start = asyncio.get_event_loop().time() async with session.ws_connect(endpoint.replace('wss://', 'https://')) as ws: latency = (asyncio.get_event_loop().time() - start) * 1000 return (endpoint, latency, True) except: return (endpoint, 9999, False) async def select_best_endpoint(): results = await asyncio.gather(*[health_check(ep) for ep in ENDPOINTS_BINANCE]) available = [r for r in results if r[2]] if available: return min(available, key=lambda x: x[1])[0] raise Exception("所有端点均不可用")

错误 2:429 Too Many Requests

错误信息WebSocket connection closed: 1008: Too Many Requests

原因分析

解决方案

import asyncio
import threading

class RateLimitedConnection:
    """带限流控制的连接管理器"""
    
    def __init__(self, max_connections_per_minute=4, max_subscriptions_per_second=10):
        self.connection_semaphore = asyncio.Semaphore(max_connections_per_minute)
        self.subscription_limiter = AsyncRateLimiter(max_subscriptions_per_second)
        self._connection_count = 0
        self._last_reset = time.time()
        self._lock = threading.Lock()
    
    async def acquire_connection(self):
        """获取连接许可(带自动重置计数)"""
        async with self.connection_semaphore:
            with self._lock:
                current_time = time.time()
                if current_time - self._last_reset > 60:
                    self._connection_count = 0
                    self._last_reset = current_time
                
                if self._connection_count >= 4:
                    wait_time = 60 - (current_time - self._last_reset)
                    print(f"连接数超限,等待 {wait_time:.0f} 秒")
                    await asyncio.sleep(wait_time)
                    self._connection_count = 0
                    self._last_reset = time.time()
                
                self._connection_count += 1
            
            return True
    
    async def subscribe(self, ws, channels):
        """限流订阅"""
        for channel in channels:
            await self.subscription_limiter.acquire()
            await ws.send(json.dumps({"op": "subscribe", "args": [channel]}))
            print(f"订阅: {channel}")
            await asyncio.sleep(0.2)  # 每次订阅间隔 200ms

class AsyncRateLimiter:
    """异步速率限制器"""
    
    def __init__(self, max_per_second):
        self.max_per_second = max_per_second
        self.interval = 1.0 / max_per_second
        self.last_execution = 0
        self._lock = asyncio.Lock()
    
    async def acquire(self):
        async with self._lock:
            now = asyncio.get_event_loop().time()
            wait_time = self.last_execution + self.interval - now
            if wait_time > 0:
                await asyncio.sleep(wait_time)
            self.last_execution = asyncio.get_event_loop().time()

错误 3:数据乱序与消息丢失

错误信息:订单簿更新序号跳号、深度数据出现负数数量

原因分析

解决方案

class OrderbookManager:
    """
    订单簿状态管理器
    包含序号验证和数据完整性检查
    """
    
    def __init__(self, symbol: str, snapshot_depth: int = 20):
        self.symbol = symbol
        self.snapshot_depth = snapshot_depth
        self.bids = {}  # {price: quantity}
        self.asks = {}
        self.last_update_id = 0
        self.last_sequence = 0
        self._pending_updates = []
        self._is_snapshot_loaded = False
    
    def apply_snapshot(self, snapshot: dict):
        """加载完整快照"""
        self.bids.clear()
        self.asks.clear()
        
        # Binance 格式
        for price, qty in snapshot.get('bids', []):
            self.bids[float(price)] = float(qty)
        for price, qty in snapshot.get('asks', []):
            self.asks[float(price)] = float(qty)
        
        self.last_update_id = snapshot.get('lastUpdateId', 0)
        self.last_sequence = self.last_update_id
        self._is_snapshot_loaded = True
        self._process_pending()
    
    def apply_update(self, update: dict) -> bool:
        """
        应用增量更新
        返回值表示是否成功应用
        """
        if not self._is_snapshot_loaded:
            self._pending_updates.append(update)
            return False
        
        # 序号验证
        new_seq = update.get('u') or update.get('seq') or update.get('sequence', 0)
        
        # 允许序号跳跃(跳过丢失的消息,直接使用最新状态)
        # 但拒绝序号倒退
        if new_seq <= self.last_sequence and self.last_sequence > 0:
            print(f"序号倒退: {new_seq} < {self.last_sequence}")
            return False
        
        self.last_sequence = new_seq
        
        # 应用更新
        for price, qty in update.get('b', update.get('bids', [])):
            price, qty = float(price), float(qty)
            if qty == 0:
                self.bids.pop(price, None)
            else:
                if qty < 0:
                    print(f"非法数量(负数): {qty}")
                    return False
                self.bids[price] = qty
        
        for price, qty in update.get('a', update.get('asks', [])):
            price, qty = float(price), float(qty)
            if qty == 0:
                self.asks.pop(price, None)
            else:
                if qty < 0:
                    print(f"非法数量(负数): {qty}")
                    return False
                self.asks[price] = qty
        
        return True
    
    def _process_pending(self):
        """处理积压的更新"""
        while self._pending_updates:
            update = self._pending_updates.pop(0)
            self.apply_update(update)
    
    def get_top_of_book(self) -> dict:
        """获取最佳买卖价"""
        if not self.bids or not self.asks:
            return {}
        
        best_bid = max(self.bids.items(), key=lambda x: x[0])
        best_ask = min(self.asks.items(), key=lambda x: x[0])
        
        return {
            "symbol": self.symbol,
            "bid_price": best_bid[0],
            "bid_qty": best_bid[1],
            "ask_price": best_ask[0],
            "ask_qty": best_ask[1],
            "spread": best_ask[0] - best_bid[0],