- 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
18 KiB
全面代码审阅报告
生成时间: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()
证据链:
signal_engine.py:690-706:save_indicator()用get_sync_conn()(即 db.py 的PG_HOST=127.0.0.1)将信号写入本地 PG 的signal_indicators表live_executor.py:50-55(已知):DB_HOST默认10.106.0.3(Cloud SQL)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()
代码逻辑(从已读内容重建):
# 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
import sqlite3
DB_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "arb.db")
SYMBOLS = ["BTCUSDT", "ETHUSDT"] # ← XRP/SOL 不在监控范围
完整问题列表:
- 读
arb.db(SQLite),V5 信号全在 PG 的signal_indicators表,此脚本从不读取 - 只覆盖 BTC/ETH,XRP/SOL 的信号永远不会被推送
- Discord Bot Token 硬编码(详见 [S1])
- 是一个一次性运行脚本,不是守护进程,PM2 管理无意义
- 查询的 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
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
import sqlite3
DB_PATH = "arb.db" # SQLite
@router.get("/api/signals/history") # ← 与 main.py 同名
def signals_history(): ...
三个问题:
- 路由路径与
main.py:221的@app.get("/api/signals/history")完全相同 - 查询 SQLite
arb.db,V5 体系已无此数据 main.py从未include_router(subscriptions.router),所以目前是死代码
若将来有人误把 subscriptions.router 加进来,会与现有 PG 版本的同名路由冲突,FastAPI 会静默使用先注册的那个,导致难以排查的 bug。
🟠 安全漏洞
[S1] Discord Bot Token 硬编码在源代码(高危)
定位:signal_pusher.py:~25
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)
_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
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
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
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 体系下不再写入任何数据。这个端点对前端返回的是永久为空的结果,但没有任何错误信息,调用方无从判断是数据为空还是系统正常运行。
🟢 值得记录的正确设计
以下是审阅过程中发现的值得肯定的设计,供参考:
-
position_sync.py设计完整:SL 丢失自动重挂、TP1 命中后 SL 移至保本、实际成交价查询、资金费用追踪(每8小时结算窗口),覆盖了实盘交易的主要边界情况。 -
risk_guard Fail-Closed 模式正确:
/tmp/risk_guard_state.json不存在时,live_executor 默认拒绝交易,而不是放行,安全方向正确。 -
paper_monitor.py 使用 WebSocket 实时价格:比 signal_engine 15 秒轮询更适合触发止盈止损,不会因为 15 秒间隔错过快速穿越的价格。
-
agg_trades_collector.py 的数据完整性保障:每 60 秒做连续性检查,断点处触发 REST 补录,每小时做完整性报告,设计周全。
-
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 | 签名扫描 |