feat(binance_client): 优化 listenKey 创建与延长逻辑,支持 WebSocket API

在 `binance_client.py` 中更新 `create_futures_listen_key` 和 `keepalive_futures_listen_key` 方法,新增优先使用 WebSocket API 的功能,若 WebSocket 不可用则回退到 REST API。增强了错误处理和日志记录,确保在请求失败时提供更清晰的反馈。此改动提升了 listenKey 管理的灵活性和系统的稳定性。
This commit is contained in:
薇薇安 2026-02-17 23:28:28 +08:00
parent c7f1361d99
commit a862aec4f5
3 changed files with 288 additions and 14 deletions

39
backend/查看同步日志.sh Executable file
View File

@ -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"

View File

@ -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,
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 _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,
)
return None
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]:

124
诊断日志问题.md Normal file
View File

@ -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.1310ROE 将达到 -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"
```