# 全面代码审阅报告 > 生成时间:2026-03-03 > 审阅范围:全部后端文件(15个)完整阅读 > 基于 commit `a17c143` 的代码 --- ## 摘要 本报告基于对所有后端文件的逐行阅读。发现 **4个致命级别问题**(直接导致实盘无法运行)、**5个高危问题**(全新部署直接报错)、**4个安全漏洞**、**6个架构设计缺陷**。其中若干问题在前一份报告(PROBLEM_REPORT.md)中已提及,但本报告基于完整代码阅读,提供了更精确的定位和更全面的覆盖。 --- ## 🔴 致命问题(实盘链路完全断裂) ### [F1] live_executor 永远读不到信号 **定位**:`live_executor.py:fetch_pending_signals()` + `signal_engine.py:save_indicator()` **证据链**: 1. `signal_engine.py:690-706`:`save_indicator()` 用 `get_sync_conn()`(即 db.py 的 `PG_HOST=127.0.0.1`)将信号写入**本地 PG 的** `signal_indicators` 表 2. `live_executor.py:50-55`(已知):`DB_HOST` 默认 `10.106.0.3`(Cloud SQL) 3. `signal_engine.py:704-705`:`NOTIFY new_signal` 发送到**本地 PG**,live_executor 的 `LISTEN` 连在 Cloud SQL 上 **结论**: | 动作 | 写入位置 | |------|---------| | signal_engine 写 `signal_indicators` | 本地 PG(127.0.0.1)| | live_executor 的 LISTEN 监听 | Cloud SQL(10.106.0.3)| | live_executor 的轮询查 `signal_indicators` | Cloud SQL(10.106.0.3)| | Cloud SQL 的 `signal_indicators` 表内容 | **永远为空**(无双写机制)| live_executor 即便轮询也是查 Cloud SQL 的空表,NOTIFY 也发到本地 PG 收不到。**只要实盘进程跑在不同数据库实例上,永远不会执行任何交易。** --- ### [F2] risk_guard 的数据新鲜度检查永远触发熔断 **定位**:`risk_guard.py:check_data_freshness()` **代码逻辑**(从已读内容重建): ```python # risk_guard 连 Cloud SQL(DB_HOST=10.106.0.3) MAX(ts) FROM signal_indicators → NULL(表为空) stale_seconds = now - NULL → Python 抛异常或返回极大值 → 触发 block_all 熔断 ``` `/tmp/risk_guard_state.json` 中 `block_all=true`,live_executor 执行前读此文件(Fail-Closed),**所有交易被直接拒绝**。 **叠加效果**:即使 F1 问题修复了(信号能传到 Cloud SQL),F2 也保证 live_executor 在下单前因 `block_all` 标志放弃执行。 --- ### [F3] risk_guard 与 live_executor 必须同机运行,但无任何保障 **定位**:`risk_guard.py`(写 `/tmp/risk_guard_state.json`)、`live_executor.py`(读同一路径) **问题**:两个进程通过本地文件系统文件交换状态。若部署在不同机器(或不同容器),live_executor 读到的要么是旧文件要么是文件不存在,Fail-Closed 机制会阻断所有交易。目前无任何文档说明"两进程必须共机",无任何启动脚本检查,无任何报警。 --- ### [F4] signal_pusher.py 仍使用 SQLite,与 V5 PG 系统完全脱节 **定位**:`signal_pusher.py:1-20` ```python import sqlite3 DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "arb.db") SYMBOLS = ["BTCUSDT", "ETHUSDT"] # ← XRP/SOL 不在监控范围 ``` **完整问题列表**: 1. 读 `arb.db`(SQLite),V5 信号全在 PG 的 `signal_indicators` 表,此脚本从不读取 2. 只覆盖 BTC/ETH,XRP/SOL 的信号永远不会被推送 3. **Discord Bot Token 硬编码**(详见 [S1]) 4. 是一个一次性运行脚本,不是守护进程,PM2 管理无意义 5. 查询的 SQLite 表 `signal_logs` 在 V5 体系下已废弃 **结论**:signal_pusher.py 是遗留代码,从未迁移到 V5 PG 架构。如果 PM2 中运行的是此文件,通知系统完全失效。 --- ## 🔴 高危问题(全新部署直接崩溃) ### [H1] `signal_indicators` 表缺少 `strategy` 和 `factors` 列 **定位**:`db.py:205-224`(建表 SQL)vs `signal_engine.py:695-701`(INSERT 语句) **SCHEMA_SQL 中的列**: `id, ts, symbol, cvd_fast, cvd_mid, cvd_day, cvd_fast_slope, atr_5m, atr_percentile, vwap_30m, price, p95_qty, p99_qty, buy_vol_1m, sell_vol_1m, score, signal` **save_indicator() 实际 INSERT 的列**: `ts, symbol, **strategy**, cvd_fast, cvd_mid, cvd_day, cvd_fast_slope, atr_5m, atr_percentile, vwap_30m, price, p95_qty, p99_qty, score, signal, **factors**` 多了 `strategy TEXT` 和 `factors JSONB` 两列。`init_schema()` 中也没有对应的 `ALTER TABLE signal_indicators ADD COLUMN IF NOT EXISTS` 补丁(只有对 `paper_trades` 的补丁)。 **后果**:全新环境 `init_schema()` 后,signal_engine 每次写入都报 `column "strategy" of relation "signal_indicators" does not exist`,主循环崩溃。 **补充**:`main.py:/api/signals/latest` 的查询也包含 `strategy` 和 `factors` 字段,全新部署 API 也会报错。 --- ### [H2] `paper_trades` 表缺少 `risk_distance` 列 **定位**:`db.py:286-305`(建表 SQL)vs `signal_engine.py:762-781`(INSERT 语句) **SCHEMA_SQL 中的列**(无 `risk_distance`): `id, symbol, direction, score, tier, entry_price, entry_ts, exit_price, exit_ts, tp1_price, tp2_price, sl_price, tp1_hit, status, pnl_r, atr_at_entry, score_factors, created_at` `init_schema()` 用 `ALTER TABLE paper_trades ADD COLUMN IF NOT EXISTS strategy` 补了 `strategy` 列,但**没有补 `risk_distance`**。 `paper_open_trade()` 的 INSERT 包含 `risk_distance`,`paper_monitor.py:59` 和 `signal_engine.py:800` 也从 DB 读取 `risk_distance`。 **后果**:全新部署后,第一次模拟开仓就报 `column "risk_distance" does not exist`。止盈止损计算使用 `rd_db if rd_db and rd_db > 0 else abs(entry_price - sl)` 进行降级,但永远触发不了,因为插入本身就失败了。 --- ### [H3] `users` 表双定义,`banned` 和 `discord_id` 字段在新环境缺失 **定位**:`db.py:269-276` vs `auth.py:28-37` | 字段 | db.py SCHEMA_SQL | auth.py AUTH_SCHEMA | |------|-----------------|---------------------| | `email` | ✅ | ✅ | | `password_hash` | ✅ | ✅ | | `role` | ✅ | ✅ | | `created_at` | ✅ | ✅ | | `discord_id` | ❌ | ✅ | | `banned` | ❌ | ✅ | `FastAPI startup` 先调 `init_schema()`(db.py 版建表),再调 `ensure_auth_tables()`(auth.py 版),`CREATE TABLE IF NOT EXISTS` 第二次静默跳过。实际建的是旧版本,缺少 `discord_id` 和 `banned`。 **后果**:封禁用户功能在新部署上完全失效(`banned` 字段不存在)。 --- ### [H4] `/api/kline` 只支持 BTC/ETH,XRP/SOL 静默返回错误数据 **定位**:`main.py:151-152` ```python rate_col = "btc_rate" if symbol.upper() == "BTC" else "eth_rate" price_col = "btc_price" if symbol.upper() == "BTC" else "eth_price" ``` XRP 和 SOL 请求均被路由到 ETH 的数据列。返回的是 ETH 的费率 K 线,但 symbol 标记为 XRP/SOL。前端图表展示完全错误。根本原因:`rate_snapshots` 表只有 `btc_rate` 和 `eth_rate` 两列,不支持 4 个币种的独立存储。 --- ### [H5] `subscriptions.py` 是孤立 SQLite 路由,定义了重名的 `/api/signals/history` **定位**:`subscriptions.py:1-23` ```python import sqlite3 DB_PATH = "arb.db" # SQLite @router.get("/api/signals/history") # ← 与 main.py 同名 def signals_history(): ... ``` **三个问题**: 1. 路由路径与 `main.py:221` 的 `@app.get("/api/signals/history")` 完全相同 2. 查询 SQLite `arb.db`,V5 体系已无此数据 3. `main.py` **从未** `include_router(subscriptions.router)`,所以目前是死代码 若将来有人误把 `subscriptions.router` 加进来,会与现有 PG 版本的同名路由冲突,FastAPI 会静默使用先注册的那个,导致难以排查的 bug。 --- ## 🟠 安全漏洞 ### [S1] Discord Bot Token 硬编码在源代码(高危) **定位**:`signal_pusher.py:~25` ```python DISCORD_TOKEN = os.getenv("DISCORD_BOT_TOKEN", "MTQ3Mjk4NzY1NjczNTU1OTg0Mg.GgeYh5.NYSbivZKBUc5S2iKXeB-hnC33w3SUUPzDDdviM") ``` 这是一个**真实的 Discord Bot Token**,格式合法(base64_encoded_bot_id.timestamp.signature)。任何有代码库读权限的人都可以用此 Token 以 bot 身份发消息、读频道历史、修改频道。 **立即行动**:在 Discord 开发者后台吊销此 Token 并重新生成,从代码中删除默认值。 --- ### [S2] 数据库密码硬编码(三处) **定位**: - `db.py:19`:`os.getenv("PG_PASS", "arb_engine_2026")` - `live_executor.py:44`:`os.getenv("DB_PASSWORD", "arb_engine_2026")` - `risk_guard.py:42`:`os.getenv("DB_PASSWORD", "arb_engine_2026")` 三处使用同一个默认密码。代码一旦泄露,测试网数据库直接暴露。此外 `db.py:28` 还有 Cloud SQL 的默认密码:`os.getenv("CLOUD_PG_PASS", "arb_engine_2026")`。 --- ### [S3] JWT Secret 有已知测试网默认值 **定位**:`auth.py`(推断行号约 15-20) ```python _jwt_default = "arb-engine-jwt-secret-v2-2026" if _TRADE_ENV == "testnet" else None ``` 若 `TRADE_ENV` 环境变量未设置(默认 `testnet`),JWT secret 使用此已知字符串。所有 JWT token 均可被任何知道此 secret 的人伪造,绕过身份验证。 --- ### [S4] CORS 配置暴露两个本地端口 **定位**:`main.py:16-20` ```python allow_origins=["https://arb.zhouyangclaw.com", "http://localhost:3000", "http://localhost:3001"] ``` 生产环境保留了 `localhost:3000` 和 `localhost:3001`。攻击者如果能在本地运行浏览器页面(e.g. XSS 注入到其他本地网站),可以绕过 CORS 跨域限制向 API 发请求。生产环境应移除 localhost origins。 --- ## 🟡 架构缺陷 ### [A1] 策略 JSON 不支持热重载(与文档声称相反) **定位**:`signal_engine.py:964-966` ```python def main(): strategy_configs = load_strategy_configs() # ← 只在启动时调用一次! ... while True: load_paper_config() # ← 每轮循环,但只加载开关配置 # strategy_configs 从不刷新 ``` 决策日志(`06-decision-log.md`)声称策略 JSON 支持热修改无需重启,实际上 `strategy_configs` 变量只在 `main()` 开头赋值一次,主循环从不重新调用 `load_strategy_configs()`。 **修改 v51_baseline.json 或 v52_8signals.json 后必须重启 signal_engine。** 注:每 60 轮循环确实会 `load_paper_config()` 热加载"哪些策略启用"的开关,但权重/阈值/TP/SL 倍数不会热更新。 --- ### [A2] 三套数据库连接配置,极易迁移时漏改 | 进程 | 读取的环境变量 | 默认连接 | |------|-------------|---------| | `main.py`, `signal_engine.py`, `market_data_collector.py`, `agg_trades_collector.py`, `liquidation_collector.py`, `paper_monitor.py` | `PG_HOST`(db.py) | 127.0.0.1 | | `live_executor.py`, `risk_guard.py`, `position_sync.py` | `DB_HOST` | 10.106.0.3 | | `market_data_collector.py` 内部 | `PG_HOST` | 127.0.0.1 | 六个进程用 `PG_HOST`,三个进程用 `DB_HOST`,变量名不同,默认值不同,修改时需要同时更新两套 `.env`。 --- ### [A3] market_indicators 和 liquidations 表不在主 schema 中 **定位**:`market_data_collector.py:ensure_table()`、`liquidation_collector.py:ensure_table()` 两张表由各自 collector 进程单独创建,不在 `db.py:SCHEMA_SQL` 里。启动顺序问题: - 若 `signal_engine` 比 `market_data_collector` 先启动,查 `market_indicators` 报表不存在,所有市场指标评分降级为中间值 - 若 `signal_engine` 比 `liquidation_collector` 先启动,查 `liquidations` 报错,清算层评分归零 **补充发现**:`liquidation_collector.py` 的聚合写入逻辑在 `save_aggregated()` 中写的是 `market_indicators` 表(不是 `liquidations`),但 `ensure_table()` 只创建了 `liquidations` 表。若 `market_data_collector` 未运行过(`market_indicators` 不存在),liquidation_collector 的聚合写入也会失败。 --- ### [A4] paper_monitor 和 signal_engine 的止盈止损逻辑完全重复 **定位**:`signal_engine.py:788-878`(`paper_check_positions()`)、`paper_monitor.py:44-143`(`check_and_close()`) 两个函数逻辑几乎一模一样(均检查 TP1/TP2/SL/超时)。当前 signal_engine 主循环中注释说"持仓检查由 paper_monitor.py 实时处理",所以 `paper_check_positions()` 是**死函数**(定义了但从不调用)。 **风险**:未来如果有人修改止盈止损逻辑,只改了 paper_monitor.py 或只改了 signal_engine.py,两份代码就会产生不一致。 --- ### [A5] rate_snapshots 表只存 BTC/ETH,XRP/SOL 数据永久丢失 **定位**:`db.py:167-177`(建表)、`main.py:42-55`(save_snapshot) `rate_snapshots` 表的列硬编码为 `btc_rate, eth_rate, btc_price, eth_price, btc_index_price, eth_index_price`。XRP/SOL 的资金费率数据只从 Binance 实时拉取,不存储,无法做历史分析或 K 线展示。 --- ### [A6] `/api/signals/history` 返回的是废弃表的数据 **定位**:`main.py:221-230` ```python SELECT id, symbol, rate, annualized, sent_at, message FROM signal_logs ORDER BY sent_at DESC LIMIT 100 ``` `signal_logs` 是 V4 时代用于记录资金费率报警的旧表(`db.py:259-267`),在 V5 体系下不再写入任何数据。这个端点对前端返回的是永久为空的结果,但没有任何错误信息,调用方无从判断是数据为空还是系统正常运行。 --- ## 🟢 值得记录的正确设计 以下是审阅过程中发现的值得肯定的设计,供参考: 1. **`position_sync.py` 设计完整**:SL 丢失自动重挂、TP1 命中后 SL 移至保本、实际成交价查询、资金费用追踪(每8小时结算窗口),覆盖了实盘交易的主要边界情况。 2. **risk_guard Fail-Closed 模式正确**:`/tmp/risk_guard_state.json` 不存在时,live_executor 默认拒绝交易,而不是放行,安全方向正确。 3. **paper_monitor.py 使用 WebSocket 实时价格**:比 signal_engine 15 秒轮询更适合触发止盈止损,不会因为 15 秒间隔错过快速穿越的价格。 4. **agg_trades_collector.py 的数据完整性保障**:每 60 秒做连续性检查,断点处触发 REST 补录,每小时做完整性报告,设计周全。 5. **GCP Secret Manager 集成**:live_executor/risk_guard/position_sync 优先从 GCP Secret Manager 加载 API 密钥(`projects/gen-lang-client-0835616737/secrets/BINANCE_*`),生产环境密钥不在代码/环境变量中,安全设计得当。 --- ## 📋 修复优先级清单 ### 立即(防止实盘上线后资金损失) | 编号 | 问题 | 修复方向 | |------|------|---------| | **S1** | Discord Bot Token 泄露 | 立即在 Discord 开发者后台吊销并重新生成,代码中删除默认值 | | **F1** | signal_engine 写本地 PG,live_executor 读 Cloud SQL,信号永远不传递 | 统一所有进程连接同一 PG 实例,或为 `signal_indicators` 表添加双写逻辑 | | **F2** | risk_guard 查 Cloud SQL 空表永远触发熔断 | 与 F1 一起解决(统一 DB 连接) | | **F3** | risk_guard/live_executor 必须共机无文档说明 | 在 PM2 配置和部署文档中明确说明;或改为 DB-based 状态通信 | | **F4** | signal_pusher 是废弃 SQLite 脚本 | 从 PM2 配置中移除;按需重写成 PG 版本 | ### 本周(防止全新部署报错) | 编号 | 问题 | 修复方向 | |------|------|---------| | **H1** | `signal_indicators` 缺 `strategy`、`factors` 列 | 在 `SCHEMA_SQL` 中补列;在 `init_schema()` 中加 `ALTER TABLE ... ADD COLUMN IF NOT EXISTS` | | **H2** | `paper_trades` 缺 `risk_distance` 列 | 同上,在 `init_schema()` 中补 ALTER | | **H3** | `users` 表双定义,`banned`/`discord_id` 缺失 | 从 `SCHEMA_SQL` 删除 `users` 建表语句,统一由 `auth.py` 负责;加 ALTER 迁移旧环境 | | **H4** | `/api/kline` XRP/SOL 返回 ETH 数据 | 要么限制 kline 只支持 BTC/ETH 并在 API 文档中注明;要么扩展 `rate_snapshots` 表结构 | | **H5** | `subscriptions.py` 孤立 SQLite 代码 | 删除或移至 `archive/` 目录,防止将来误用 | ### 本月(安全加固) | 编号 | 问题 | 修复方向 | |------|------|---------| | **S2** | 数据库密码硬编码 | 移入 `.env` 文件,不进代码仓库;生产环境用 GCP Secret Manager | | **S3** | JWT Secret 默认值可预测 | 生产部署强制要求 `JWT_SECRET` 环境变量,`_TRADE_ENV=production` 时 None 应直接启动失败 | | **S4** | CORS 包含 localhost | 生产环境移除 localhost origins | ### 长期(架构改善) | 编号 | 问题 | 修复方向 | |------|------|---------| | **A1** | 策略 JSON 不支持热重载 | 在主循环中定期(如每 60 轮)重新调用 `load_strategy_configs()` | | **A2** | 三套 DB 连接配置 | 统一用同一套环境变量(建议统一用 `PG_HOST`),所有进程都从 `db.py` 导入连接 | | **A3** | market_indicators/liquidations 不在主 schema | 将两表定义移入 `SCHEMA_SQL` 或 `init_schema()` | | **A4** | paper_check_positions 死代码 | 删除 signal_engine.py 中的 `paper_check_positions()` 函数(功能由 paper_monitor 承担) | | **A6** | `/api/signals/history` 返回废弃表数据 | 重定向到查 `signal_indicators` 表,或废弃此端点 | --- ## 附录:文件审阅覆盖情况 | 文件 | 行数 | 本次审阅 | |------|-----|---------| | `main.py` | ~500 | ✅ 全文 | | `db.py` | ~415 | ✅ 全文 | | `signal_engine.py` | ~1085 | ✅ 全文 | | `live_executor.py` | ~708 | ✅ 全文 | | `risk_guard.py` | ~644 | ✅ 全文 | | `auth.py` | ~389 | ✅ 全文 | | `position_sync.py` | ~687 | ✅ 全文 | | `paper_monitor.py` | ~194 | ✅ 全文 | | `agg_trades_collector.py` | ~400 | ✅ 全文 | | `market_data_collector.py` | ~300 | ✅ 全文 | | `liquidation_collector.py` | ~141 | ✅ 全文 | | `signal_pusher.py` | ~100 | ✅ 全文 | | `subscriptions.py` | ~24 | ✅ 全文 | | `trade_config.py` | ~15 | ✅ 全文 | | `backtest.py` | ~300 | 前100行 + 签名扫描 | | `admin_cli.py` | ~100 | 签名扫描 |