arbitrage-engine/docs/REVIEW.md
fanziqi ad60a53262 review: add code audit annotations and REVIEW.md for v5.1
P0 issues annotated (critical, must fix before live trading):
- signal_engine.py: cooldown blocks reverse-signal position close
- paper_monitor.py + signal_engine.py: pnl_r 2x inflated for TP scenarios
- signal_engine.py: entry price uses 30min VWAP instead of real-time price
- paper_monitor.py + signal_engine.py: concurrent write race on paper_trades

P1 issues annotated (long-term stability):
- db.py: ensure_partitions uses timedelta(30d) causing missed monthly partitions
- signal_engine.py: float precision drift in buy_vol/sell_vol accumulation
- market_data_collector.py: single bare connection with no reconnect logic
- db.py: get_sync_pool initialization not thread-safe
- signal_engine.py: recent_large_trades deque has no maxlen

P2/P3 issues annotated across backend and frontend:
- coinbase_premium KeyError for XRP/SOL symbols
- liquidation_collector: redundant elif condition in aggregation logic
- auth.py: JWT secret hardcoded default, login rate-limit absent
- Frontend: concurrent refresh token race, AuthContext not synced on failure
- Frontend: universal catch{} swallows all API errors silently
- Frontend: serial API requests in LatestSignals, market-indicators over-polling

docs/REVIEW.md: comprehensive audit report with all 34 issues (P0×4, P1×5,
P2×6, P3×4 backend + FE-P1×4, FE-P2×8, FE-P3×3 frontend), fix suggestions
and prioritized remediation roadmap.
2026-03-01 17:14:52 +08:00

33 KiB
Raw Blame History

Arbitrage Engine V5.1 — 代码审阅报告

审阅时间2026-03-01 审阅范围backend/ 全部核心文件(跳过迁移脚本和已废弃文件) 审阅视角:资深量化交易系统架构师,以实盘资金安全为最高优先级


总体评价

系统架构设计合理关注点分离清晰PM2 多进程模型满足毫秒级 TP/SL 监控需求。但在接入真实资金前,有 4 个 P0 级问题必须修复——其中 pnl_r 计算错误和冷却期反向平仓失效会直接影响资金安全和策略可信度。P1 级的分区月份 Bug 在月底可能引发数据写入完全失败。


P0 — 会直接导致资金损失的问题

P0-1 冷却期阻断反向信号,持仓无法被对冲信号平仓

属性 内容
文件 signal_engine.py
行号 309COOLDOWN_MS 定义、305in_cooldown 判断、414-425signal 置 None、702-720主循环平仓触发
严重性 🔴 P0

问题描述

evaluate_signalin_cooldown=True 时统一将 result["signal"] 置为 None。主循环的反向平仓逻辑(paper_close_by_signal)依赖 if result.get("signal"): 为真才执行。

危险场景

  1. BTC LONG 信号触发,开仓。last_signal_ts 更新,冷却 10 分钟。
  2. 5 分钟后市场急速反转SHORT 评分达 90 分(强力信号)。
  3. in_cooldown=Trueresult["signal"] = None → 主循环不执行反向平仓。
  4. LONG 仓位继续亏损,直到 SL1.4×ATRpaper_monitor 触发才关闭。
  5. 等价于:忽略了一个 90 分的强烈反向信号,仓位被迫吃满亏损。

实盘影响:对于带杠杆的合约交易,这意味着无法在强反向行情中及时止损换仓。

建议修复

# 方案A推荐反向信号不受冷却限制
# 在 evaluate_signal 中,即使 in_cooldown=True也输出 direction 供主循环判断
# 主循环中:
existing_dir = paper_get_active_direction(sym)
eval_direction = result.get("direction")  # 始终有值
if existing_dir and eval_direction and existing_dir != eval_direction:
    if result["score"] >= 60:  # 反向信号评分够高才平仓
        paper_close_by_signal(sym, result["price"], now_ms)

# 方案B在 evaluate_signal 中为反向信号单独处理冷却
# 仅对同方向信号应用冷却,反向信号始终允许

P0-2 pnl_r 计算错误TP 场景收益虚高 2 倍

属性 内容
文件 paper_monitor.pysignal_engine.py
行号 paper_monitor.py:69,81,87,99signal_engine.py:558-570
严重性 🔴 P0

问题描述

TP/SL_BE 的 pnl_r 使用了硬编码的错误倍数。

核心数学

risk_atr = 0.7 × ATR
risk_distance (1R 基准) = 2.0 × risk_atr = 1.4 × ATR

