arbitrage-engine/docs/LIVE_TRADING_REVIEW.md

403 lines
13 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 实盘交易系统代码审阅报告(第二轮)
**审阅分支**`review/live-trading-v2`
**审阅日期**2026-03-02
**审阅范围**
- `backend/live_executor.py`
- `backend/position_sync.py`
- `backend/risk_guard.py`
- `backend/signal_engine.py`
- `backend/main.py`
- `backend/trade_config.py`
- `frontend/app/live/page.tsx`
- `frontend/` 其余页面(重点检查权限、交易展示口径、调用链)
---
## 审阅结论(摘要)
本轮已验证上一轮多数问题已修复(如 SL 三次失败补救、TP1 检测逻辑、实盘高危接口 admin 校验、funding 周期窗口、CORS、日志文件输出等
但当前仍存在 **P0 级问题 6 项**(含上轮未彻底闭环与新暴露问题),涉及:
- 鉴权密钥默认值导致的管理员权限伪造风险
- 数据库密码明文 fallback 仍存在
- 紧急平仓路径在特定分支仍可能失败或不够安全
- TP1 后 SL 重挂失败时状态机错误前进
- 双策略同币叠仓导致真实仓位与本地记录错配
**结论:当前版本不可以上实盘。**
---
## 一、上一轮问题复检
### 已确认修复
- P0-1`live_executor.py` 已实现 SL 失败重试并在 3 次失败后紧急平仓。
- P0-2`position_sync.py` 已改为基于 `qty` 的 TP1 触发检测。
- P0-3`main.py` 实盘高危接口已接入 `_require_admin()`
- P1-2funding 追踪已改为按结算周期起始时间对齐。
- P1-3收到 NOTIFY 后不再强制 sleep。
- P2-2紧急指令文件删除时机已后移竞争窗口收窄。
- P2-3自动恢复增加了原因约束避免日限亏损被 API 恢复误清)。
- P3-1精度常量已抽取到 `trade_config.py`
- P3-3CORS 不再使用 `*`
- P3-4三个实盘进程已接入文件日志。
### 未彻底闭环或残留
- P0-4DB 密码 fallback 仍存在3 个实盘进程)。
- P0-5`close_all` 路径已修,但超时自动平仓路径仍未做数量精度处理。
- P2-1工作连接有重连`LISTEN` 连接仍无重连。
- P3-2部分 USDT 展示仍硬编码 `*2`,与 live_config 动态 1R 不一致。
---
## 二、P0 — 资金安全(必须修复)
### P0-1 JWT_SECRET 存在默认值,可伪造管理员 Token
- **文件**`backend/auth.py:15`
- **问题**`JWT_SECRET` 使用硬编码默认值,攻击者可离线签发合法 JWT伪造 `role=admin`
- **风险**:可直接调用 `/api/live/emergency-close`、`/api/live/config` 等高危接口。
**修复代码**
```python
# backend/auth.py
JWT_SECRET = os.getenv("JWT_SECRET")
if not JWT_SECRET or len(JWT_SECRET) < 32:
raise RuntimeError("JWT_SECRET 未配置或长度不足(>=32)")
```
---
### P0-2 DB_PASSWORD 明文 fallback 仍在三处
- **文件**
- `backend/live_executor.py:49`
- `backend/position_sync.py:42`
- `backend/risk_guard.py:47`
- **问题**:缺失环境变量时仍回退到固定密码。
- **风险**:环境误配即使用弱口令连接生产库,且密码已暴露于代码历史语义中。
**修复代码**(三文件同改):
```python
DB_PASSWORD = os.getenv("DB_PASSWORD")
if not DB_PASSWORD:
logger.error("DB_PASSWORD 未设置,拒绝启动")
sys.exit(1)
DB_CONFIG = {
"host": os.getenv("DB_HOST", "10.106.0.3"),
"port": int(os.getenv("DB_PORT", "5432")),
"dbname": os.getenv("DB_NAME", "arb_engine"),
"user": os.getenv("DB_USER", "arb"),
"password": DB_PASSWORD,
}
```
---
### P0-3 SL 三次失败后的紧急平仓未显式 reduceOnly 且未校验结果
- **文件**`backend/live_executor.py:423`(调用点),`backend/live_executor.py:190`(下单函数)
- **问题**:调用的是通用 `place_market_order`,未显式 `reduceOnly`,也未在失败后再升级处置。
- **风险**:极端市场/交易所拒单场景下,可能平仓失败后继续裸奔;在参数异常时也可能出现行为偏差。
**修复代码**
```python
# backend/live_executor.py
async def place_reduce_only_close(session: aiohttp.ClientSession, symbol: str, side: str):
pos = await get_position(session, symbol)
if not pos:
return {"msg": "no_position"}, 200
amt = abs(float(pos.get("positionAmt", 0)))
if amt <= 0:
return {"msg": "no_position"}, 200
prec = SYMBOL_PRECISION.get(symbol, {"qty": 3})
qty_str = f"{amt:.{prec['qty']}f}"
return await binance_request(session, "POST", "/fapi/v1/order", {
"symbol": symbol,
"side": side,
"type": "MARKET",
"quantity": qty_str,
"reduceOnly": "true",
})
# 替换原 SL 失败分支
if sl_status != 200:
logger.error(f"[{symbol}] ❌ SL 3次全部失败紧急reduceOnly平仓! data={sl_data}")
close_data, close_status = await place_reduce_only_close(session, symbol, close_side)
if close_status != 200:
_log_event(db_conn, "critical", "trade", "SL失败后紧急平仓也失败", symbol,
{"sl_data": str(sl_data), "close_data": str(close_data)})
return None
```
---
### P0-4 TP1 触发后新 SL 挂单失败,代码仍推进到 `tp1_hit`
- **文件**`backend/position_sync.py:373`、`backend/position_sync.py:385`
- **问题**`rehang_sl()` 返回失败时未阻断,后续仍可能写 DB 为 `tp1_hit`
- **风险**:旧单已取消、新 SL 未挂成功时,剩余仓位无保护且系统状态误报“安全”。
**修复代码**
```python
# backend/position_sync.py (check_tp1_triggers 内)
ok, sl_resp = await rehang_sl(session, symbol, lp["direction"], new_sl, bp["amount"])
if not ok:
logger.error(f"[{symbol}] ❌ TP1后重挂SL失败: {sl_resp}")
_log_event(conn, "critical", "trade",
"TP1后重挂SL失败需人工确认/应急平仓", symbol,
{"trade_id": lp["id"], "sl_resp": str(sl_resp)})
continue
tp2_data, tp2_status = await binance_request(session, "POST", "/fapi/v1/order", {
"symbol": symbol,
"side": close_side,
"type": "TAKE_PROFIT_MARKET",
"stopPrice": price_str,
"quantity": qty_str,
"reduceOnly": "true",
})
if tp2_status != 200:
logger.error(f"[{symbol}] ❌ TP2重挂失败: {tp2_data}")
continue
cur.execute("""
UPDATE live_trades SET tp1_hit=TRUE, sl_price=%s, status='tp1_hit'
WHERE id=%s
""", (new_sl, lp["id"]))
conn.commit()
```
---
### P0-5 超时自动平仓路径仍未做数量精度格式化
- **文件**`backend/risk_guard.py:284`
- **问题**`"quantity": str(amt)` 可能触发币安精度拒单。
- **风险**:超时强平本应兜底,实际可能静默失败。
**修复代码**
```python
# backend/risk_guard.py (check_hold_timeout 内)
qty_prec = SYMBOL_QTY_PRECISION.get(symbol, 3)
qty_str = f"{amt:.{qty_prec}f}"
close_data, close_status = await binance_request(session, "POST", "/fapi/v1/order", {
"symbol": symbol,
"side": close_side,
"type": "MARKET",
"quantity": qty_str,
"reduceOnly": "true",
})
if close_status != 200:
logger.error(f"[{symbol}] ❌ 自动平仓失败: {close_data}")
else:
logger.info(f"[{symbol}] 🔴 自动平仓完成 qty={qty_str}")
```
---
### P0-6 同币种仅按“同策略”去重,双策略并发时可能踩仓
- **文件**`backend/live_executor.py:358`
- **问题**:检查条件是 `symbol + strategy`,而交易所实际是同一净仓(单向模式)。
- **风险**V5.1 与 V5.2 可同时在同币开仓,导致本地两条 trade 共享一个交易所仓位SL/TP/对账失真。
**修复代码**
```python
# backend/live_executor.py
cur.execute("""
SELECT id, strategy, direction
FROM live_trades
WHERE symbol=%s AND status IN ('active', 'tp1_hit')
LIMIT 1
""", (symbol,))
existing = cur.fetchone()
if existing:
logger.warning(f"[{symbol}] ❌ 已有活跃仓位(策略隔离不足),拒绝开新仓")
return None
```
**建议配套 DB 约束**
```sql
CREATE UNIQUE INDEX IF NOT EXISTS uq_live_active_symbol
ON live_trades(symbol)
WHERE status IN ('active', 'tp1_hit');
```
---
## 三、P1 — 逻辑正确性(上线前应修)
### P1-1 手续费窗口条件仍写反(修复未生效)
- **文件**`backend/position_sync.py:439`
- **问题**注释写“开仓后200ms起算”实际代码是 `entry_ts - 200`
- **影响**:会纳入开仓前成交手续费,净值偏低。
**修复代码**
```python
# 原: if t_time >= entry_ts - 200:
if t_time >= entry_ts + 200:
actual_fee_usdt += abs(float(t.get("commission", 0)))
```
---
### P1-2 实盘未实现 USDT 换算仍硬编码 `*2`
- **文件**`backend/main.py:1281`
- **问题**:与 live_config 的 `risk_per_trade_usd` 动态值不一致。
- **影响**:持仓页美元盈亏展示失真。
**修复代码**
```python
risk_usd = await _get_risk_usd()
...
d["unrealized_pnl_usdt"] = round(d["unrealized_pnl_r"] * risk_usd, 2)
```
---
### P1-3 前端持仓盈亏 USDT 仍硬编码 `*2`
- **文件**`frontend/app/live/page.tsx:287`
- **问题**:与后端配置不一致。
- **影响**:交易员在盘中看到的风险金额错误。
**修复代码**
```tsx
const [riskUsd, setRiskUsd] = useState(2);
useEffect(() => {
const f = async () => {
try {
const r = await authFetch("/api/live/config");
if (r.ok) {
const cfg = await r.json();
setRiskUsd(parseFloat(cfg?.risk_per_trade_usd?.value ?? "2"));
}
} catch {}
};
f();
}, []);
const unrealUsdt = unrealR * riskUsd;
```
---
### P1-4 平仓成交兜底逻辑可能误取开仓成交
- **文件**`backend/position_sync.py:433`
- **问题**`close_trades` 为空时fallback 到 `trades_data[-1]`,可能不是平仓单。
- **影响**exit_price 偏差pnl_r 分类错误tp/sl/closed
**修复代码**
```python
if close_trades:
total_qty = sum(float(t["qty"]) for t in close_trades)
if total_qty > 0:
exit_price = sum(float(t["price"]) * float(t["qty"]) for t in close_trades) / total_qty
else:
logger.warning(f"[{symbol}] 未找到明确平仓成交,延后本轮结算")
continue
```
---
## 四、P2 — 健壮性(建议修复)
### P2-1 LISTEN 连接无重连,通知链路可静默退化
- **文件**`backend/live_executor.py:611`、`backend/live_executor.py:636`
- **问题**:仅 `work_conn` 有重连;`listen_conn` 断开后不会重建。
- **影响**:系统退化为轮询,信号延迟上升且不可观测。
**修复代码**
```python
def ensure_listen_conn(conn):
try:
conn.poll()
return conn
except Exception:
logger.warning("LISTEN连接断开重建中...")
try:
conn.close()
except Exception:
pass
c = get_db_connection()
cur = c.cursor()
cur.execute("LISTEN new_signal;")
return c
# 主循环内
listen_conn = ensure_listen_conn(listen_conn)
```
---
### P2-2 余额风控仅“触发阻断”无“恢复解锁”
- **文件**`backend/risk_guard.py:541`
- **问题**:余额低时会置 `block_new_entries=True`,余额恢复后不会自动清除。
- **影响**:系统可长期停摆(假阳性熔断残留)。
**修复代码**
```python
threshold = RISK_PER_TRADE_USD * MIN_BALANCE_MULTIPLE
if balance < threshold:
if risk_state.circuit_break_reason != "LOW_BALANCE":
risk_state.status = "warning"
risk_state.block_new_entries = True
risk_state.circuit_break_reason = "LOW_BALANCE"
else:
if risk_state.circuit_break_reason == "LOW_BALANCE":
risk_state.block_new_entries = False
risk_state.status = "normal"
risk_state.circuit_break_reason = None
```
---
### P2-3 asyncio 事件循环内大量同步 DB 调用
- **文件**`backend/live_executor.py:645`、`backend/position_sync.py:634`
- **问题**`psycopg2` 同步查询在 async 主循环直接执行。
- **影响**:高负载时任务抖动,影响信号时效与风控响应。
**修复建议代码(渐进式)**
```python
signals = await asyncio.to_thread(fetch_pending_signals, work_conn)
```
长期建议:实盘三进程统一迁移 `asyncpg` 或线程池封装 DB 访问。
---
## 五、P3 — 代码质量与一致性
### P3-1 dashboard 页面仍走旧 token 与旧 API
- **文件**`frontend/app/dashboard/page.tsx:21`、`:23`、`:31`
- **问题**:使用 `arb_token` + `/api/user/*`,而当前 auth 体系是 `access_token` + `/api/auth/*`
- **影响**:该页功能不可用,且认证实现分叉。
**修复代码**
```tsx
import { authFetch } from "@/lib/auth";
useEffect(() => {
authFetch("/api/auth/me")
.then(r => (r.ok ? r.json() : Promise.reject()))
.then(d => setUser(d))
.catch(() => router.push("/login"));
}, [router]);
```
如果暂不支持 Discord 绑定接口,建议临时隐藏按钮或补齐后端路由,避免前端死链。
---
## 六、可上实盘结论
**是否可以上实盘:不可以。**
阻断上线原因:存在 P0 级风险(权限伪造、密钥管理、关键平仓路径安全性与状态一致性问题)。
建议顺序:
1. 先修全部 P0 并做单元/集成回归。
2. 再修 P1 的口径一致性,确保运营面板与实际风险一致。
3. 最后补 P2/P3提升长期稳定性与维护性。