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

458 lines
17 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.

# 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` 崩溃/未启动/状态文件损坏时,系统会在**无风控联动**情况下继续开新仓。
- 风险评估:高概率形成失控开仓窗口,属于直接资金风险。
- 修复代码建议:
```python
# 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**,可能延迟熔断,属于资金安全风险。
- 修复代码建议:
```python
# 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` 返回值未校验,日志直接记录“紧急平仓”,没有失败重试/二次验仓。
- 风险评估:在网络抖动、限流、精度错误等场景会留下未平仓暴露仓位,属于直接资金风险。
- 修复代码建议:
```python
# 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:126``premium_pct = ... * 100`,以“百分比值”存储)
- `backend/signal_engine.py:576-579`(按 `0.0005` 阈值判断,语义是“小数比例”)
- 问题描述:存储值是百分比(如 `0.05` 表示 0.05%但评分阈值按比例值0.0005=0.05%)处理,导致大多数微小溢价都被判为强辅助信号。
- 风险评估:信号评分体系失真(你要求重点检查的“评分正确性”),会显著改变开仓频率与方向。
- 修复代码建议:
```python
# 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_conn``ensure_db_conn()``listen_conn` 没有重连逻辑。`listen_conn.poll()` 异常后会进入外层异常分支,下一轮继续使用坏连接。
- 风险评估:信号监听失效,开仓链路中断。
- 修复代码建议:
```python
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统计失真。
- 风险评估:重启后信号质量不稳定,评分偏移。
- 修复代码建议:
```python
# 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` 被低估,影响绩效和风控阈值判断一致性。
- 修复代码建议:
```python
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_SEC``last_market_data/last_account_update`,但未更新也未纳入熔断判断。
- 风险评估:行情/账户数据陈旧时无法触发预期保护。
- 修复代码建议:
```python
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`
- 问题描述:先 `SELECT``UPDATE revoked=1`并发请求可同时通过校验导致同一refresh token被重复换新。
- 风险评估:会话安全边界被削弱。
- 修复代码建议:
```python
@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`,但运行时广泛依赖这些表。
- 风险评估:新环境或灾备恢复时会直接启动失败。
- 修复代码建议:
```sql
-- 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放大与同响应内数值不一致风险。
- 修复代码建议:
```python
_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 也不会变化。
- 风险评估:操作员环境感知错误,容易误判操作风险。
- 修复代码建议:
```tsx
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_config``initial_balance*risk_per_trade` 不一致。
- 风险评估:前端显示误导,策略评估偏差。
- 修复代码建议:
```tsx
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`
- 风险评估:日志噪音、错误监控污染、无意义异常重试。
- 修复代码建议:
```python
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。
- 风险评估:凭据管理不符合生产安全基线。
- 修复代码建议:
```python
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异常且不符合参数化规范。
- 修复代码建议:
```python
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`不...`),影响排障可读性。
- 风险评估:低风险,但会降低紧急问题定位效率。
- 修复代码建议:
```python
# fallback: 未找到明确平仓成交,延后本轮结算
logger.warning(f"[{symbol}] 未找到明确平仓成交,延后结算")
```
### P3-4 `ensure_partitions()` 在高频flush路径重复调用
- 文件:行号:`backend/agg_trades_collector.py:77`
- 问题描述:每次 `flush_buffer()` 都调用分区DDL检查。
- 风险评估不必要的DB负担影响吞吐稳定性。
- 修复代码建议:
```python
# 启动时 + 定时任务中维护分区不在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-open`live_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样本)
通过后再进入实盘灰度。