TP1 实际位置 = entry ± 1.5 × risk_atr = entry ± 1.05 ATR
TP1 对应 R   = 1.5 × risk_atr / (2.0 × risk_atr) = 0.75R  ← 不是 1.5R
TP2 实际位置 = entry ± 3.0 × risk_atr = entry ± 2.1 ATR
TP2 对应 R   = 3.0 × risk_atr / (2.0 × risk_atr) = 1.5R   ← 不是 3.0R

当前错误paper_monitor.py

# TP1 sl_bepnl_r = 0.5 * 1.5 = 0.75R  (应为 0.375R,虚高 2×)
# 全 TP   pnl_r = 0.5*1.5 + 0.5*3.0 = 2.25R (应为 1.125R,虚高 2×)

对比正确的超时计算(同文件 line 110

pnl_r = move / risk_distance   # ← 这个是对的!

影响

  • balanceequity curve虚增完整 TP 一笔记 2.25R×200=$450实际只有 1.125R×$225
  • 胜率不变,但 avg_win 虚高PF 虚高Sharpe 虚高
  • 若基于此回测参数标定实盘仓位,会严重高估策略收益

建议修复

# paper_monitor.py check_and_close 中,统一使用实际价差计算
risk_distance = 2.0 * 0.7 * atr_entry  if atr_entry > 0 else 1

if new_status == "tp":
    # 半仓 TP1 + 半仓 TP2 的加权 R
    tp1_r = (tp1 - entry_price) / risk_distance if direction == "LONG" else (entry_price - tp1) / risk_distance
    tp2_r = (tp2 - entry_price) / risk_distance if direction == "LONG" else (entry_price - tp2) / risk_distance
    pnl_r = 0.5 * tp1_r + 0.5 * tp2_r

elif new_status == "sl_be":
    tp1_r = (tp1 - entry_price) / risk_distance if direction == "LONG" else (entry_price - tp1) / risk_distance
    pnl_r = 0.5 * tp1_r  # 已平半仓的 TP1 盈利

P0-3 开仓价格使用 30 分钟 VWAP 而非实时价格

属性 内容
文件 signal_engine.py
行号 285price = vwap、499paper_open_trade 入参)
严重性 🔴 P0实盘/ P1模拟盘

问题描述

evaluate_signalprice = vwap if vwap > 0 else 0,这里 vwap 是 30 分钟成交量加权均价。在评分触发信号的时刻,市场价格可能已经大幅偏离 30 分钟 VWAP

  • 强烈单边行情中(这正是 CVD 信号触发的场景),价格可能在 30 分钟内上涨 1-3%
  • 用 VWAP 作为 entry_priceTP1/TP2/SL 的价位全部基于一个过去的"平均价"
  • 实际开仓后TP1 可能已经被穿越low entry或 SL 过近high entry

建议修复

# 从 win_fast.trades 取最新的成交价
if self.win_fast.trades:
    price = self.win_fast.trades[-1][2]  # (time_ms, qty, price, ibm)
else:
    price = vwap

P0-4 双进程并发写同一持仓行,无互斥机制

属性 内容
文件 signal_engine.py + paper_monitor.py
行号 signal_engine.py:624-650paper_close_by_signalpaper_monitor.py:44-130check_and_close
严重性 🔴 P0

问题描述

两个独立 PM2 进程同时操作 paper_trades 表,均直接执行 UPDATE paper_trades SET status=... WHERE id=?,没有任何行级锁或应用层互斥。

竞态场景

T1: paper_monitor SELECT → 返回 active 持仓,检查价格 → 即将触发 SL
T2: signal_engine 出现反向信号 → paper_close_by_signal → UPDATE status='signal_flip'
T3: paper_monitor UPDATE status='sl' → 覆盖 T2 的 signal_flip
结果:一笔交易产生错误的 status 和 pnl_r后者覆盖前者

建议修复

-- 方案APostgreSQL 行级锁(推荐)
SELECT ... FROM paper_trades
WHERE symbol=%s AND status IN ('active','tp1_hit')
FOR UPDATE SKIP LOCKED;   -- 已被锁定的行跳过,避免死锁
# 方案B将所有平仓逻辑集中在 paper_monitor 一个进程
# signal_engine 仅标记"需要平仓"(写一个 pending_close 字段),
# paper_monitor 读取后统一处理

P1 — 长期运行稳定性问题

P1-1 ensure_partitions 月份计算 Bug 导致分区缺失

