arbitrage-engine/docs/LIVE_TRADING_REVIEW.md

13 KiB
Raw Blame History

实盘交易系统代码审阅报告(第二轮)

审阅分支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-1live_executor.py 已实现 SL 失败重试并在 3 次失败后紧急平仓。
  • P0-2position_sync.py 已改为基于 qty 的 TP1 触发检测。
  • P0-3main.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-5close_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 等高危接口。

修复代码

# 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
  • 问题:缺失环境变量时仍回退到固定密码。
  • 风险:环境误配即使用弱口令连接生产库,且密码已暴露于代码历史语义中。

修复代码(三文件同改):

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,也未在失败后再升级处置。
  • 风险:极端市场/交易所拒单场景下,可能平仓失败后继续裸奔;在参数异常时也可能出现行为偏差。

修复代码

# 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:373backend/position_sync.py:385
  • 问题rehang_sl() 返回失败时未阻断,后续仍可能写 DB 为 tp1_hit
  • 风险:旧单已取消、新 SL 未挂成功时,剩余仓位无保护且系统状态误报“安全”。

修复代码

# 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) 可能触发币安精度拒单。
  • 风险:超时强平本应兜底,实际可能静默失败。

修复代码

# 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/对账失真。

修复代码

# 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 约束

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
  • 影响:会纳入开仓前成交手续费,净值偏低。

修复代码

# 原: 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 动态值不一致。
  • 影响:持仓页美元盈亏展示失真。

修复代码

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
  • 问题:与后端配置不一致。
  • 影响:交易员在盘中看到的风险金额错误。

修复代码

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

修复代码

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:611backend/live_executor.py:636
  • 问题:仅 work_conn 有重连;listen_conn 断开后不会重建。
  • 影响:系统退化为轮询,信号延迟上升且不可观测。

修复代码

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,余额恢复后不会自动清除。
  • 影响:系统可长期停摆(假阳性熔断残留)。

修复代码

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:645backend/position_sync.py:634
  • 问题psycopg2 同步查询在 async 主循环直接执行。
  • 影响:高负载时任务抖动,影响信号时效与风控响应。

修复建议代码(渐进式)

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/*
  • 影响:该页功能不可用,且认证实现分叉。

修复代码

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提升长期稳定性与维护性。