From a862aec4f5cc5f6d7273fc89c5508f2553b5070d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Tue, 17 Feb 2026 23:28:28 +0800 Subject: [PATCH] =?UTF-8?q?feat(binance=5Fclient):=20=E4=BC=98=E5=8C=96=20?= =?UTF-8?q?listenKey=20=E5=88=9B=E5=BB=BA=E4=B8=8E=E5=BB=B6=E9=95=BF?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81=20WebSocket=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 在 `binance_client.py` 中更新 `create_futures_listen_key` 和 `keepalive_futures_listen_key` 方法,新增优先使用 WebSocket API 的功能,若 WebSocket 不可用则回退到 REST API。增强了错误处理和日志记录,确保在请求失败时提供更清晰的反馈。此改动提升了 listenKey 管理的灵活性和系统的稳定性。 --- backend/查看同步日志.sh | 39 +++++++++ trading_system/binance_client.py | 139 +++++++++++++++++++++++++++---- 诊断日志问题.md | 124 +++++++++++++++++++++++++++ 3 files changed, 288 insertions(+), 14 deletions(-) create mode 100755 backend/查看同步日志.sh create mode 100644 诊断日志问题.md diff --git a/backend/查看同步日志.sh b/backend/查看同步日志.sh new file mode 100755 index 0000000..4a5d9de --- /dev/null +++ b/backend/查看同步日志.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# 查看同步订单日志的便捷脚本 + +cd "$(dirname "$0")" + +echo "=== 同步订单日志查看工具 ===" +echo "" + +# 检查日志文件是否存在 +if [ ! -f "logs/api.log" ]; then + echo "⚠️ 日志文件不存在: logs/api.log" + echo " 请先启动 backend 服务" + exit 1 +fi + +echo "日志文件位置:" +echo " - Python 应用日志: backend/logs/api.log" +echo " - Uvicorn 服务器日志: backend/logs/uvicorn.log" +echo "" + +# 显示最近的同步日志 +echo "=== 最近的同步订单日志(最后 50 行)===" +echo "" +tail -50 logs/api.log | grep -i "同步\|sync\|订单\|order" --color=always || echo "未找到同步相关日志" + +echo "" +echo "=== 使用说明 ===" +echo "" +echo "实时查看同步日志:" +echo " tail -f logs/api.log | grep -i '同步\|sync'" +echo "" +echo "查看最近的同步日志:" +echo " tail -100 logs/api.log | grep -i '同步\|sync'" +echo "" +echo "查看特定时间的同步日志:" +echo " grep '2026-02-17 23:' logs/api.log | grep -i '同步\|sync'" +echo "" +echo "查看所有同步相关日志(包括详细信息):" +echo " grep -i '同步\|sync\|订单\|order' logs/api.log | tail -100" diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 7787345..e1cf289 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -386,20 +386,38 @@ class BinanceClient: """合约 REST 根地址(用于 listenKey 等)""" return "https://testnet.binancefuture.com" if self.testnet else "https://fapi.binance.com" - async def create_futures_listen_key(self) -> Optional[str]: - """创建 U 本位合约 User Data Stream listenKey(用于 WS 订阅订单/持仓推送)。60 分钟无 keepalive 会失效。""" + async def create_futures_listen_key(self, prefer_ws: bool = True) -> Optional[str]: + """ + 创建 U 本位合约 User Data Stream listenKey(用于 WS 订阅订单/持仓推送)。60 分钟无 keepalive 会失效。 + + 优先使用 WebSocket API(如果 WSTradeClient 已连接),否则使用 REST API。 + 根据币安文档:如果该帐户具有有效的listenKey,则将返回该listenKey并将其有效期延长60分钟。 + + Args: + prefer_ws: 是否优先使用 WebSocket API(默认 True)。如果 WebSocket 不可用,自动回退到 REST API。 + """ if not self.api_key: return None + + # 方法1: WebSocket API(优先,如果 WSTradeClient 已连接) + if prefer_ws: + ws_result = await self._create_listen_key_via_ws() + if ws_result: + return ws_result + # WebSocket 不可用,回退到 REST API + logger.debug("WSTradeClient 未连接或 WebSocket API 失败,回退到 REST API...") + + # 方法2: REST API(备选方案) try: import aiohttp url = f"{self._futures_base_url()}/fapi/v1/listenKey" headers = {"X-MBX-APIKEY": self.api_key} async with aiohttp.ClientSession() as session: - async with session.post(url, headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp: + async with session.post(url, headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp: text = await resp.text() if resp.status != 200: logger.warning( - "create_futures_listen_key 失败 status=%s body=%s", + "create_futures_listen_key (REST) 失败 status=%s body=%s", resp.status, (text[:500] if text else ""), ) return None @@ -409,30 +427,85 @@ class BinanceClient: data = {} key = data.get("listenKey") if isinstance(data, dict) else None if key: - logger.info("✓ 合约 User Data Stream listenKey 已创建") + logger.info("✓ 合约 User Data Stream listenKey 已创建 (REST)") return key except asyncio.TimeoutError: - logger.warning("create_futures_listen_key 失败: 请求超时(可检查该账号网络或代理)") + logger.warning("create_futures_listen_key (REST) 失败: 请求超时(30秒)") return None except Exception as e: err_msg = getattr(e, "message", str(e)) or repr(e) logger.warning( - "create_futures_listen_key 失败: %s - %s", + "create_futures_listen_key (REST) 失败: %s - %s", + type(e).__name__, err_msg, + ) + return None + + async def _create_listen_key_via_ws(self) -> Optional[str]: + """通过 WebSocket API 创建 listenKey(优先方案)。""" + if not self.api_key: + return None + + # 检查是否有可用的 WSTradeClient + ws_client = getattr(self, "_ws_trade_client", None) + if not ws_client: + return None + + # 检查连接状态(如果未连接,返回 None 让调用方回退到 REST) + if not ws_client.is_connected(): + return None + + try: + # 使用 WebSocket API: userDataStream.start + result = await ws_client._send_request( + "userDataStream.start", + {"apiKey": self.api_key}, + timeout=30.0 + ) + if result and isinstance(result, dict): + listen_key = result.get("listenKey") + if listen_key: + logger.info("✓ 合约 User Data Stream listenKey 已创建 (WebSocket API)") + return listen_key + logger.debug("WebSocket API 创建 listenKey 返回结果格式异常") + return None + except Exception as e: + err_msg = getattr(e, "message", str(e)) or repr(e) + logger.debug( + "create_futures_listen_key (WebSocket API) 失败: %s - %s", type(e).__name__, err_msg, - exc_info=logger.isEnabledFor(logging.DEBUG), ) return None - async def keepalive_futures_listen_key(self, listen_key: str): - """延长 listenKey 有效期(文档:延长至本次调用后 60 分钟)。返回 (ok: bool, code_1125: bool),-1125 表示 key 不存在需换新。""" + async def keepalive_futures_listen_key(self, listen_key: str, prefer_ws: bool = True): + """ + 延长 listenKey 有效期(文档:延长至本次调用后 60 分钟)。返回 (ok: bool, code_1125: bool),-1125 表示 key 不存在需换新。 + + 优先使用 WebSocket API(如果 WSTradeClient 已连接),否则使用 REST API。 + + Args: + prefer_ws: 是否优先使用 WebSocket API(默认 True)。如果 WebSocket 不可用,自动回退到 REST API。 + """ if not self.api_key or not listen_key: return False, False + + # 方法1: WebSocket API(优先,如果 WSTradeClient 已连接) + if prefer_ws: + ws_result = await self._keepalive_listen_key_via_ws(listen_key) + if ws_result[0]: # 如果成功 + return ws_result + # WebSocket 不可用或失败,回退到 REST API + if ws_result[1]: # code_1125 + # WebSocket 返回 -1125,说明 key 不存在,直接返回 + return False, True + logger.debug("WSTradeClient 未连接或 WebSocket API 失败,回退到 REST API...") + + # 方法2: REST API(备选方案) try: import aiohttp url = f"{self._futures_base_url()}/fapi/v1/listenKey" headers = {"X-MBX-APIKEY": self.api_key} async with aiohttp.ClientSession() as session: - async with session.put(f"{url}?listenKey={listen_key}", headers=headers, timeout=aiohttp.ClientTimeout(total=10)) as resp: + async with session.put(f"{url}?listenKey={listen_key}", headers=headers, timeout=aiohttp.ClientTimeout(total=30)) as resp: text = await resp.text() ok = resp.status == 200 code_1125 = False @@ -442,11 +515,49 @@ class BinanceClient: code_1125 = int(data.get("code", 0)) == -1125 except Exception: pass - logger.debug(f"keepalive_futures_listen_key 失败 status={resp.status} body={text}") + logger.debug(f"keepalive_futures_listen_key (REST) 失败 status={resp.status} body={text}") return ok, code_1125 - except Exception as e: - logger.debug(f"keepalive_futures_listen_key 失败: {e}") + except asyncio.TimeoutError: + logger.warning("keepalive_futures_listen_key (REST) 失败: 请求超时(30秒)") return False, False + except Exception as e: + logger.debug(f"keepalive_futures_listen_key (REST) 失败: {e}") + return False, False + + async def _keepalive_listen_key_via_ws(self, listen_key: str) -> tuple[bool, bool]: + """通过 WebSocket API 延长 listenKey 有效期(优先方案)。""" + if not self.api_key or not listen_key: + return False, False + + # 检查是否有可用的 WSTradeClient + ws_client = getattr(self, "_ws_trade_client", None) + if not ws_client: + return False, False + + # 检查连接状态(如果未连接,返回 False 让调用方回退到 REST) + if not ws_client.is_connected(): + return False, False + + try: + # 使用 WebSocket API: userDataStream.ping + result = await ws_client._send_request( + "userDataStream.ping", + {"apiKey": self.api_key}, + timeout=30.0 + ) + if result: + logger.debug("✓ listenKey keepalive 成功 (WebSocket API)") + return True, False + return False, False + except Exception as e: + err_msg = getattr(e, "message", str(e)) or repr(e) + error_code = getattr(e, "code", None) + code_1125 = (error_code == -1125) if error_code else False + logger.debug( + "keepalive_futures_listen_key (WebSocket API) 失败: %s - %s", + type(e).__name__, err_msg, + ) + return False, code_1125 @staticmethod def _to_bool(value: Any) -> Optional[bool]: diff --git a/诊断日志问题.md b/诊断日志问题.md new file mode 100644 index 0000000..8a4324f --- /dev/null +++ b/诊断日志问题.md @@ -0,0 +1,124 @@ +# 日志问题分析 + +## 1. IPUSDT 持仓状态分析 + +**日志信息**: +``` +IPUSDT [实时监控] 诊断: + • ROE(保证金盈亏): -5.83% (用户关注) + • 价格变动: -1.46% (实际币价涨跌) + • 杠杆倍数: 4.0x (放大倍数) + • 当前价: 1.1490 | 入场价: 1.1660 + • 止损价: 1.1310 (目标ROE: -12.00%) + • 触发止损: NO +``` + +**分析**: +- ✅ **计算验证**: + - 价格变动: (1.1490 - 1.1660) / 1.1660 = -1.46% ✓ + - ROE: -1.46% × 4.0 = -5.84% ≈ -5.83% ✓ + +- ✅ **止损状态**: + - 当前价 1.1490 > 止损价 1.1310,未触发止损 ✓ + - 距离止损: (1.1490 - 1.1310) / 1.1310 = 1.59% + - 如果价格继续下跌到 1.1310,ROE 将达到 -12.00% + +- ⚠️ **风险提示**: + - 当前亏损 -5.83%,距离止损还有 1.59% 的价格空间 + - 如果价格继续下跌,可能会触发止损 + +## 2. User Data Stream listenKey 创建失败 + +**错误信息**: +``` +create_futures_listen_key 失败: 请求超时(可检查该账号网络或代理) +UserDataStream(account_id=2): 重新创建 listenKey 失败,60s 后重试 +``` + +**可能原因**: +1. **网络连接问题**: + - 到币安 API 的网络不稳定 + - 代理设置问题 + - 防火墙阻止连接 + +2. **API 权限问题**: + - API Key 未启用 "Enable Reading" + - API Key 未启用 "Enable Futures" + - API Key 已过期或被禁用 + +3. **IP 白名单限制**: + - 当前 IP 不在 API Key 的 IP 白名单中 + - IP 白名单配置错误 + +4. **币安服务问题**: + - 币安 API 服务暂时不可用 + - API 限流或维护中 + +**影响**: +- ❌ 无法实时接收订单/持仓/余额推送 +- ❌ 订单号可能无法及时同步到数据库 +- ⚠️ 系统会每 60 秒重试创建 listenKey + +## 3. 网络超时问题 + +**日志显示**: +- 大量 `TimeoutError` 错误 +- 获取持仓信息失败(重试 7 次后失败) +- 获取成交记录失败(重试 5 次后失败) + +**建议排查步骤**: + +### 步骤 1: 检查网络连接 +```bash +# 测试币安 API 连接 +curl -I https://fapi.binance.com/fapi/v1/ping + +# 测试 listenKey 创建(需要 API Key) +curl -X POST "https://fapi.binance.com/fapi/v1/listenKey" \ + -H "X-MBX-APIKEY: YOUR_API_KEY" +``` + +### 步骤 2: 检查 API Key 权限 +1. 登录币安账户 +2. 进入 API 管理页面 +3. 检查账号 2 的 API Key: + - ✅ Enable Reading + - ✅ Enable Futures + - ✅ IP 白名单设置(如果启用了) + +### 步骤 3: 运行诊断工具 +```bash +cd trading_system +python3 -m trading_system.check_user_data_stream +``` + +### 步骤 4: 检查代理设置 +如果使用了代理,检查: +- 代理服务器是否正常运行 +- 代理配置是否正确 +- 代理是否支持 HTTPS 连接 + +## 4. 建议解决方案 + +### 短期方案(立即执行): +1. **检查网络连接**: 确认服务器到币安的网络是否正常 +2. **验证 API Key**: 确认账号 2 的 API Key 权限和 IP 白名单 +3. **增加超时时间**: 如果网络较慢,可以增加超时时间(当前 10 秒) + +### 长期方案(优化): +1. **增加重试机制**: 对于 listenKey 创建失败,增加指数退避重试 +2. **监控告警**: 当 listenKey 创建失败时发送告警 +3. **降级方案**: 当 WS 不可用时,增加 REST API 轮询频率 + +## 5. 查看详细日志 + +```bash +# 查看最近的 listenKey 相关日志 +tail -100 trading_2.out.log | grep -i "listen_key\|UserDataStream" + +# 查看 IPUSDT 相关日志 +tail -100 trading_2.out.log | grep -i "IPUSDT" + +# 查看所有超时错误 +tail -200 trading_2.out.log | grep -i "TimeoutError" +```