属性 内容
文件 db.py
行号 320-332
严重性 🟠 P1月底跨月时触发

问题描述

for delta in range(0, 3):
    d = now + datetime.timedelta(days=delta * 30)  # ← 错误

具体反例1月1日运行

  • delta=0: Jan 01 → "202601"
  • delta=1: Jan 31 → "202601" ← 同月!
  • delta=2: Mar 02 → "202603" ← 跳过2月

set(months) = {"202601", "202603"}agg_trades_202602 分区不会被创建

后果1月31日到2月的 flush_buffer 将抛出:

ERROR: no partition of relation "agg_trades" found for row

所有 BTC/ETH/XRP/SOL 数据写入失败,交易信号引擎读不到新数据,评分僵死。

建议修复

import calendar
for delta in range(3):
    month = now.month + delta
    year = now.year + (month - 1) // 12
    month = ((month - 1) % 12) + 1
    months.append(f"{year}{month:02d}")

P1-2 buy_vol/sell_vol 浮点精度漂移

属性 内容
文件 signal_engine.py
行号 143-162TradeWindow.add/trim
严重性 🟠 P1

问题描述

buy_volsell_vol 是浮点数通过数千万次加减操作累积误差。IEEE 754 双精度浮点数在大量加减后可能累积可测量的误差。BTC 每笔交易 qty 精度为 0.001,经过 7000 万次操作后误差可能达到数十至数百 BTC。

CVD = buy_vol - sell_vol 将产生系统性偏差,影响评分准确性。

建议修复

# 每隔 N 次 trim 操作后,从 deque 重算
def rebuild_sums(self):
    self.buy_vol = sum(t[1] for t in self.trades if t[3] == 0)
    self.sell_vol = sum(t[1] for t in self.trades if t[3] == 1)
    self.pq_sum = sum(t[2] * t[1] for t in self.trades)
    self.q_sum = sum(t[1] for t in self.trades)

P1-3 market_data_collector 单连接无重连

属性 内容
文件 market_data_collector.py
行号 51-57
严重性 🟠 P1

使用单个裸 psycopg2 连接无连接池无断线重连逻辑。GCP Cloud SQL 网络抖动、实例重启时连接中断进程崩溃PM2 重启需要数秒到数分钟期间市场指标停止更新signal_engine 评分使用过期数据。

建议修复:改用 db.get_sync_conn() 连接池,每次 save_indicator 自动获取和归还连接。


P1-4 get_sync_pool 初始化线程不安全

属性 内容
文件 db.py
行号 36-43
严重性 🟠 P1

_sync_pool is None 判断和赋值之间没有锁,多线程同时调用可能创建多个连接池实例,造成连接泄漏。当前各进程是单线程,问题不会频繁触发,但 ThreadedConnectionPool 表明设计上考虑了多线程使用。

建议修复

import threading
_pool_lock = threading.Lock()

def get_sync_pool():
    global _sync_pool
    if _sync_pool is None:
        with _pool_lock:
            if _sync_pool is None:
                _sync_pool = psycopg2.pool.ThreadedConnectionPool(...)
    return _sync_pool

P1-5 recent_large_trades deque 无 maxlen 限制

属性 内容
文件 signal_engine.py
行号 232
严重性 🟠 P1

self.recent_large_trades: deque = deque() 无上限。极端行情(挤压、崩盘)时 P99 大单频繁出现15 分钟窗口内可能积累数万条记录,导致内存持续增长。

建议修复deque(maxlen=2000),超出自动丢弃最老的。


P2 — 数据完整性问题

P2-1 coinbase_premium 仅支持 BTC/ETHXRP/SOL 永久数据缺失

属性 内容
文件 market_data_collector.py
行号 112-118

pair_map 只含 BTC/ETHXRPUSDT/SOLUSDT 调用 pair_map[symbol] 会抛出 KeyError。被 asyncio.gather(return_exceptions=True) 捕获,不会崩溃,但这两个币种的 coinbase_premium 永远为 None。signal_engine 对它们的辅助层给默认 2 分(中性),不影响系统运行,但长期是数据空洞。

建议修复

if symbol not in pair_map:
    return  # XRP/SOL 无 Coinbase 对应交易对,跳过

P2-2 ensure_partitions 使用本地时区

属性 内容
文件 db.py
行号 329-330

datetime.datetime(year, month, 1).timestamp() 使用服务器本地时区。GCP asia-northeast1东京JST UTC+9若未设置 TZ=UTC分区边界将偏移 9 小时,导致接近月末的数据可能写入错误分区。

