- 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
380 lines
18 KiB
Markdown
380 lines
18 KiB
Markdown
# 全面代码审阅报告
|
||
|
||
> 生成时间: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 | 签名扫描 |
|