arbitrage-engine/docs/FULL_AUDIT_V3.md
2026-03-03 01:07:08 +08:00

17 KiB
Raw Permalink Blame History

FULL AUDIT V3review/full-audit-v3

审阅时间2026-03-02
分支:review/full-audit-v3
范围:后端 + 前端(按你的清单全覆盖)

审阅说明

  • 当前分支中未找到你提到的上一轮报告文件 docs/LIVE_TRADING_REVIEW.md,因此本轮以代码现状做完整复核,并尽量对照你描述的“已修复点”验证。
  • 已确认若干上轮高优先级修复仍在(例如:/api/live/* 紧急接口管理员校验、reduceOnly 紧急平仓、TP1后SL重挂失败不推进状态等
  • 下面仅列出本轮仍存在/新发现问题。

P0资金安全 / 核心正确性)

P0-1 风控进程失联时实盘仍会开仓Fail-Open

  • 文件:行号:backend/live_executor.py:321-334
  • 问题描述:execute_entry() 读取 /tmp/risk_guard_state.json 失败时,当前逻辑是 pass 或“告警后继续交易”。这意味着 risk_guard 崩溃/未启动/状态文件损坏时,系统会在无风控联动情况下继续开新仓。
  • 风险评估:高概率形成失控开仓窗口,属于直接资金风险。
  • 修复代码建议:
# backend/live_executor.py
state_path = "/tmp/risk_guard_state.json"
try:
    st = os.stat(state_path)
    # 15秒内未更新视为risk_guard失联
    if time.time() - st.st_mtime > 15:
        logger.error(f"[{symbol}] 风控状态文件过期,拒绝开仓")
        return None

    with open(state_path) as f:
        risk_state = json.load(f)

    if risk_state.get("block_new_entries") or risk_state.get("reduce_only"):
        logger.warning(f"[{symbol}] 风控阻断开仓: {risk_state.get('circuit_break_reason', 'N/A')}")
        return None

except FileNotFoundError:
    logger.error(f"[{symbol}] 风控状态文件不存在,拒绝开仓")
    return None
except Exception as e:
    logger.error(f"[{symbol}] 读取风控状态失败({e}),拒绝开仓")
    return None

P0-2 1R基准在执行层/对账层/风控层不一致,导致风险预算失真

  • 文件:行号:
    • backend/live_executor.py:67-80(从 live_config 动态刷新 RISK_PER_TRADE_USD
    • backend/position_sync.py:54,469-483固定环境变量值用于fee/funding换算
    • backend/risk_guard.py:61,232,551固定环境变量值用于R换算与日限判断
  • 问题描述:实盘配置页可修改 risk_per_trade_usd,但 position_sync/risk_guard 仍按启动时环境变量计算。结果是同一笔交易在不同模块里 R 值不一致。
  • 风险评估当实际1R小于模块内旧值时低估亏损R,可能延迟熔断,属于资金安全风险。
  • 修复代码建议:
# backend/position_sync.py / backend/risk_guard.py 共用

def load_live_risk_usd(conn, default=2.0):
    try:
        cur = conn.cursor()
        cur.execute("SELECT value FROM live_config WHERE key='risk_per_trade_usd'")
        row = cur.fetchone()
        return float(row[0]) if row and row[0] else default
    except Exception:
        return default

# 在每轮主循环开始时刷新
risk_usd = load_live_risk_usd(conn)

# 使用 risk_usd 统一替换静态 RISK_PER_TRADE_USD
fee_r = actual_fee_usdt / risk_usd if risk_usd > 0 else 0
unrealized_r = total_unrealized / risk_usd if risk_usd > 0 else 0
threshold = risk_usd * MIN_BALANCE_MULTIPLE

P0-3 熔断 close_all 不校验平仓结果,可能“已熔断但未平仓”

  • 文件:行号:backend/risk_guard.py:343-357
  • 问题描述:trigger_circuit_break(action="close_all")POST /fapi/v1/order 返回值未校验,日志直接记录“紧急平仓”,没有失败重试/二次验仓。
  • 风险评估:在网络抖动、限流、精度错误等场景会留下未平仓暴露仓位,属于直接资金风险。
  • 修复代码建议:
# backend/risk_guard.py
close_resp, 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_resp}")
    _log_event(conn, "critical", "risk", f"紧急平仓失败 {symbol}", symbol,
               {"response": str(close_resp)})
    continue

# 二次验仓
verify, v_status = await binance_request(session, "GET", "/fapi/v2/positionRisk", {"symbol": symbol})
if v_status == 200 and any(abs(float(p.get("positionAmt", 0))) > 0 and p.get("symbol") == symbol for p in verify):
    logger.error(f"[{symbol}] 紧急平仓后仍有仓位,进入人工告警")

P0-4 V5辅助层 Coinbase Premium 单位不一致,评分被系统性放大

  • 文件:行号:
    • backend/market_data_collector.py:126premium_pct = ... * 100,以“百分比值”存储)
    • backend/signal_engine.py:576-579(按 0.0005 阈值判断,语义是“小数比例”)
  • 问题描述:存储值是百分比(如 0.05 表示 0.05%但评分阈值按比例值0.0005=0.05%)处理,导致大多数微小溢价都被判为强辅助信号。
  • 风险评估:信号评分体系失真(你要求重点检查的“评分正确性”),会显著改变开仓频率与方向。
  • 修复代码建议:
# backend/market_data_collector.py
premium_ratio = (coinbase_price - binance_price) / binance_price
payload = {
    "premium_ratio": premium_ratio,          # 0.0005 = 0.05%
    "premium_pct": premium_ratio * 100.0,   # 展示用途
    ...
}

# backend/signal_engine.py
premium_ratio = to_float((self.market_indicators.get("coinbase_premium") or {}).get("premium_ratio")) \
    if isinstance(self.market_indicators.get("coinbase_premium"), dict) else None

if premium_ratio is None:
    aux_score = 2
elif (direction == "LONG" and premium_ratio > 0.0005) or (direction == "SHORT" and premium_ratio < -0.0005):
    aux_score = 5
elif abs(premium_ratio) <= 0.0005:
    aux_score = 2
else:
    aux_score = 0

P1逻辑正确性 / 并发与健壮性)

P1-1 LISTEN连接断开后无自愈信号链路可永久中断

  • 文件:行号:backend/live_executor.py:636-664, 644-655
  • 问题描述:仅 work_connensure_db_conn()listen_conn 没有重连逻辑。listen_conn.poll() 异常后会进入外层异常分支,下一轮继续使用坏连接。
  • 风险评估:信号监听失效,开仓链路中断。
  • 修复代码建议:
def ensure_listen_conn(conn):
    try:
        conn.poll()
        return conn
    except Exception:
        try:
            conn.close()
        except Exception:
            pass
        new_conn = get_db_connection()
        cur = new_conn.cursor()
        cur.execute("LISTEN new_signal;")
        logger.warning("LISTEN连接已重建")
        return new_conn

# 主循环中
listen_conn = ensure_listen_conn(listen_conn)

P1-2 冷启动只加载4小时历史但策略依赖24小时窗口统计

  • 文件:行号:backend/signal_engine.py:110-113, 971
  • 问题描述:win_day 用于 p95/p99 及大单拥挤判断,但 load_historical(state, WINDOW_MID) 只回灌4h重启后前20h统计失真。
  • 风险评估:重启后信号质量不稳定,评分偏移。
  • 修复代码建议:
# backend/signal_engine.py
for sym, state in states.items():
    # 至少加载DAY窗口保证p95/p99与day-cvd有效
    load_historical(state, WINDOW_DAY)

P1-3 资金费率收益未计入净PnL净值统计偏差

  • 文件:行号:backend/position_sync.py:480-483
  • 问题描述:当前仅在 funding_usdt < 0 时扣减 funding_r正向funding收益被忽略。
  • 风险评估:net_pnl_r 被低估,影响绩效和风控阈值判断一致性。
  • 修复代码建议:
risk_usd = load_live_risk_usd(conn)
funding_r = (funding_usdt / risk_usd) if risk_usd > 0 else 0
# funding正值应增加净收益负值应减少净收益
pnl_r = gross_pnl_r - fee_r + funding_r

P1-4 风控规则“数据新鲜度”未真正落地

  • 文件:行号:backend/risk_guard.py:70-71, 98-99, 248-257
  • 问题描述:定义了 MARKET_DATA_STALE_SEC / ACCOUNT_UPDATE_STALE_SEClast_market_data/last_account_update,但未更新也未纳入熔断判断。
  • 风险评估:行情/账户数据陈旧时无法触发预期保护。
  • 修复代码建议:
def check_data_freshness(conn):
    now = time.time()
    issues = []

    # API健康
    api_gap = now - risk_state.last_api_success
    if api_gap > API_DISCONNECT_THRESHOLD_SEC:
        issues.append(f"API无响应{api_gap:.0f}秒")

    # 行情新鲜度基于signal_indicators最新ts
    cur = conn.cursor()
    cur.execute("SELECT MAX(ts) FROM signal_indicators")
    row = cur.fetchone()
    if row and row[0]:
        market_age = now - (row[0] / 1000)
        if market_age > MARKET_DATA_STALE_SEC:
            issues.append(f"行情数据延迟{market_age:.1f}秒")

    return issues

P1-5 Refresh Token轮转非原子存在并发重放窗口

  • 文件:行号:backend/auth.py:313-325
  • 问题描述:先 SELECTUPDATE revoked=1并发请求可同时通过校验导致同一refresh token被重复换新。
  • 风险评估:会话安全边界被削弱。
  • 修复代码建议:
@router.post("/auth/refresh")
def refresh_token(body: RefreshReq):
    now_iso = datetime.utcnow().isoformat()
    row = _fetchone(
        "UPDATE refresh_tokens "
        "SET revoked = 1 "
        "WHERE token = %s AND revoked = 0 AND expires_at > %s "
        "RETURNING user_id",
        (body.refresh_token, now_iso),
    )
    if not row:
        raise HTTPException(status_code=401, detail="invalid refresh token")

    user = _fetchone("SELECT * FROM users WHERE id = %s", (row["user_id"],))
    ...

P1-6 init_schema() 未包含 live 表结构部署依赖隐式外部DDL

  • 文件:行号:backend/db.py:166-306, 345-363
  • 问题描述:SCHEMA_SQL 里没有 live_trades/live_config/live_events,但运行时广泛依赖这些表。
  • 风险评估:新环境或灾备恢复时会直接启动失败。
  • 修复代码建议:
-- backend/db.py SCHEMA_SQL 追加
CREATE TABLE IF NOT EXISTS live_config (
    key TEXT PRIMARY KEY,
    value TEXT NOT NULL,
    label TEXT,
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE TABLE IF NOT EXISTS live_events (
    id BIGSERIAL PRIMARY KEY,
    ts BIGINT DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT,
    level TEXT,
    category TEXT,
    symbol TEXT,
    message TEXT,
    detail JSONB
);

CREATE TABLE IF NOT EXISTS live_trades (
    id BIGSERIAL PRIMARY KEY,
    symbol TEXT NOT NULL,
    strategy TEXT NOT NULL,
    direction TEXT NOT NULL,
    status TEXT NOT NULL DEFAULT 'active',
    entry_price DOUBLE PRECISION,
    exit_price DOUBLE PRECISION,
    entry_ts BIGINT,
    exit_ts BIGINT,
    pnl_r DOUBLE PRECISION,
    risk_distance DOUBLE PRECISION,
    tp1_hit BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP DEFAULT NOW()
);

P2状态一致性 / 前后端一致性 / 性能)

P2-1 _get_risk_usd() 无缓存且被循环多次调用

  • 文件:行号:backend/main.py:1283, 1356, 1476-1482
  • 问题描述函数注释写“缓存60秒”实际每次调用都查DB且在列表循环内重复调用。
  • 风险评估不必要的DB放大与同响应内数值不一致风险。
  • 修复代码建议:
_risk_cache = {"v": 2.0, "ts": 0.0}

async def _get_risk_usd() -> float:
    now = time.time()
    if now - _risk_cache["ts"] < 60:
        return _risk_cache["v"]
    try:
        row = await async_fetchrow("SELECT value FROM live_config WHERE key = $1", "risk_per_trade_usd")
        v = float(row["value"]) if row else 2.0
    except Exception:
        v = 2.0
    _risk_cache.update({"v": v, "ts": now})
    return v

# 调用侧每个接口先取一次
risk_usd = await _get_risk_usd()

P2-2 实盘页顶部文案硬编码“测试网”,与后端环境可能不一致

  • 文件:行号:frontend/app/live/page.tsx:740
  • 问题描述:页面固定展示“测试网”,即使后端切到 production 也不会变化。
  • 风险评估:操作员环境感知错误,容易误判操作风险。
  • 修复代码建议:
const [tradeEnv, setTradeEnv] = useState("unknown");
useEffect(() => {
  (async () => {
    const r = await authFetch("/api/live/config");
    if (r.ok) {
      const cfg = await r.json();
      setTradeEnv(cfg?.trade_env?.value || "unknown");
    }
  })();
}, []);

<p className="text-[10px] text-slate-500">
  V5.2策略 · 币安USDT永续合约 · {tradeEnv === "production" ? "生产网" : "测试网"}
</p>

P2-3 模拟盘前端仍有 1R=$200 硬编码,和配置页不一致

  • 文件:行号:
    • frontend/app/paper/page.tsx:234
    • frontend/app/paper-v52/page.tsx:297
  • 问题描述浮盈USDT使用 unrealR * 200 固定换算,与 paper_configinitial_balance*risk_per_trade 不一致。
  • 风险评估:前端显示误导,策略评估偏差。
  • 修复代码建议:
const [paper1R, setPaper1R] = useState(200);
useEffect(() => {
  (async () => {
    const r = await authFetch("/api/paper/config");
    if (r.ok) {
      const cfg = await r.json();
      setPaper1R((cfg.initial_balance || 10000) * (cfg.risk_per_trade || 0.02));
    }
  })();
}, []);

const unrealUsdt = unrealR * paper1R;

P2-4 XRP/SOL 在 Coinbase Premium 采集中每轮抛异常

  • 文件:行号:backend/market_data_collector.py:112-117, 160-165
  • 问题描述:collect_symbol() 对4币种都调用 collect_coinbase_premium(),但 pair_map 仅 BTC/ETHXRP/SOL 会 KeyError
  • 风险评估:日志噪音、错误监控污染、无意义异常重试。
  • 修复代码建议:
pair_map = {
    "BTCUSDT": "BTC-USD",
    "ETHUSDT": "ETH-USD",
    "XRPUSDT": "XRP-USD",
    "SOLUSDT": "SOL-USD",
}
coinbase_pair = pair_map.get(symbol)
if not coinbase_pair:
    logger.info("[%s] coinbase_premium skipped (no mapping)", symbol)
    return

P3代码质量 / 安全规范)

P3-1 多处保留默认数据库密码

  • 文件:行号:
    • backend/db.py:19, 28
    • backend/market_data_collector.py:19
  • 问题描述:默认密码 arb_engine_2026 仍作为fallback。
  • 风险评估:凭据管理不符合生产安全基线。
  • 修复代码建议:
PG_PASS = os.getenv("PG_PASS")
if not PG_PASS:
    raise RuntimeError("PG_PASS is required")

CLOUD_PG_PASS = os.getenv("CLOUD_PG_PASS")
if CLOUD_PG_ENABLED and not CLOUD_PG_PASS:
    raise RuntimeError("CLOUD_PG_PASS is required when CLOUD_PG_ENABLED=true")

P3-2 fetch_pending_signals() 使用字符串拼接SQL

  • 文件:行号:backend/live_executor.py:528-542
  • 问题描述:strategies_str 通过f-string注入SQL可维护性与安全性都较差。
  • 风险评估策略名包含特殊字符会导致SQL异常且不符合参数化规范。
  • 修复代码建议:
cur.execute(
    """
    SELECT si.id, si.symbol, si.signal, si.score, si.ts, si.factors, si.strategy, si.price
    FROM signal_indicators si
    WHERE si.signal IS NOT NULL
      AND si.signal != ''
      AND si.strategy = ANY(%s)
      AND si.ts > extract(epoch from now()) * 1000 - 60000
      AND NOT EXISTS (
          SELECT 1 FROM live_trades lt
          WHERE lt.signal_id = si.id AND lt.strategy = si.strategy
      )
    ORDER BY si.ts DESC
    """,
    (ENABLED_STRATEGIES,)
)

P3-3 对账日志存在乱码(编码污染)

  • 文件:行号:backend/position_sync.py:447-448
  • 问题描述:注释和日志出现 mojibake不...),影响排障可读性。
  • 风险评估:低风险,但会降低紧急问题定位效率。
  • 修复代码建议:
# fallback: 未找到明确平仓成交,延后本轮结算
logger.warning(f"[{symbol}] 未找到明确平仓成交,延后结算")

P3-4 ensure_partitions() 在高频flush路径重复调用

  • 文件:行号:backend/agg_trades_collector.py:77
  • 问题描述:每次 flush_buffer() 都调用分区DDL检查。
  • 风险评估不必要的DB负担影响吞吐稳定性。
  • 修复代码建议:
# 启动时 + 定时任务中维护分区不在flush热路径调用
# flush_buffer() 内删除 ensure_partitions()

async def ensure_partitions_loop():
    while True:
        try:
            ensure_partitions()
        except Exception as e:
            logger.warning(f"ensure_partitions_loop error: {e}")
        await asyncio.sleep(3600)

是否可以上实盘

结论:当前代码不建议上实盘。

必须先修复的阻断项Blocking

  1. P0-1 风控失联 fail-openlive_executor 仍可开仓)。
  2. P0-2 1R基准跨模块不一致会导致风控阈值偏差
  3. P0-3 close_all 未校验平仓结果(可能“已熔断未平仓”)。
  4. P0-4 评分公式单位错误Coinbase Premium层被系统性放大
  5. P1-1 LISTEN链路无重连可导致信号执行中断
  6. P1-6 缺少 live 表初始化DDL新环境不可恢复启动

当以上阻断项修复并回归后,再进行一次针对:

  • 熔断全链路演练(含限流/超时注入)
  • 重启恢复演练signal_engine + live_executor + position_sync
  • 评分一致性回放V5.1/V5.2样本)

通过后再进入实盘灰度。