建议修复

from datetime import timezone
start = datetime.datetime(year, month, 1, tzinfo=timezone.utc)

P2-3 ensure_partitions 在 flush_buffer 热路径中被频繁调用

属性 内容
文件 agg_trades_collector.py
行号 77

每 1 秒(或每 200 条交易)调用一次 ensure_partitions(),其内部执行 CREATE TABLE IF NOT EXISTS3 次),属冗余 DDL 操作,增加每批写入的延迟。

建议修复:将分区创建移到独立的定时任务(每小时执行一次)。


P2-4 liquidation_collector 聚合逻辑 elif 冗余条件

属性 内容
文件 liquidation_collector.py
行号 127
if now - buf["window_start"] >= AGG_INTERVAL:    # 外层 if
    if buf["count"] > 0:
        save_aggregated(...)
    elif now - buf["window_start"] >= AGG_INTERVAL:  # 永远成立!
        save_aggregated(sym, now * 1000, 0, 0, 0)

elif 条件与外层 if 完全相同,在 else 分支中永远为 True实际等价于 else。逻辑正确但代码有误导性。


P2-5 atr_percentile property 含写副作用

属性 内容
文件 signal_engine.py
行号 209-219

@property atr_percentile 内部调用 self.atr_history.append(current)。若此属性被意外多次访问,会重复追加同一 ATR 值,使分布历史偏向当前 ATR影响百分位计算准确性。

建议修复:将 append 移到显式调用的 update_atr_history() 方法中。


P2-6 paper_summary 硬编码 1R = $200

属性 内容
文件 main.py
行号 554
total_pnl_usdt = total_pnl * 200  # 1R = $200硬编码

若通过 API 修改了 risk_per_tradeinitial_balance,此处计算不会随之更新,展示给用户的 USDT 余额将是错误的。


P3 — 安全与 API 问题

P3-1 JWT 密钥硬编码默认值

属性 内容
文件 auth.py
行号 15
JWT_SECRET = os.getenv("JWT_SECRET", "arb-engine-jwt-secret-v2-2026")

默认密钥以明文存在源代码中。若服务器未设置环境变量(或被 git clone 后直接运行),任何人都可以用此密钥伪造合法 JWT以任意身份访问所有需要认证的 API。

建议修复:移除默认值,启动时强制校验:

JWT_SECRET = os.getenv("JWT_SECRET")
if not JWT_SECRET:
    raise RuntimeError("JWT_SECRET environment variable is required")

P3-2 CORS allow_origins=["*"]

属性 内容
文件 main.py
行号 17-21

允许任意域名发起跨域请求,浏览器的同源策略保护失效。若用户在访问恶意网站时处于登录状态,恶意网站可通过 JavaScript 向 API 发起请求。

建议修复allow_origins=["https://arb.zhouyangclaw.com"]


P3-3 refresh token 刷新非原子,存在并发竞态

属性 内容
文件 auth.py
行号 316-327

SELECT检查 revoked=0和 UPDATErevoked=1之间存在时间窗口两个并发请求可能都通过校验生成两个不同的新 access token。

建议修复:原子化操作:

UPDATE refresh_tokens SET revoked = 1
WHERE token = %s AND revoked = 0
RETURNING user_id, expires_at

P3-4 登录接口无频率限制

属性 内容
文件 auth.py
行号 292-308

登录端点无请求频率限制,可被自动化工具暴力破解密码。

建议修复:接入 slowapi 或 Redis 计数器,限制同一 IP 每分钟最多 10 次登录尝试。


汇总表

