arbitrage-engine/docs/DETAILED_CODE_REVIEW.md
fanziqi 22787b3e0a docs: add AI documentation suite and comprehensive code review report
- Generate full AI-consumable docs (docs/ai/): system overview, architecture,
  module cheatsheet, API contracts, data model, build guide, decision log,
  glossary, and open questions (deep tier coverage)
- Add PROBLEM_REPORT.md: categorized bug/risk summary
- Add DETAILED_CODE_REVIEW.md: full line-by-line review of all 15 backend
  files, documenting 4 fatal issues, 5 critical deployment bugs, 4 security
  vulnerabilities, and 6 architecture defects with prioritized fix plan
2026-03-03 19:01:18 +08:00

380 lines
18 KiB
Markdown
Raw 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.

# 全面代码审阅报告
> 生成时间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` | 本地 PG127.0.0.1|
| live_executor 的 LISTEN 监听 | Cloud SQL10.106.0.3|
| live_executor 的轮询查 `signal_indicators` | Cloud SQL10.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 SQLDB_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 SQLF2 也保证 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`SQLiteV5 信号全在 PG 的 `signal_indicators` 表,此脚本从不读取
2. 只覆盖 BTC/ETHXRP/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`(建表 SQLvs `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`(建表 SQLvs `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/ETHXRP/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/ETHXRP/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 写本地 PGlive_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 | 签名扫描 |