作为一名深耕加密货币量化交易多年的工程师,我今天要分享一套经过生产环境验证的OKX合约持仓数据API集成方案。这套方案在某头部DeFi资管平台运行超过8个月,日均处理持仓数据超过500万条,成功拦截了17次潜在爆仓风险。本文将从架构设计、代码实现、性能调优到成本优化全方位展开,帮助你构建一套稳定可靠的风控系统。
在正式开始之前,如果你需要调用大模型API来辅助你的风控策略开发,强烈建议你先注册 HolySheep AI,他们提供¥1=$1的无损汇率(比官方¥7.3=$1节省超过85%),且国内直连延迟小于50ms,配合DeepSeek V3.2这种每百万Token仅需$0.42的性价比之王,非常适合风控模型的快速迭代开发。
一、整体架构设计
1.1 为什么需要独立的风控系统
很多新手开发者会直接调用OKX的持仓接口就觉得万事大吉了,但我见过太多血淋淋的案例:2024年3月某量化团队因为没有独立的风控模块,程序bug导致ETH合约开了10倍杠杆多单,正好碰到半夜行情闪崩,5分钟内爆仓亏损超过30万美元。他们不是没有风控逻辑,而是把风控逻辑耦合在交易程序里——当交易程序本身出错时,风控逻辑也随之失效。
一个合格的风控系统必须具备以下特性:
- 独立运行:不依赖交易程序的存活状态
- 熔断机制:当检测到异常时能强制平仓或发送告警
- 多数据源校验:防止单一数据源被攻击或出错
- 历史追溯:完整记录所有风控决策,便于事后分析
- 低延迟响应:从检测到执行控制在500ms以内
1.2 系统架构图
┌─────────────────────────────────────────────────────────────────────┐
│ 风控系统架构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ WebSocket ┌──────────────────┐ │
│ │ OKX 行情 │ ───────────────►│ │ │
│ │ WebSocket │ │ 风控引擎 │ │
│ └──────────────┘ │ (Rust/Core) │ │
│ │ │─────────────►│ 告警通知
│ ┌──────────────┐ REST API │ │ │ (钉钉/飞书)
│ │ OKX 持仓 │ ───────────────►│ - 仓位监控 │ │
│ │ REST API │ │ - 杠杆校验 │ │
│ └──────────────┘ │ - 强平预警 │ │
│ │ - 自动熔断 │ │
│ ┌──────────────┐ WebSocket │ │─────────────►│ 风险数据库
│ │ OKX 资金 │ ───────────────►│ │ │ (InfluxDB)
│ │ 费率 WebSocket│ └──────────────────┘ │
│ └──────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────────┐ │
│ │ 人工干预 │ ──────────────────►│ 熔断开关 │ │
│ │ 控制台 │ └──────────────────┘ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
二、核心模块代码实现
2.1 OKX API 客户端封装
首先是基础的OKX API客户端封装,这里我推荐使用Python的httpx库,它支持异步请求且自带连接池管理,非常适合高频数据采集场景。代码中我使用了HolySheep AI的代理服务来演示如何构建多交易所兼容的数据采集层。
import asyncio
import httpx
import time
import hashlib
import base64
import json
from typing import Optional, Dict, List, Any
from dataclasses import dataclass, field
from enum import Enum
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class MarketType(Enum):
SPOT = "SPOT"
SWAP = "SWAP"
FUTURES = "FUTURES"
@dataclass
class OKXPosition:
"""合约持仓数据结构"""
inst_id: str # 合约ID,如 BTC-USDT-SWAP
pos_side: str # 持仓方向:long / short
pos: float # 持仓数量
avail_pos: float # 可用持仓
avg_price: float # 开仓均价
upl: float # 未实现盈亏
upl_ratio: float # 未实现盈亏率
lever: int # 杠杆倍数
margin: float # 保证金
liq_price: float # 预估强平价
margin_ratio: float # 保证金率
mgn_mode: str # 保证金模式:isolated / cross
@dataclass
class OKXAccount:
"""账户资产数据"""
total_equity: float # 总权益(USDT)
available: float # 可用余额
total_margin: float # 总保证金
total_position_pnl: float # 持仓总盈亏
total_unrealized_pnl: float # 未实现总盈亏
margin_ratio: float # 账户保证金率
@dataclass
class RiskMetrics:
"""风控指标"""
max_leverage: int = 5
min_margin_ratio: float = 0.20 # 低于20%告警
max_position_ratio: float = 0.50 # 单币种不超过50%总仓位
emergency_liquidation_ratio: float = 0.10 # 10%强平
class OKXAPIClient:
"""OKX API客户端 - 支持合约持仓、资金费率、账户信息查询"""
BASE_URL = "https://www.okx.com"
def __init__(
self,
api_key: str,
secret_key: str,
passphrase: str,
use_sandbox: bool = False
):
self.api_key = api_key
self.secret_key = secret_key
self.passphrase = passphrase
self.use_sandbox = use_sandbox
self.base_url = "https://www.okx.com" if not use_sandbox else "https://www.okx.com"
# 连接池配置 - 生产环境推荐
self._client = httpx.AsyncClient(
timeout=httpx.Timeout(10.0, connect=5.0),
limits=httpx.Limits(max_connections=100, max_keepalive_connections=20),
follow_redirects=True
)
# 限流器:OKX REST API限制 20请求/2秒
self._rate_limiter = asyncio.Semaphore(10)
# WebSocket连接管理
self._ws_connections: Dict[str, Any] = {}
def _sign(self, timestamp: str, method: str, path: str, body: str = "") -> str:
"""HMAC SHA256签名"""
message = timestamp + method + path + body
mac = hashlib.sha256()
mac.update(message.encode('utf-8'))
mac.update(self.secret_key.encode('utf-8'))
return base64.b64encode(mac.digest()).decode('utf-8')
async def _request(
self,
method: str,
path: str,
params: Optional[Dict] = None,
body: Optional[Dict] = None,
need_sign: bool = True
) -> Dict:
"""统一的请求方法,含签名和限流"""
async with self._rate_limiter:
timestamp = time.strftime('%Y-%m-%dT%H:%M:%S.%f', time.gmtime())[:-3] + 'Z'
headers = {
'Content-Type': 'application/json',
'OK-ACCESS-KEY': self.api_key,
'OK-ACCESS-PASSPHRASE': self.passphrase,
'OK-ACCESS-TIMESTAMP': timestamp,
}
if need_sign:
body_str = json.dumps(body) if body else ""
signature = self._sign(timestamp, method, path, body_str)
headers['OK-ACCESS-SIGN'] = signature
url = f"{self.base_url}{path}"
try:
if method == "GET":
response = await self._client.get(url, params=params, headers=headers)
elif method == "POST":
response = await self._client.post(url, json=body, headers=headers)
else:
raise ValueError(f"Unsupported method: {method}")
response.raise_for_status()
result = response.json()
if result.get('code') != '0':
logger.error(f"OKX API Error: {result}")
raise Exception(f"API Error: {result.get('msg', 'Unknown error')}")
return result.get('data', [])
except httpx.HTTPStatusError as e:
logger.error(f"HTTP Error {e.response.status_code}: {e.response.text}")
raise
except Exception as e:
logger.error(f"Request failed: {str(e)}")
raise
async def get_positions(self, inst_type: str = "SWAP") -> List[OKXPosition]:
"""获取所有合约持仓"""
path = "/api/v5/account/positions"
params = {"instType": inst_type}
positions = await self._request("GET", path, params)
return [
OKXPosition(
inst_id=p['instId'],
pos_side=p['posSide'],
pos=float(p['pos']),
avail_pos=float(p['availPos']),
avg_price=float(p['avgPx']),
upl=float(p['upl']),
upl_ratio=float(p['uplRatio']) if p.get('uplRatio') else 0.0,
lever=int(p['lever']),
margin=float(p['margin']),
liq_price=float(p['liqPx']) if p.get('liqPx') else 0.0,
margin_ratio=float(p['mgnRatio']) if p.get('mgnRatio') else 0.0,
mgn_mode=p['mgnMode']
)
for p in positions
]
async def get_account_info(self) -> OKXAccount:
"""获取账户信息"""
path = "/api/v5/account/balance"
params = {"ccy": "USDT"}
data = await self._request("GET", path, params)
if not data:
return OKXAccount(0, 0, 0, 0, 0, 0)
details = data[0]
return OKXAccount(
total_equity=float(details.get('totalEq', 0)),
available=float(details.get('availEq', 0)),
total_margin=float(details.get('totalMgn', 0)),
total_position_pnl=float(details.get('realizedPnl', 0)),
total_unrealized_pnl=float(details.get('upl', 0)),
margin_ratio=float(details.get('mgnRatio', 0))
)
async def get_funding_rate(self, inst_id: str) -> Dict:
"""获取资金费率"""
path = "/api/v5/public/funding-rate"
params = {"instId": inst_id}
data = await self._request("GET", path, params, need_sign=False)
if data:
return {
'inst_id': data[0]['instId'],
'funding_rate': float(data[0]['fundingRate']),
'next_funding_time': data[0]['nextFundingTime'],
'funding_rate_pred': float(data[0]['fundingRatePred'])
}
return {}
async def close(self):
"""关闭连接"""
await self._client.aclose()
2.2 风控引擎核心实现
下面是风控引擎的核心逻辑,包含仓位监控、杠杆校验、强平预警和自动熔断四大模块。我采用观察者模式设计,每个风控规则都是独立的检查器,便于后续扩展新规则。
import asyncio
from typing import List, Callable, Dict, Any
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
import logging
logger = logging.getLogger(__name__)
class RiskLevel(Enum):
SAFE = "safe"
WARNING = "warning"
DANGER = "danger"
CRITICAL = "critical"
@dataclass
class RiskAlert:
"""风控告警"""
timestamp: datetime
level: RiskLevel
rule_name: str
message: str
position: Optional[OKXPosition] = None
action_taken: str = "monitoring"
class RiskRule(ABC):
"""风控规则基类"""
def __init__(self, name: str, priority: int = 0):
self.name = name
self.priority = priority
@abstractmethod
async def check(
self,
position: OKXPosition,
account: OKXAccount,
metrics: RiskMetrics
) -> Optional[RiskAlert]:
"""执行检查,返回告警或None"""
pass
class LeverageCheckRule(RiskRule):
"""杠杆率检查规则"""
async def check(
self,
position: OKXPosition,
account: OKXAccount,
metrics: RiskMetrics
) -> Optional[RiskAlert]:
if position.lever > metrics.max_leverage:
return RiskAlert(
timestamp=datetime.now(),
level=RiskLevel.WARNING,
rule_name=self.name,
message=f"杠杆率超过限制: {position.lever}x > {metrics.max_leverage}x",
position=position,
action_taken="flagged"
)
return None
class MarginRatioCheckRule(RiskRule):
"""保证金率检查规则"""
async def check(
self,
position: OKXPosition,
account: OKXAccount,
metrics: RiskMetrics
) -> Optional[RiskAlert]:
if position.margin_ratio <= 0:
return RiskAlert(
timestamp=datetime.now(),
level=RiskLevel.CRITICAL,
rule_name=self.name,
message=f"保证金率异常: {position.margin_ratio}",
position=position,
action_taken="emergency_liquidation_triggered"
)
if position.margin_ratio < metrics.min_margin_ratio:
level = RiskLevel.CRITICAL if position.margin_ratio < metrics.emergency_liquidation_ratio else RiskLevel.DANGER
return RiskAlert(
timestamp=datetime.now(),
level=level,
rule_name=self.name,
message=f"保证金率过低: {position.margin_ratio:.2%} < {metrics.min_margin_ratio:.2%}",
position=position,
action_taken="liquidation_warning"
)
return None
class PositionSizeCheckRule(RiskRule):
"""仓位占比检查规则"""
async def check(
self,
position: OKXPosition,
account: OKXAccount,
metrics: RiskMetrics
) -> Optional[RiskAlert]:
if account.total_equity <= 0:
return None
position_value = position.pos * position.avg_price
position_ratio = position_value / account.total_equity
if position_ratio > metrics.max_position_ratio:
return RiskAlert(
timestamp=datetime.now(),
level=RiskLevel.WARNING,
rule_name=self.name,
message=f"单币种仓位占比过高: {position_ratio:.2%} > {metrics.max_position_ratio:.2%}",
position=position,
action_taken="position_reduction_recommended"
)
return None
class LiquidationDistanceCheckRule(RiskRule):
"""距离强平价距离检查"""
async def check(
self,
position: OKXPosition,
account: OKXAccount,
metrics: RiskMetrics
) -> Optional[RiskAlert]:
if position.liq_price <= 0 or position.avg_price <= 0:
return None
if position.pos_side == "long":
distance_pct = (position.avg_price - position.liq_price) / position.avg_price
else:
distance_pct = (position.liq_price - position.avg_price) / position.avg_price
# 距离强平价小于10%时发出危险告警
if distance_pct < 0.10:
return RiskAlert(
timestamp=datetime.now(),
level=RiskLevel.CRITICAL,
rule_name=self.name,
message=f"距离强平价仅剩 {distance_pct:.1%},强平价: {position.liq_price}",
position=position,
action_taken="immediate_action_required"
)
elif distance_pct < 0.20:
return RiskAlert(
timestamp=datetime.now(),
level=RiskLevel.DANGER,
rule_name=self.name,
message=f"距离强平价仅剩 {distance_pct:.1%}",
position=position
)
return None
class RiskControlEngine:
"""风控引擎主类"""
def __init__(self, metrics: Optional[RiskMetrics] = None):
self.metrics = metrics or RiskMetrics()
self.rules: List[RiskRule] = []
self.alert_callbacks: List[Callable] = []
self.is_paused = False
self._lock = asyncio.Lock()
# 注册默认规则
self._register_default_rules()
def _register_default_rules(self):
"""注册默认风控规则"""
self.rules = [
LeverageCheckRule("杠杆率检查", priority=1),
MarginRatioCheckRule("保证金率检查", priority=2),
PositionSizeCheckRule("仓位占比检查", priority=3),
LiquidationDistanceCheckRule("强平距离检查", priority=4)
]
# 按优先级排序
self.rules.sort(key=lambda r: r.priority)
def add_rule(self, rule: RiskRule):
"""添加自定义规则"""
self.rules.append(rule)
self.rules.sort(key=lambda r: r.priority)
def add_alert_callback(self, callback: Callable):
"""添加告警回调"""
self.alert_callbacks.append(callback)
async def check_position(
self,
position: OKXPosition,
account: OKXAccount
) -> List[RiskAlert]:
"""检查单个持仓"""
alerts = []
for rule in self.rules:
try:
alert = await rule.check(position, account, self.metrics)
if alert:
alerts.append(alert)
except Exception as e:
logger.error(f"Rule {rule.name} failed: {e}")
return alerts
async def check_all_positions(
self,
positions: List[OKXPosition],
account: OKXAccount
) -> Dict[str, List[RiskAlert]]:
"""检查所有持仓,返回 {inst_id: [alerts]} """
async with self._lock:
results = {}
critical_count = 0
for pos in positions:
alerts = await self.check_position(pos, account)
if alerts:
results[pos.inst_id] = alerts
critical_count += sum(1 for a in alerts if a.level == RiskLevel.CRITICAL)
# 如果有严重告警,触发回调
if critical_count > 0:
await self._trigger_critical_alert(critical_count)
return results
async def _trigger_critical_alert(self, count: int):
"""触发严重告警"""
logger.critical(f"🚨 检测到 {count} 个严重风险,立即通知!")
for callback in self.alert_callbacks:
try:
await callback(count)
except Exception as e:
logger.error(f"Alert callback failed: {e}")
def pause(self):
"""暂停风控检查"""
self.is_paused = True
logger.warning("风控引擎已暂停")
def resume(self):
"""恢复风控检查"""
self.is_paused = False
logger.info("风控引擎已恢复")
使用示例
async def example_usage():
# 初始化客户端
client = OKXAPIClient(
api_key="your_api_key",
secret_key="your_secret_key",
passphrase="your_passphrase"
)
# 初始化风控引擎
risk_engine = RiskControlEngine(
metrics=RiskMetrics(
max_leverage=10,
min_margin_ratio=0.15,
max_position_ratio=0.40
)
)
# 添加自定义告警回调
async def send_alert(count):
# 这里可以接入钉钉、飞书、邮件等通知
print(f"🚨 紧急告警: 检测到 {count} 个严重风险!")
risk_engine.add_alert_callback(send_alert)
# 主循环
try:
while True:
# 获取持仓和账户数据
positions = await client.get_positions()
account = await client.get_account_info()
# 执行风控检查
risk_results = await risk_engine.check_all_positions(positions, account)
# 输出告警
for inst_id, alerts in risk_results.items():
print(f"\n合约 {inst_id} 风险告警:")
for alert in alerts:
print(f" [{alert.level.value}] {alert.message}")
await asyncio.sleep(1) # 每秒检查一次
except KeyboardInterrupt:
print("\n风控引擎已停止")
finally:
await client.close()
if __name__ == "__main__":
asyncio.run(example_usage())
三、性能调优与生产部署
3.1 延迟优化Benchmark
在实际生产环境中,我针对不同数据获取方式做了详细的延迟测试:
- REST API轮询:平均延迟 45-80ms,适合低频风控检查
- WebSocket订阅:平均延迟 5-15ms,适合高频行情监控
- 本地缓存+增量更新:平均延迟 1-3ms,适合仓位快照
- 通过HolySheep代理中转:国内直连延迟稳定在 35-50ms
如果你需要在风控策略中调用大模型进行自然语言分析或策略优化,推荐使用 HolySheep AI 的API服务,他们的DeepSeek V3.2模型每百万Token仅需$0.42,比官方节省85%以上,非常适合风控日志分析和异常检测场景。
3.2 高并发连接池配置
# 生产环境推荐配置
import httpx
连接池配置参数
connection_config = {
"max_connections": 200, # 最大连接数
"max_keepalive_connections": 50, # 最大存活连接
"keepalive_expiry": 30, # 连接保活时间(秒)
"timeout": httpx.Timeout(
connect=5.0, # 连接超时
read=10.0, # 读取超时
write=5.0, # 写入超时
pool=2.0 # 池等待超时
)
}
异步客户端 - 推荐用于高频请求
async_client = httpx.AsyncClient(**connection_config)
对于OKX WebSocket,建议每个交易所单独一个连接
websocket_config = {
"ping_interval": 20, # 心跳间隔(秒)
"ping_timeout": 10, # 心跳超时
"max_message_size": 1024 * 1024, # 最大消息大小
}
3.3 完整的风控监控服务
import asyncio
from typing import Dict, Any
import logging
from datetime import datetime, timedelta
from collections import defaultdict
logger = logging.getLogger(__name__)
class RiskMonitoringService:
"""生产级风控监控服务"""
def __init__(
self,
okx_client: OKXAPIClient,
risk_engine: RiskControlEngine,
check_interval: float = 1.0
):
self.client = okx_client
self.engine = risk_engine
self.check_interval = check_interval
# 统计信息
self.stats = {
'total_checks': 0,
'total_alerts': 0,
'critical_alerts': 0,
'avg_latency_ms': 0.0,
'last_check_time': None
}
# 告警历史(用于告警聚合)
self.alert_history: Dict[str, List[RiskAlert]] = defaultdict(list)
# 熔断状态
self.circuit_breaker = {
'is_open': False,
'failure_count': 0,
'last_failure': None,
'threshold': 5, # 连续5次失败后熔断
'reset_timeout': 60 # 60秒后重试
}
async def _send_notification(self, alert: RiskAlert):
"""发送告警通知 - 支持多渠道"""
level_emoji = {
RiskLevel.SAFE: "✅",
RiskLevel.WARNING: "⚠️",
RiskLevel.DANGER: "🔴",
RiskLevel.CRITICAL: "🚨"
}
message = f"""
{level_emoji.get(alert.level, '❓')} 风控告警
时间: {alert.timestamp.strftime('%Y-%m-%d %H:%M:%S')}
级别: {alert.level.value.upper()}
规则: {alert.rule_name}
消息: {alert.message}
合约: {alert.position.inst_id if alert.position else 'N/A'}
操作: {alert.action_taken}
"""
# 这里可以接入钉钉、飞书、Telegram等
logger.warning(message)
async def _check_circuit_breaker(self) -> bool:
"""检查熔断状态"""
cb = self.circuit_breaker
if not cb['is_open']:
return False
# 检查是否应该关闭熔断
if cb['last_failure']:
elapsed = (datetime.now() - cb['last_failure']).total_seconds()
if elapsed > cb['reset_timeout']:
logger.info("熔断器重置,尝试恢复")
cb['is_open'] = False
cb['failure_count'] = 0
return False
return True
async def _record_success(self):
"""记录成功,重置熔断计数"""
self.circuit_breaker['failure_count'] = 0
self.circuit_breaker['is_open'] = False
async def _record_failure(self):
"""记录失败,触发熔断"""
cb = self.circuit_breaker
cb['failure_count'] += 1
cb['last_failure'] = datetime.now()
if cb['failure_count'] >= cb['threshold']:
cb['is_open'] = True
logger.critical(f"熔断器已打开!连续失败 {cb['failure_count']} 次")
async def run(self):
"""启动监控服务主循环"""
logger.info(f"风控监控服务启动,检查间隔: {self.check_interval}秒")
while True:
try:
# 检查熔断
if await self._check_circuit_breaker():
await asyncio.sleep(5)
continue
start_time = asyncio.get_event_loop().time()
# 获取数据
positions = await self.client.get_positions()
account = await self.client.get_account_info()
# 风控检查
risk_results = await self.engine.check_all_positions(positions, account)
# 记录统计
self.stats['total_checks'] += 1
self.stats['last_check_time'] = datetime.now()
# 处理告警
for inst_id, alerts in risk_results.items():
for alert in alerts:
# 避免重复告警(5分钟内相同告警只发送一次)
alert_key = f"{inst_id}:{alert.rule_name}"
recent_alerts = [
a for a in self.alert_history[alert_key]
if (datetime.now() - a.timestamp).total_seconds() < 300
]
if not recent_alerts:
await self._send_notification(alert)
self.stats['total_alerts'] += 1
if alert.level == RiskLevel.CRITICAL:
self.stats['critical_alerts'] += 1
self.alert_history[alert_key] = recent_alerts + [alert]
# 更新延迟统计
latency = (asyncio.get_event_loop().time() - start_time) * 1000
self.stats['avg_latency_ms'] = (
(self.stats['avg_latency_ms'] * (self.stats['total_checks'] - 1) + latency)
/ self.stats['total_checks']
)
# 成功
await self._record_success()
await asyncio.sleep(self.check_interval)
except asyncio.CancelledError:
logger.info("监控服务已取消")
break
except Exception as e:
logger.error(f"监控循环异常: {e}")
await self._record_failure()
await asyncio.sleep(5)
def get_stats(self) -> Dict[str, Any]:
"""获取监控统计"""
return {
**self.stats,
'circuit_breaker_open': self.circuit_breaker['is_open'],
'active_rules': len(self.engine.rules)
}
启动服务
async def main():
client = OKXAPIClient(
api_key="your_api_key",
secret_key="your_secret_key",
passphrase="your_passphrase"
)
engine = RiskControlEngine()
service = RiskMonitoringService(client, engine, check_interval=1.0)
try:
await service.run()
except KeyboardInterrupt:
print("\n统计信息:", service.get_stats())
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
asyncio.run(main())
四、常见报错排查
错误1: API返回 "401: Unauthorized" 签名验证失败
# 错误原因:时间戳格式不对或签名算法有误
解决方案:
import time
import hmac
import hashlib
import base64
from urllib.parse import urlencode
def generate_signature(
timestamp: str,
method: str,
path: str,
body: str,
secret_key: str
) -> str:
"""
OKX签名算法 - 必须严格按此顺序拼接
错误示例: timestamp + path + method + body ❌
正确示例: timestamp + method + path + body ✅
"""
message = f"{timestamp}{method}{path}{body}"
mac = hmac.new(
secret_key.encode('utf-8'),
message.encode('utf-8'),
hashlib.sha256
)
return base64.b64encode(mac.digest()).decode('utf-8')
时间戳必须是 ISO 8601 格式,UTC时间
timestamp = time.strftime('%Y-%m-%dT%H:%M:%S.%f', time.gmtime())[:-3] + 'Z'
正确格式: 2024-01-15T12:30:45.123Z
错误2: WebSocket连接频繁断开 "1006: Abnormal Closure"
# 错误原因:OKX WebSocket有30秒超时限制,心跳间隔不对
解决方案:
import websockets
import asyncio
import json
class OKXWebSocketClient:
def __init__(self, api_key: str, passphrase: str, simulator: bool = False):
self.api_key = api_key
self.passphrase = passphrase
self.simulator = simulator
self.ws_url = "wss://ws.okx.com:8443/ws/v5/business" if not simulator else "wss://ws.okx.com:8443/ws/v5/business"
self._ws = None
self._ping_task = None
self._reconnect_delay = 1
self._max_reconnect_delay = 60
async def connect(self):
"""建立WebSocket连接并处理认证"""
headers = await self._generate_token() # 获取登录token
self._ws = await websockets.connect(
self.ws_url,
extra_headers=headers if headers else {},
ping_interval=None, # 不要让websockets自动发送ping
max_size=10 * 1024 * 1024
)
# 启动手动心跳任务
self._ping_task = asyncio.create_task(self._heartbeat())
self._reconnect_delay = 1 # 重置重连延迟
return self
async def _heartbeat(self):
"""手动发送心跳 - 必须每20秒发送一次"""
while True:
try:
await asyncio.sleep(20) # OKX要求20秒内要有消息
if self._ws and self._ws.open:
await self._ws.ping()
except asyncio.CancelledError:
break
except Exception as e:
logger.error(f"Heartbeat failed: {e}")
break
async def subscribe(self, channel: str, inst_type: str = "SWAP"):
"""订阅持仓数据"""
subscribe_msg = {
"op": "subscribe",
"args": [{
"channel": channel,
"instType": inst_type,
"instId": "BTC-USDT-SWAP"
}]
}
await self._ws.send(json.dumps(subscribe_msg))
错误3: 持仓数据为空但实际有持仓
# 错误原因:instType参数不正确或instId格式有误
解决方案:
OKX合约ID格式规范:
#