ID 优先级 文件 行号 标题 影响
P0-1 🔴 P0 signal_engine.py 305,414-425,702-720 冷却期阻断反向信号平仓 持仓无法被强烈反向信号关闭
P0-2 🔴 P0 paper_monitor.py 69,81,87,99 pnl_r TP场景虚高2倍 统计数据失真,实盘参数标定偏差
P0-3 🔴 P0 signal_engine.py 285,499 开仓价用VWAP而非实时价 TP/SL价位基于历史均价偏离实际
P0-4 🔴 P0 signal_engine.py + paper_monitor.py 624,44 双进程并发写竞态 同一持仓被两个进程覆盖写
P1-1 🟠 P1 db.py 320-332 月份计算用timedelta(30天) 月初跨月时漏建分区,数据写入失败
P1-2 🟠 P1 signal_engine.py 143-162 浮点精度漂移 长期运行后CVD偏差影响评分
P1-3 🟠 P1 market_data_collector.py 51-57 单连接无重连 DB断线后进程崩溃指标停止更新
P1-4 🟠 P1 db.py 36-43 连接池初始化线程不安全 多线程下可能连接泄漏
P1-5 🟠 P1 signal_engine.py 232 recent_large_trades无maxlen 极端行情内存无限增长
P2-1 🟡 P2 market_data_collector.py 112-118 XRP/SOL coinbase_premium KeyError 两个币种辅助层永久为None
P2-2 🟡 P2 db.py 329-330 分区边界用本地时区 非UTC服务器分区边界偏移
P2-3 🟡 P2 agg_trades_collector.py 77 flush_buffer每次调用ensure_partitions 热路径冗余DDL操作
P2-4 🟡 P2 liquidation_collector.py 127 elif条件冗余 逻辑误导,代码质量问题
P2-5 🟡 P2 signal_engine.py 209-219 atr_percentile有写副作用 多次调用时分布历史失真
P2-6 🟡 P2 main.py 554 1R=$200硬编码 参数变更时余额显示错误
P3-1 🔵 P3 auth.py 15 JWT密钥硬编码默认值 可伪造任意用户JWT
P3-2 🔵 P3 main.py 17-21 CORS allow_origins=["*"] 浏览器同源策略失效
P3-3 🔵 P3 auth.py 316-327 refresh token刷新非原子 并发刷新可能双重成功
P3-4 🔵 P3 auth.py 292-308 登录无频率限制 可暴力破解密码

优先修复建议

接入实盘前必须完成P0

  1. P0-1:重写冷却逻辑,确保反向信号不被冷却屏蔽,持仓可被对冲信号平仓
  2. P0-2:统一 pnl_r 计算,所有平仓场景改用 (exit_price - entry_price) / risk_distance
  3. P0-3:开仓价改用 win_fast.trades[-1][2](最新成交价)而非 VWAP
  4. P0-4:为 paper_trades 更新操作加 SELECT FOR UPDATE SKIP LOCKED,或集中平仓逻辑到单一进程

持续运行必须完成P1

  1. P1-1ensure_partitions 用正确的月份加法,确保月末不遗漏下月分区
  2. P1-3market_data_collector 改用连接池,加断线重连
  3. P1-2:定期从 deque 重算 buy_vol/sell_vol 防止浮点漂移

V5.2 迭代完成P2/P3

  1. P2-1collect_coinbase_premium 跳过 XRP/SOL
  2. P2-2:分区边界改用 UTC
  3. P3-1JWT_SECRET 强制环境变量,移除默认值
  4. P3-2CORS 限制到前端域名
  5. P3-4:登录接口加频率限制

本报告仅标注问题和建议,未修改任何业务逻辑。所有 inline 注释格式为 # [REVIEW] P级 | 描述 | 建议,已写入各后端文件对应行。


前端审阅补充frontend/

审阅范围lib/auth.tsx、lib/api.ts、app/paper/page.tsx、app/signals/page.tsx、app/page.tsx、app/trades/page.tsx、app/server/page.tsx


FE-P1 — 认证与稳定性(会导致用户无声失联/看到过期数据)

FE-P1-1 authFetch 并发刷新竞态:多个 401 同时触发多次 refresh

属性 内容
文件 frontend/lib/auth.tsx
行号 113-134
严重性 🟠 P1

问题描述

paper 页面同时运行 6 个独立的 setInterval 轮询组件。当 access token 恰好过期时,多个组件在同一时间触发 authFetch,都收到 401全部进入刷新逻辑并并发调用 /api/auth/refresh

后端 refresh token 是单次使用(用完即 revoke。第一个到达的刷新请求成功后续请求都得到 "invalid refresh token" → 进入 else 分支。

后果

  • else 分支清除 localStorage 但不更新 React AuthContext见 FE-P1-2
  • 这些组件的请求静默失败,显示过期数据
  • 用户不知道自己已经"半登出"

建议修复

// lib/auth.tsx 模块顶层
let _refreshPromise: Promise<string | null> | null = null;

async function refreshAccessToken(): Promise<string | null> {
  if (_refreshPromise) return _refreshPromise;
  _refreshPromise = (async () => {
    const rt = localStorage.getItem("refresh_token");
    if (!rt) return null;
    const res = await fetch(`${API_BASE}/api/auth/refresh`, { ... });
    if (res.ok) {
      const data = await res.json();
      localStorage.setItem("access_token", data.access_token);
      localStorage.setItem("refresh_token", data.refresh_token);
      return data.access_token;
    }
    // 清除 localStorage + 通知 AuthContext
    return null;
  })().finally(() => { _refreshPromise = null; });
  return _refreshPromise;
}

FE-P1-2 刷新失败后 React AuthContext 状态未同步

属性 内容
文件 frontend/lib/auth.tsx
行号 127-133
严重性 🟠 P1

问题描述

当 refresh 失败时,代码清除了 localStorage 但无法直接调用 AuthContext.logout()authFetch 是模块级函数,不在 Context 内):

