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:
parent
c7f1361d99
commit
a862aec4f5
39
backend/查看同步日志.sh
Executable file
39
backend/查看同步日志.sh
Executable 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"
|
||||||
|
|
@ -386,20 +386,38 @@ class BinanceClient:
|
||||||
"""合约 REST 根地址(用于 listenKey 等)"""
|
"""合约 REST 根地址(用于 listenKey 等)"""
|
||||||
return "https://testnet.binancefuture.com" if self.testnet else "https://fapi.binance.com"
|
return "https://testnet.binancefuture.com" if self.testnet else "https://fapi.binance.com"
|
||||||
|
|
||||||
async def create_futures_listen_key(self) -> Optional[str]:
|
async def create_futures_listen_key(self, prefer_ws: bool = True) -> Optional[str]:
|
||||||
"""创建 U 本位合约 User Data Stream listenKey(用于 WS 订阅订单/持仓推送)。60 分钟无 keepalive 会失效。"""
|
"""
|
||||||
|
创建 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:
|
if not self.api_key:
|
||||||
return None
|
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:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
url = f"{self._futures_base_url()}/fapi/v1/listenKey"
|
url = f"{self._futures_base_url()}/fapi/v1/listenKey"
|
||||||
headers = {"X-MBX-APIKEY": self.api_key}
|
headers = {"X-MBX-APIKEY": self.api_key}
|
||||||
async with aiohttp.ClientSession() as session:
|
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()
|
text = await resp.text()
|
||||||
if resp.status != 200:
|
if resp.status != 200:
|
||||||
logger.warning(
|
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 ""),
|
resp.status, (text[:500] if text else ""),
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
@ -409,30 +427,85 @@ class BinanceClient:
|
||||||
data = {}
|
data = {}
|
||||||
key = data.get("listenKey") if isinstance(data, dict) else None
|
key = data.get("listenKey") if isinstance(data, dict) else None
|
||||||
if key:
|
if key:
|
||||||
logger.info("✓ 合约 User Data Stream listenKey 已创建")
|
logger.info("✓ 合约 User Data Stream listenKey 已创建 (REST)")
|
||||||
return key
|
return key
|
||||||
except asyncio.TimeoutError:
|
except asyncio.TimeoutError:
|
||||||
logger.warning("create_futures_listen_key 失败: 请求超时(可检查该账号网络或代理)")
|
logger.warning("create_futures_listen_key (REST) 失败: 请求超时(30秒)")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
err_msg = getattr(e, "message", str(e)) or repr(e)
|
err_msg = getattr(e, "message", str(e)) or repr(e)
|
||||||
logger.warning(
|
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,
|
type(e).__name__, err_msg,
|
||||||
exc_info=logger.isEnabledFor(logging.DEBUG),
|
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def keepalive_futures_listen_key(self, listen_key: str):
|
async def keepalive_futures_listen_key(self, listen_key: str, prefer_ws: bool = True):
|
||||||
"""延长 listenKey 有效期(文档:延长至本次调用后 60 分钟)。返回 (ok: bool, code_1125: bool),-1125 表示 key 不存在需换新。"""
|
"""
|
||||||
|
延长 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:
|
if not self.api_key or not listen_key:
|
||||||
return False, False
|
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:
|
try:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
url = f"{self._futures_base_url()}/fapi/v1/listenKey"
|
url = f"{self._futures_base_url()}/fapi/v1/listenKey"
|
||||||
headers = {"X-MBX-APIKEY": self.api_key}
|
headers = {"X-MBX-APIKEY": self.api_key}
|
||||||
async with aiohttp.ClientSession() as session:
|
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()
|
text = await resp.text()
|
||||||
ok = resp.status == 200
|
ok = resp.status == 200
|
||||||
code_1125 = False
|
code_1125 = False
|
||||||
|
|
@ -442,11 +515,49 @@ class BinanceClient:
|
||||||
code_1125 = int(data.get("code", 0)) == -1125
|
code_1125 = int(data.get("code", 0)) == -1125
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
return ok, code_1125
|
||||||
except Exception as e:
|
except asyncio.TimeoutError:
|
||||||
logger.debug(f"keepalive_futures_listen_key 失败: {e}")
|
logger.warning("keepalive_futures_listen_key (REST) 失败: 请求超时(30秒)")
|
||||||
return False, False
|
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
|
@staticmethod
|
||||||
def _to_bool(value: Any) -> Optional[bool]:
|
def _to_bool(value: Any) -> Optional[bool]:
|
||||||
|
|
|
||||||
124
诊断日志问题.md
Normal file
124
诊断日志问题.md
Normal 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.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"
|
||||||
|
```
|
||||||
Loading…
Reference in New Issue
Block a user