// refresh failed, clear auth
localStorage.removeItem("access_token");   // ✓ localStorage 清了
localStorage.removeItem("refresh_token");  // ✓
localStorage.removeItem("user");           // ✓
// ← 但 AuthContext.user 和 accessToken state 仍然是旧值!

后果链

  1. useAuth().isLoggedIn === trueReact state 未变)
  2. 页面继续显示已登录 UI轮询仍然继续
  3. 所有后续请求因无 token或过期 token而失败catch {} 静默吞掉
  4. 用户看到的是"运行中"状态但所有数据已冻结——对交易监控极度危险

建议修复

// 方案A全局事件总线
window.dispatchEvent(new CustomEvent("auth:session-expired"));
// AuthProvider 中监听并调用 logout()

// 方案B将 logout 函数存到模块变量(不推荐,破坏封装)
// 方案C刷新失败时返回 res状态码 401调用方 useEffect 捕获后执行 logout()

FE-P1-3 全局 catch {} 静默吞掉所有 API 错误

属性 内容
文件 所有页面组件
行号 paper/page.tsx:25,72,128,176,257,295,384signals/page.tsx:101,180,233page.tsx:137,143
严重性 🟠 P1

问题描述

整个项目的 API 调用模式为:

try { const r = await authFetch(...); if (r.ok) setData(...); } catch {}

当网络中断、服务器 5xx、或 token 失效时,数据状态不更新,也不给用户任何提示。用户面对的是静止的数字,无法判断是"市场没动"还是"系统断连"。

尤其危险的场景:实盘监控时,服务器崩溃,前端 signal page 显示的是上一个 15 秒前的评分,用户以为还在监控但实际上系统已宕机。

建议修复:至少在每个组件维护一个 error: string | null state

const [error, setError] = useState<string | null>(null);
try {
  const r = await authFetch(...);
  if (!r.ok) { setError(`API ${r.status}`); return; }
  setData(await r.json());
  setError(null);
} catch (e) {
  setError("网络连接失败");
}
// 渲染时显示 error && <div className="text-red-500">⚠ {error}</div>

FE-P1-4 LatestSignals 串行发 4 个 API 请求,每轮耗时约 2 秒

属性 内容
文件 frontend/app/paper/page.tsx
行号 119-132
严重性 🟠 P1

问题描述

for (const sym of COINS) {  // 串行!
  const r = await authFetch(`...?symbol=${sym}...`);
  setSignals(prev => ({ ...prev, [sym]: j.data[0] }));  // 触发 4 次 re-render
}

4 个请求串行执行,每轮约 500ms × 4 = 2 秒。每次 setSignals 都触发组件重新渲染,共 4 次。每 15 秒一轮,共消耗约 2/15 = 13% 的时间在等待 API。

建议修复:改用并行请求 + 批量 state 更新:

const results = await Promise.allSettled(
  COINS.map(sym => authFetch(`/api/signals/signal-history?symbol=${sym.replace("USDT","")}&limit=1`))
);
const newSignals: Record<string, any> = {};
for (const [i, result] of results.entries()) {
  if (result.status === "fulfilled" && result.value.ok) {
    const j = await result.value.json();
    if (j.data?.[0]) newSignals[COINS[i]] = j.data[0];
  }
}
setSignals(prev => ({ ...prev, ...newSignals }));  // 1 次 re-render

FE-P2 — 数据展示与 UX 问题

FE-P2-1 MiniKChart 每 30 秒销毁并重建图表,导致视觉闪烁

属性 内容
文件 frontend/app/page.tsx
行号 52-78MiniKChart 组件)
// 每次 render() 调用:
chartRef.current?.remove();              // 销毁旧图表
const chart = createChart(ref.current, baseChartOpts(220));  // 重建
series.setData([...]);                   // 重新写入所有数据

用户每 30 秒会看到 K 线图短暂消失再出现destroy → create 有约 100ms 空白)。

建议修复:只在 symbol/interval 变化时重建,数据更新时只调用 series.setData()

const seriesRef = useRef<any>(null);
// 初始化时: 创建 chart + series
// 数据更新时: seriesRef.current?.setData(newBars);

FE-P2-2 async render 未检查组件挂载状态(潜在内存泄漏)

属性 内容
文件 frontend/app/page.tsx
行号 52-78

cleanup 函数执行 chartRef.current = null 时,如果 render()await api.kline() 仍 in-flight后续代码继续运行可能在已卸载的组件上调用 setState 或操作已销毁的 chart 对象。

建议修复

useEffect(() => {
  let mounted = true;
  const doRender = async () => {
    const json = await api.kline(symbol, interval);
    if (!mounted || !ref.current) return;  // ← 检查挂载状态
    // ... 渲染逻辑
  };
  doRender();
  const iv = window.setInterval(doRender, 30_000);
  return () => { mounted = false; window.clearInterval(iv); chartRef.current?.remove(); };
}, [symbol, interval, mode]);

FE-P2-3 ControlPanel 对所有登录用户显示,非 admin 点击无任何反馈

属性 内容
文件 frontend/app/paper/page.tsx
行号 20-65

"启动/停止模拟盘"按钮对所有已登录用户显示。非 admin 用户点击后,后端返回 403catch {} 吞掉,用户界面无任何反应(按钮状态不变,也无错误提示)。

建议修复

const { isAdmin } = useAuth();
// 方案A隐藏按钮
{isAdmin && <button onClick={toggle}>...</button>}
// 方案Bdisable + tooltip
<button disabled={!isAdmin} title={isAdmin ? undefined : "仅管理员可操作"}>

FE-P2-4 ActivePositions WebSocket 无断线重连逻辑

属性 内容
文件 frontend/app/paper/page.tsx
行号 181-195

WebSocket 连接断开后网络抖动、Binance 服务重启),wsPrices 停止更新,组件降级到 REST 价格10 秒延迟),但没有任何视觉提示告诉用户"实时价格已断线"。对于监控持仓的页面,用户可能误以为价格在实时更新。

建议修复

ws.onclose = () => {
  setTimeout(() => { /* 重建 WebSocket */ }, 3000);
  setWsConnected(false);  // 显示"价格延迟"警告
};
ws.onerror = () => ws.close();

FE-P2-5 浮动盈亏 1R=$200 硬编码(前端 duplicate 了后端的同类问题)

属性 内容
文件 frontend/app/paper/page.tsx
行号 217
const unrealUsdt = unrealR * 200;  // 1R = $200 硬编码

与后端 main.py:554 同样问题。若 initial_balancerisk_per_trade 变化,此处不随之更新。


FE-P2-6 market-indicators 每 5 秒轮询,但数据 5 分钟才更新一次

属性 内容
文件 frontend/app/signals/page.tsx
行号 101-112
const iv = setInterval(fetch, 5000);  // 每5秒请求一次

market_data_collector 每 300 秒采集一次,数据变化频率与轮询频率相差 60 倍,制造了 59/60 的冗余请求。

建议修复:将间隔改为 300_0005 分钟),或从后端提供 Last-Modified / ETag 头支持 conditional GET。


FE-P2-7 LayerScore factors 缺失时用比例估算,逻辑有误

属性 内容
文件 frontend/app/signals/page.tsx
行号 336-340
score={data.factors?.direction?.score ?? Math.min(Math.round(data.score * 0.45), 45)}

factors 为 null旧版记录总分 × 权重 来估算各层分数。这假设各层得分比例与权重一致,但实际评分各层独立,可能出现方向层满分 45 而其他层 0 分但总分仍为 45 的情况,此时展示的进度条会严重失真。


FE-P2-8 any 类型大量使用,绕过 TypeScript 类型安全

属性 内容
文件 paper/page.tsx, signals/page.tsx
行号 paper:21,70,116,289,381
const [config, setConfig] = useState<any>(null);
const [data, setData] = useState<any>(null);

使用 any 意味着 API 返回异常结构时,运行时错误(Cannot read property 'enabled' of undefined)不会在编译期被发现。对于交易数据展示,null?.toFixed() 会显示 "undefined" 或 "NaN",误导用户。

建议修复:为每个 API 响应定义 TypeScript interface参考 lib/api.ts 已有的模式)。


FE-P3 — 安全问题

FE-P3-1 Token 存储在 localStorageXSS 风险)

属性 内容
文件 frontend/lib/auth.tsx
行号 33-34、44-48

localStorage 可被页面上运行的任意 JavaScript 访问。若某个 npm 依赖包含恶意代码(供应链攻击),或存在 XSS 漏洞access_token 和 refresh_token 将被盗取,攻击者可以任意访问该用户的所有 API。

建议修复:将 token 改为 httpOnly; Secure; SameSite=Strict cookie浏览器 JavaScript 无法读取:

# 后端新增 /api/auth/session 端点Set-Cookie 响应头设置 httpOnly cookie
# 前端 fetch 请求加 credentials: "include",无需手动处理 token

FE-P3-2 密码强度校验仅要求 6 位,无复杂度要求

属性 内容
文件 frontend/app/register/page.tsx

注册时仅校验密码长度 ≥ 6无字符复杂度要求。对于访问交易数据的账号建议至少要求大小写+数字+符号。


FE-P3-3 Promise.all 中任一请求失败导致所有数据丢失

属性 内容
文件 frontend/app/page.tsx
行号 144
const [s, h, sig, y] = await Promise.all([api.stats(), api.history(), api.signalsHistory(), api.statsYtd()]);

/api/stats/ytd 因 Binance API 限速1000条/次)超时,整个 Promise.all 失败,stats/history/signals 数据也全部丢失,用户看到页面数据全部空白。

建议修复:改用 Promise.allSettled

const [s, h, sig, y] = await Promise.allSettled([api.stats(), api.history(), ...]);
if (s.status === "fulfilled") setStats(s.value);
// 部分失败不影响其他数据展示

前端问题汇总表

ID 优先级 文件 行号 标题
FE-P1-1 🟠 P1 lib/auth.tsx 113-134 并发401时多个刷新请求竞态
FE-P1-2 🟠 P1 lib/auth.tsx 127-133 刷新失败后AuthContext状态未同步
FE-P1-3 🟠 P1 所有页面组件 广泛 全局catch{}静默吞掉所有API错误
FE-P1-4 🟠 P1 paper/page.tsx 119-132 LatestSignals串行4请求耗时2秒
FE-P2-1 🟡 P2 app/page.tsx 52-78 MiniKChart每30秒销毁重建视觉闪烁
FE-P2-2 🟡 P2 app/page.tsx 52-78 async render未检查组件挂载状态
FE-P2-3 🟡 P2 paper/page.tsx 20-65 ControlPanel对非admin用户可见无反馈
FE-P2-4 🟡 P2 paper/page.tsx 181-195 WebSocket无断线重连逻辑
FE-P2-5 🟡 P2 paper/page.tsx 217 浮动盈亏1R=$200前端硬编码
FE-P2-6 🟡 P2 signals/page.tsx 101-112 market-indicators每5秒轮询但5分钟更新
FE-P2-7 🟡 P2 signals/page.tsx 336-340 LayerScore factors估算逻辑失真
FE-P2-8 🟡 P2 paper/page.tsx等 广泛 大量any类型绕过TypeScript安全
FE-P3-1 🔵 P3 lib/auth.tsx 33-48 Token存于localStorageXSS风险
FE-P3-2 🔵 P3 register/page.tsx 密码强度仅6位无复杂度要求
FE-P3-3 🔵 P3 app/page.tsx 144 Promise.all任一失败导致全部数据丢失

前端优先修复建议

接入实盘前必须完成P1

  1. FE-P1-1 + FE-P1-2:用单例 Promise 防止并发刷新竞态,刷新失败时通过事件总线同步 AuthContext 强制重新登录
  2. FE-P1-3:为所有轮询组件添加 error state显示"连接失败"提示,让用户知道数据是否过期

近期迭代P2

  1. FE-P2-4ActivePositions WebSocket 添加重连逻辑和断线提示
  2. FE-P2-3ControlPanel 校验 isAdmin非 admin 隐藏或禁用按钮
  3. FE-P1-4LatestSignals 改为 Promise.allSettled 并行请求
  4. FE-P2-1MiniKChart 只在参数变化时重建图表,数据更新时复用

安全加固P3

  1. FE-P3-1:评估是否将 token 迁移到 httpOnly cookie
  2. FE-P3-3Promise.all 改为 Promise.allSettled,防止单个 API 失败影响整体

前端 inline 注释格式为 // [REVIEW] FE-P级 | 描述 | 建议,已写入各 .tsx 文件对应行。