tradingSystem/CODE_REVIEW_2026-08-28.md

341 lines
35 KiB
Markdown
Raw Permalink 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-08-28
## 一、这次审查做了什么
审查对象是提交 654d88f2026-08-28 09:51时的全部代码。范围包括 app 目录约一万八千行、scripts 全部脚本、前端页面、配置与环境模板。
审查分四层做。第一层,全部 86 个 Python 文件做语法编译检查,全部通过。第二层,把代码快照到一个干净的 Python 3.11 环境,装好依赖,把项目自带的 560 例单测完整跑了一遍,结果是 ALL SUITES PASS。第三层用静态分析工具扫了一遍没有未定义变量这类硬伤。第四层是重点分十二路对全部模块逐行人工审查然后把重要发现逐条回到代码里复核真伪。
一句话结论:编译和单测全部通过,但逐行审查发现了一批真问题。其中最严重的十四条我已经逐条复核确认。这批问题多数出在两类地方:一类是单测的桩和真实表结构脱节,测试测不到;另一类是模块之间的接线处,每个模块自己是对的,拼起来是错的。
下文每条问题都给出文件和行号、一句话结论、为什么错、什么场景触发。行号以当天快照为准。
---
## 二、已复核确认的严重问题(十四条)
这一档的共同点是:会造成错账、错单、假状态,或者让一道安全闸完全失效。
### 1. 联调单识别闸从未生效过,假成交会按真单入账
位置:`app/services/ledger_service.py` 第 127 行。
入账三分流时读的是 `od.get("parent_instruction_id")`,但出口表 `pms_qmt_order` 的真实列名是 `parent_id`(见 `ddl_pms_v1.sql` 第 215 行和 `qmt_repo.enqueue_order`)。这个键永远取不到值,`is_smoke` 永远返回否所以联调测试单SMOKE 前缀)的成交全部落进真单分支入账。
这正是代码注释里记载过的那类事故:模拟端按 99.99 元造假成交,批次按荒谬价核销,摊薄成本和安全垫全错。这道闸写了,但从没起过作用。
单测没抓住它的原因很说明问题:`scripts/test_wiring.py` 第 380 到 382 行的测试桩把键名也写成了 `parent_instruction_id`,还配了一句错误注释说真表就叫这个名。桩错得和代码一样,所以全绿。
触发场景ws 通道在线时跑一次 `scripts/ws_smoke.py` 联调,标的恰好在持仓里,一分钟内假成交就进账本。
修法方向:第 127 行改读 `parent_id`,同时把 test_wiring 桩的键名和注释一起改对,再补一条用真实列名的用例。
### 2. 成交回放游标按字典序推进,会永久漏掉一部分成交
位置:`app/repo/downstream_repo.py` 第 126 到 147 行,配合 `app/core/recon.py``next_cursor`
回放的增量条件是 `order_id > :sid`,按字符串比较。但 `latest_filled_order_id` 自己的注释写明order_id 长得像 `SELL_601956.SH_1778549400`,是方向前缀加代码加时间戳,不是时间序。字符串比较时 BUY 开头的永远小于 SELL 开头的。游标一旦落在任何一条 SELL 上(冷启动取 MAX 几乎必然如此),此后所有买入成交、以及代码序更小的卖出成交,都满足不了「大于游标」,永远拉不到。
后果:影子期 trading_order 是唯一入账路径,手工买入这类外部成交会静默漏账。日终对账只能按数量差补一条 RECON 批次,真实成交价和费用丢了,摊薄成本从此不准。
触发场景:游标停在某条 SELL 之后,发生任何 BUY 成交。
修法方向:游标别用 order_id改按成交时间加自增主键推进或者拉取条件放宽成按时间窗拉、逐单靠已见集合去重`recon.map_fills_to_book` 本来就有 `known_order_ids` 参数,一直没接线,见第 4 条)。
### 3. ws 成交入账不是「精确一次」:批内出错会重复入账
位置:`app/services/ledger_service.py` 第 199 到 206 行,配合 `_apply_action`(第 331 行起)。
入账循环逐笔执行,出错记入 errors但「标记已入账」是整批的只有全程无错才把这批 inbox 行标成已处理。批内任何一笔出错(一笔成交入账失败、费用写入失败、成本重算失败都算),已经成功入账的其余笔也留在未处理状态,下一分钟整批重来,再入一遍账。
代码注释说「重试是安全的,幂等由成交号兜着」,这句话不成立。成交号去重只在收件那一层,防的是同一笔成交插两行;`_apply_action` 本身没有任何按成交号防重的逻辑:买入直接插批次,卖出直接累加核销,批次表也没有成交号唯一键。重复消费一遍就是持仓翻倍、成本全错。(费用那条恰好有唯一键兜住,批次账没有。)
叠加因素worker 开了两个并发,回放任务每分钟发一跳、单跳最长可跑 300 秒,又没有互斥锁。前一跳超过一分钟,下一跳并发读到同一批未处理行,同样双入账。
反向问题在 trading_order 回放那条路(第 285 到 305 行):单笔入账失败只记 errors游标照样推进那笔成交永久丢失。同一种失败ws 路是重复入账,回放路是永久漏账,两边口径相反,两边都错。
修法方向:给批次入账加成交号幂等(批次表加成交号列和唯一键,或入账前按成交号查已入集合);标记改成逐笔成功逐笔标;回放路在游标推进前检查本批是否全部成功;把 `known_order_ids` 参数接上;给每分钟任务加互斥。
### 4. 撤销命令不撤下游,券商侧委托继续成交
位置:`app/services/command_service.py` 的 `cancel` 函数(第 426 到 443 行附近)。
撤销命令时,对在途指令只做了本端置 CANCELLED既不调 `executor.cancel_instruction`,也不发下游撤单。同一个文件里 `_cancel_marked_instructions` 的注释白纸黑字记着 2026-07-31 的教训:必须走 executor 的撤单,不能只把本端的行标掉,并且点名「撤销命令」是五条中招路径之一。但 `cancel` 本身恰恰还是老写法。
后果ws 模式下,用户在页面点「撤销命令」,看到「已撤销,撤回指令 N 条」,而出口表里排队和已发出的子单没人管,券商侧继续挂着继续成交。成交回来还会按父指令入账。用户以为踩了刹车,实际只是账面划掉。
触发场景ws 模式下撤销一条有在途指令的命令。
修法方向:`cancel` 里对每条在途指令改走 `executor.cancel_instruction`,失败的单独列出来,不能吞成「已撤销」。
### 5. 指令编号会撞车,命令从此每分钟报错、永久卡死
位置:`app/services/executor.py` 第 72 行和第 95 到 96 行。
方案转指令时,序号 `seq` 每次调用从零重数,指令编号是「日期、代码、动作、序号」,不含方案编号。指令表对编号有唯一键,而已终态(撤销、过期)的旧指令行还在。同一天对同一只票再下一次同动作的命令,新方案在扫描里排到同一个序号,生成同一个编号,插入报唯一键冲突。方案留在待转状态,下一分钟重算出同一个编号再撞,死循环,这条命令永远执行不了,错误日志每分钟一条。
触发场景:同日「下清仓命令、撤销、再对同一只票下清仓」;或上午一条降仓完成后,下午再下一条,同股同动作序号对齐即撞。
修法方向:编号里带上方案编号(方案编号本身已含命令号和序号,天然幂等),或撞键时改用时间戳序号重试。
### 6. 指令全部成交后,进度永远回不到方案,完成的命令被判「部分完成」
位置:`app/services/executor.py` 的 `sweep_windows`(第 330 行起),配合 `ledger_service._settle_instruction`
方案的已成交量只有 `sweep_windows` 一处回写,而它只扫「在途」状态的指令。但成交入账的同一个调用里,`_settle_instruction` 就把成交量达标的指令即时置成 CONFIRMED指令当场离开在途集合。于是最后一笔成交的增量必然漏同步几笔成交在同一次回放里到齐时方案的已成交量干脆停在零。
后果是主路径上的:命令进度按「金额乘以方案成交比例」结算,账永远欠一截,正常全部成交的命令拖到窗口末被判 PARTIAL 并告警;方案永久停在执行中,后续降仓命令还会把这些票当「已有在途方案」跳过。撤销和过期路径同样漏同步。
修法方向:在 `_settle_instruction` 置终态之前(或之后顺手)把 exec_qty 回写方案;或 sweep 扫描范围加上刚进终态的指令。
### 7. 盘前一键清仓会被判「已完成」,实际一股没卖
位置:`app/services/command_service.py` 的 `refresh_progress`,配合 `app/core/command_spec.py` 第 421 行和 `app/core/planner.py``plan_liquidate_all`
链条是这样的。清仓方案对取不到现价的持仓照样下单,但金额记零,命令的目标金额只累计有价票。盘前和夜间全部持仓都取不到现价,目标金额等于零。而进度结算规定「目标金额小于等于零就是 DONE」。命令轮询全天每分钟跑方案转指令却只在交易时段跑。于是夜里下的一键清仓下一分钟就被判 DONE方案永远不会转成指令一股不卖页面显示已完成。
一键清仓恰恰最容易在盘前盘后下达,这个场景不是边角。部分停牌票的变体也存在:有价票成交后命令提前 DONE停牌票的卖出还在途但命令已经不能撤销。
修法方向:目标金额为零但方案里有单时不判 DONE或清仓类命令的完成判据改成「全部方案到终态」。
### 8. 自主建仓的名额和金额不计在途,跨跳会超配
位置:`app/services/proposal_service.py` 第 216 到 217 行。
名额等于上限减「已成交持仓只数」,金额余量按已成交市值算。在途的建仓指令和等拍板的建仓提议,两头都不占。同票同动作有去重,换一只票就拦不住。扫描每分钟一跳:候选没进买入区间时指令滞留在途,下一跳名额照旧,又放两只。全自动档位下几分钟就能放出超过上限的新票建仓指令;有 ws 资金快照时金额侧有兜底(可用资金封顶),但影子模式没有,名额侧任何模式都没有。
修法方向:算名额和金额时把在途建仓指令与等拍板的建仓提议计入;执行终检同样计入。
### 9. 信号「试算」会把真信号永久吞掉
位置:`app/services/signal_service.py` 的 `_read`(第 310 行附近)和 `_ack`
消费用的是 Redis 消费组,读取参数是「只给从未投递过的消息」。消息一旦读到就进了本消费者的待确认清单;全模块没有任何找回待确认消息的逻辑。所以「读了不确认」不等于「下次还能拿到」,而是永远拿不到。
页面上的「信号消化试算」正踩在这上面:试算走同一个消费组读取,但刻意不确认。恰有一条未消化的高置信风控卖出时,点一次试算,这条消息被标记已投递、永不确认、永不重来,该票永远不会因这条信号清仓,唯一痕迹是待处理计数悄悄加一。正常消费中处理抛异常的消息,同样一次性永久滞留。
修法方向:试算改用只读命令(旁边的 `upstream_signals.py` 就是这么做的,用 XREVRANGE 不进消费组);正常消费补一段启动时找回待确认消息的逻辑。
### 10. 除权检测永远不会触发,而正常加仓日天天误报
位置:`app/services/ledger_service.py` 的 `detect_and_apply_ex_right`(第 922 行起),配合 `daily_settle` 的步骤顺序和 `app/core/recon.py``detect_ex_right`
除权检测比较的是「昨日快照数量」对「今日账本数量」。但送转股发生在券商账户,账本数量只会因成交或对账修正而变,而对账排在除权检测之后。所以除权当天,账本数量和昨日快照相等,检测返回无异动;随后对账把多出来的股数当普通数量差异,按下游成本价补一条 RECON 批次,老批次的成本价不做除权缩减,摊薄成本被抬高,安全垫假报深亏,补仓档位被凭空触发。次日快照已同步,再也检不出来。
反过来任何正常加仓日当天有买入成交账本数量真的比昨日快照多了检测就进入比例核对数量比是一点几价格反比接近一判成「疑似非除权变动ERROR 待人工」。也就是说,这个检测对真除权失明,对正常加仓天天误报。
修法方向:除权检测改用下游快照的数量跳变来比(对账拿到的下游持仓对昨日下游持仓),并把「当日有成交的票」排除出比例核对;`apply_ex_right` 对已部分核销的批次也要处理 closed_qty。
### 11. 「须人工全量对账」的标记活不过下一次重连
位置:`app/ws/runner.py` 第 344 行。
补发不全时置 resync_flag语义是「中间一段成交永远丢了必须人工全量对账后经页面清除」。但每次握手都调 `set_conn` 并传入本次算出的 resync 布尔值,正常重连传的是否,于是把标记写回零。断线置标记后几分钟网络恢复、正常重连,标记消失,只剩滚动日志里一行错误。丢失区间的成交没人补对账,成本静默错下去。页面专设的人工清除接口形同虚设。
修法方向:`set_conn` 只在 resync 为真时写标记,清零只留给页面接口。
### 12. 科创板两百股起的规则,规划与闸门层完全没有处理
位置:`app/core/sizer.py` 第 15 行LOT 等于 100planner 和 rule_gate 全部取整点。
科创板688、689 开头)买入申报两百股起。`strategy_advisor.py` 第 76 到 84 行自己承认:持仓里已经有 688802.SH全库其他地方一律按一百股一手两百股这条只在策略挂载侧兜了。规划器会切出一百股的批次规则闸按一百校验放行交易所拒单指令挂死到窗口过期。减仓摊派同样会切出一百股。
触发场景:候选池或持仓中出现科创板代码,而现在就真实持有一只。
修法方向:把 `strategy_advisor.lot_of` 这类「按代码定最小申报单位」的函数下沉到 sizerplanner、rule_gate、exec_timing 全部改用它。
### 13. 网格会在中枢上方买入,和它自己的说明相反
位置:`app/services/strategy_runner.py` 第 429 到 450 行,配合 `_grid_levels`
档位表是从下界到上界整个区间生成的,中枢上下都有档。下行分支的注释写着「只买中枢下方」,但代码对跌破的任何档都买,没有任何和中枢的比较。预算按「中枢以下档数」摊每档金额,前提也是只买下半区。
后果:价格先涨向上界再回落一档,就在中枢上方买入一手。价格在区间上半区震荡时,预算先被上半区吃掉,等真跌回吸筹区反而触到投入上限停买。恰好高买、低不买。
修法方向:下行买入分支加一条「档价低于中枢才买」。
### 14. 跟踪止盈的部分卖出没有闩锁,回撤期间会级联卖光
位置:`app/services/strategy_runner.py` 第 494 到 507 行。
设定「回撤百分之五卖一半」的本意是卖一次。但高水位只抬不降、武装标志永不复位,卖出后也没有任何「本次回撤已经卖过」的状态。上一笔卖出到终态后,价格仍在触发线下,条件再次成立,再卖剩余的一半。几何级联,直到剩不足一手。持一万股的票会在半小时级别被卖到只剩零头,和挂载说明完全不符。
修法方向:卖出后置一个「本轮回撤已处理」标志,高水位重新创高时复位;或每次卖出后把触发线下移。
---
## 三、已复核确认的中等问题
### 15. 操作留痕的「操作人」取自请求体,可以随意冒名
`app/web/main.py` 第 77 行已经把认证身份放进了 `request.state.user`,但所有写接口的操作人都取 `payload.get("by")`(第 289、292、245、414、780、791、851 行等)。任何登录用户都能在请求体里写别人的名字,下单、撤单、挂策略都记到别人头上。权限体系刚建,问责凭证却是客户端说了算。修法:操作人一律改取会话里的用户名,请求体的 by 顶多当备注。
### 16. 环境模板的两处敏感配置
`.env.example` 第 40 行把管理员标记写成了 `band:yhyqx`(「用户与权限」菜单),和 `config/settings.py` 的默认值 `pms:admin` 不一致。注释说明这是联测用的临时做法,但模板是运维会照抄的文件,凡持有这条通用权限的账号都会成为 PMS 系统管理员。联测结束后要记得收回来。另外第 19 行 `SIGNAL_REDIS_PASSWORD=wlkj2018` 看起来是真实密码,进了版本库就算泄漏,建议改成占位符并考虑轮换。
### 17. 宏观信号同日重扫的失败路径会冲掉当日命令留痕
`app/services/macro_service.py` 第 155 和 174 行附近:成功路径小心保留了当日已下命令的动作字段、单号和周期状态,但取数失败与数据守卫未过这两条路径按默认值整行覆盖(`macro_repo.upsert_signal` 是全字段更新)。盘中重扫恰逢行情源抖动,当天的「已下命令」痕迹被冲掉,「当日不重复下命令」的判据落空,之后再扫成功可再下一条同向命令;次日取昨日状态还会取到这行不可用记录,极值周期和退出事件丢失,偏热侧「回落再动」的降仓会被静默吞掉。修法:失败路径只更新 zone 和 note不动 action、ref_id、detail 里的 cycle。
### 18. 安全垫峰值清仓后不清零,复开仓的票一开仓就可能触发保垫减仓
`cushion_peak``ledger_service.py` 第 401 和 1170 行只做「取历史最大」,全库没有任何清零点。清仓再重新建仓的票带着上一轮的峰值:旧峰值百分之十二,新仓垫子百分之一,回吐条件立即成立,保垫减仓不设确认门槛直接落指令。候选池反复推荐旧票是常态,这条会真实触发。修法:持仓归零转 CLOSED 时把峰值一并清零。
### 19. GATED 批次没有任何解锁代码,建仓命令永远到不了「完成」
方案状态 GATED等条件解锁的补足与加仓批`executor.py` 第 9 行注释里说「由动作引擎解锁」,但全库 `update_plan` 只有四个调用点,没有一处把 GATED 转成 PENDING动作引擎里也没有这个词。命令目标金额含全部批次GATED 批永远不执行,每条建仓命令必然拖满窗口判 PARTIAL 并告警。另外撤销命令和调整窗口的 SQL`pms_repo.py` 的 `cancel_plans_of_command``set_plans_deadline`WHERE 里只认 PENDING 和 EXECUTINGGATED 批撤不掉也顺延不了。修法要么把解锁机制补上要么建仓命令的目标金额只算基础批、GATED 批单独呈现;两个 SQL 的状态清单加上 GATED。
### 20. 调整窗口对已转成指令的单子完全无效
`_adjust_window` 只改方案表的截止日和命令进度,但方案转指令时把截止日冻结进了指令自己的进度字段,此后配额、窗口末日强制完成、收口全读指令里那份。方案落表后一分钟内就会转指令,所以用户真去调窗口时指令早已存在,顺延等于没顺延。修法:调窗口时同步更新该命令下在途指令进度里的截止日。
### 21. 行业约束在一次库抖动后会静默失效一整天
`app/services/industry.py` 第 213 到 219 行:批量查行业抛异常时,这批票全部按「查不到」缓存为空并当日不再重试。「查不到」和「查询失败」被写进同一个按日缓存。开盘第一跳库超时一次,全天所有票的行业都是空,行业集中度硬拦截逐票跳过,而就绪探测走的是另一条路,故障恢复后报就绪,页面无任何异常。这违反本仓库自己反复强调的「拿不到不等于通过」。修法:异常路径不写缓存,下一跳重查。
### 22. 长假后第一个交易日取不到昨收,候选池会空一天
`app/services/market.py` 第 125 行:昨收回看窗按「天数乘一点六」个自然日算,默认三天只回看四点八天。春节国庆停市七到九个自然日,节后首日最近一根日线在窗外,昨收取空,候选池整日空掉。这正是该函数注释里说要修的 2026-07-31 故障在长假后的复发形态。同文件三个取数函数还把「查询异常」的空结果按日缓存,盘前一次抖动当天不再重试。修法:回看窗按交易日算或放宽到十五个自然日以上;异常不落缓存。
### 23. 同一批告警类小问题
这几条各自独立,一并列出。`signal_service` 的当日去重集合超过五百条时按字典序裁剪,丢的不是最旧的键(第 338 行)。提议编号是「日期加代码加动作」的确定性编号,驳回后的同日重建撞唯一键,该动作当日进不了队列且每分钟报错刷屏(`proposal_service.py` 第 538 行,信号路同病)。置信度归一只对「大于一」除以一百,零到一百制里的一会被当成百分之百,越低的置信被解释得越高,风控流置信度恰好是零到一百制(`app/core/signal_rules.py` 第 57 到 62 行)。动作引擎四个求值器互不知晓,同一只票同轮可以同时产出减仓和加仓两条候选,各自分流成一卖一买(`action_engine.py`,根子在保垫峰值是全时段而加仓判据是五日窗,两套时间基准)。规则闸持续拦截的指令每分钟写一行拒绝留痕,无当日去重,会把作为判分事实源的账本淹掉(`executor.py` 第 249 到 261 行,同文件参考位漂移刚为同类问题做过一天一条)。
### 24. 调度与通道层的几条
回放任务 15:05 停跑而日终对账 15:10 全量修正,尾窗成交先被对账按数量补一次、次日开盘又正常入账一次,账本多出一份整整一个交易日(`scheduler.py` 会话窗口与 `daily_settle` 的时间差)。休假模式下 14:50 的做 T 强制平回也被跳过,当日 T 仓会裸奔过夜,违反「绝不过夜」的铁律(`t0_close` 受执行暂停闸管辖。ws 进程启动自检失败时就绪标志已提前置真,「下轮重连会重试」的承诺不成立,进程会带着零水位上线,把真缺口误判成冷启动(`runner.py` 第 176 到 232 行)。重连退避的失败计数只在优雅退出时清零,健康跑几天后每次断线都固定等满三十秒(第 241 到 251 行)。心跳把正被并发修改的统计字典直接交给线程里的 JSON 序列化,高峰期可能炸掉当轮心跳(第 212 行)。
---
## 四、前端页面index.html
以下都已对照后端路由核实。
第一条,纯交易员每次登录、刷新、快捷下单成功后都会弹「无权限」错误。`loadAll`(第 2739 行)无条件调管理员专属的 `/api/ws-channel`,而 `call` 对 403 一律弹全局错误提示(第 2407 到 2409 行)。交易员刚点完清仓看到成功提示,紧跟一条无权限报错,极易误解为交易被拒;且通道告警对纯交易员永远不亮。
第二条,策略产生的指令会从「今日」面板凭空消失。「今日成交」「已经结束」按 `INS_` 加日期的前缀过滤(第 2181 行),但策略指令的前缀是 `STR``strategy_runner.py` 第 139 行)。策略单在途时可见,一旦成交或撤销就从三个页签和消息栏同时消失,「今日成交」计数永远不含策略成交。
第三条,快捷命令只看 ok 字段就报「已下达」(第 2281 行)。后端对「该产方案却一条没产」的命令返回 ok 为真、状态为已作废,交易员看到的是成功,实际命令已自我作废。运维视图的下达入口会显示状态和说明,交易员视图没有。
第四条,采纳提议成功时丢弃了后端刻意返回的警告字段(第 2819 行)。「一次性纪律计数器写入失败」的提示永远到不了用户眼前。
第五条,交易员视图里有两个按钮调的是管理员接口,点了必弹无权限:「我的策略」区的「试算一遍」(第 993 行,调 `/api/ops/strategy-attach-scan`)和「选股计划」抽屉的「强刷加灌行业映射」(第 1744 行,调 `/api/ops/plan-refresh`)。要么前端按角色隐藏,要么后端把这两条挪出管理员前缀。
第六条,候选「已下单」的判定用任意日期、任意状态的建仓指令(第 2201 行),几天前早已作废的指令会把今天真实在盯的候选从两张表里吞掉。
第七条,交易员视图的全部危险操作没有防重复点击的禁用态(第 2269 到 2283 行),双击能发出两条相同的资金命令,而同类命令后端不互斥,两条降仓百分之五等于降仓百分之十。
第八条,保存参数在传输层失败时仍弹「已保存」(第 2457 到 2460 行),随后刷新还会用服务器旧值覆盖用户刚改的输入。
第九条,登出或会话过期后,三十秒一轮的自动刷新照常运转,停在登录遮罩上的页面无限期打 401 请求(第 2976 行,登出不清定时器)。
第十条,「今天发生了什么」没有日期过滤(第 2484 行),当天记录少时昨天的行只显示时分,看起来就像今天发生的。
另有一条后端配套问题:`/health` 在免登录白名单里,返回体带总操作规模、各仓位上限、自主档位和数据库连接错误串,未认证即可读到真实资金体量。建议把这些字段挪进需登录的接口。
---
## 五、两个回测脚本backtest_churn.py 与 backtest_entry.py逐行审
这两个脚本整体结构清楚,只读不写表的承诺守住了,读数纪律(样本不足只报数)也贯彻了。以下是方法学层面的问题,会影响你拿它们做「要不要加护栏、要不要加入场过滤」的决策,按影响大小排。
第一条,入场过滤的三条规则都用到了买入时刻还不知道的信息。追高度用的是当日最高最低价(全天才知道),当日涨跌和放量阴线用的是收盘价和全天成交量。注释说「只用买入日及之前的信息,无未来函数」,按日粒度成立,按盘中粒度不成立:真实盘中过滤器拿不到当天收盘。⑤ 段的反事实结果会比实盘可实现的效果偏乐观。建议在读法里加一句说明,或把规则改成「昨日阴线放量」「开盘至买入时点涨幅」这类盘中可得的口径。
第二条,持有天数是自然日,输出文案却写「交易日」。配对时 `hold_days` 按日历日差算churn 第 237 行、entry 第 293 行,注释自己承认是近似),但 ① 段和护栏档位都印着「交易日」。护栏 N 等于 1 那档失真最大:周五买周一卖是一个交易日,却算三个自然日,被 N 等于 1 漏掉。判护栏档位前建议把它改成真交易日(`app/core/tradedays.py` 现成)。
第三条,无行情日的回退用了未来数据。`entry_context` 在买入日没有 K 线时用「其后第一根」近似entry 第 239 到 244 行),画像和过滤用到了买入之后那天的数据,和「无未来函数」的承诺相悖。路径罕见,建议直接跳过该样本而不是回退。
第四条,费用函数和「每边最低五元」从没被用到。两个脚本都定义了 `trade_cost`含最低佣金但全部计算只用比例费率小金额来回的摩擦被低估。churn ④ 段注释还承诺「有 hard_numbers.amount 时用真实额」,代码没实现;「累计摩擦约等于名义规模的百分之几」这个口径是各来回等名义的假设,建议在输出里说明白。
第五条churn 的护栏扫描里,卖价缺失的来回会被当成零收益样本混入。`_guard_sweep` 第 368 行 `(t["gross_ret"] or 0)` 把取不到收益的来回按零处理再参与差值,污染样本。建议 gross_ret 为空直接跳过。
另有一条小的churn 的 ④ 段循环累加的是同一个常量,写法绕了(第 339 到 342 行),结果没错但读着像要按笔算又没算。
---
## 六、测试套件的盲区
560 例的断言密度和边界覆盖高于一般水准,但有两类结构性盲区,其中第一类直接掩护了本报告第 1 条严重问题。
第一类,桩没跟着产品长。`test_wiring.py` 的 FakeRepo 落后真实 `pms_repo` 二十六个函数。缺 `active_strategy_codes` 导致「挂策略的票收到风控卖出只落提议不强清」这条 2026-08 加的保护全套件零覆盖,而且生产上这个查询失败也会同样静默降级成空集,直接强清用户挂着策略的票。缺列名核对导致第 1 条的 SMOKE 闸测试桩键名错误还全绿。建议给 repo 层加函数时同步检查假仓面,并给「桩键名对齐真表列名」补一条结构自检。
第二类,个别用例声称测了链路实际没测。`test_batch10` 第 622 行起的用例注释说「走 _route_one规则闸必须拿到真涨幅」实际只测了取数函数接线处历史 bug 恰恰在那)没碰。`test_wiring.py` 第 1720 行拿了桩没断言「关闭开关时不确认消息」;`test_batch7` 第 223 行的「超期抛错」用例在不抛错时静默通过try 后缺 else raise`test_batch17` 第 483 行的 `out2`、`test_batch11` 第 262 行的 `r3` 赋值后没断言;`test_batch2` 第 197 行「不超卖」断言全在循环里,空计划零断言通过;`test_batch6` 第 621 行的密钥自证拿实现自己的名单当预期,名单被删测试跟着消失。
顺带一条脚本侧的同类问题:`gen_keys.py --check` 和 `check_db.py` 的「密钥不落库自证」是永真检查,因为 `param_store.get` 对密钥键无条件返回空串,等于用门上的锁证明屋里没贼。要自证得直查参数表。
---
## 七、看过且认为干净的部分
以下文件逐行看过,除上文点名的条目外没有发现真缺陷:`plan_diff.py`、`rebuild_check.py`、`macro_rules.py`(区域迟滞、对数映射、标定口径实测一致)、`rule_gate.py`、`ws_codec.py`(签名验签、水位、防重放实测正确)、`tradedays.py`(调休周六正确排除)、`db/session.py`(单表守卫、事务收尾、无连接泄漏)、`industry_repo.py`、`macro_repo.py`、`param_store.py`、`portfolio.py`、`judge.py`、`upstream_signals.py`(严格只读)、`plan_feed.py` 的新鲜度链路、`qmt_repo.py`(水位防回退、先查后插去重、认领的条件更新都对)、`calibrate_macro_signal.py`(标定数学数值复核过)、`init_db.py`、`rebuild_ledger.py`、`reset_ledger.py` 的确认闸、`code_fingerprint.py`、`probe_bionic.py`、`probe_plan_api.py`。
时区专项查过三个容器统一上海时区表时间戳都由应用侧写入SQL 里没有数据库端的取时函数,「当日」口径一致。唯一例外是 `reset_ledger.py` 第 208 行用了数据库端 NOW()(库时钟是 UTC该行时间戳会倒退八小时只影响排障时的时间线阅读。
登录会话票本身的签发与校验可靠HMAC 覆盖完整、常量时间比较、过期有校验不可伪造。多角色集合判定逻辑正确。34 条路由逐条核对过,当前没有漏挂鉴权的运维写接口;但鉴权模型是「前缀黑名单之外全放行」,日后新增运维接口挂错前缀会静默对交易员开放,建议改成运维默认拒绝。另外会话 cookie 未设 secure当前裸 HTTP 部署下是既成事实)、登出无服务端失效手段、`/api/params` 对交易员放行的键里含下发通道切换和自主档位这类系统级开关,这三条属于要不要收紧的取舍,列出供拍板。
---
## 八、其余由分路审查报出、我未逐行复核的线索
以下条目方向可信但我没有逐行验证建议照单核对ws 通道 T_ACK 分支无终态保护重发场景下已成交委托可能被改回在途runner 第 561 行优雅停机的最后一次确认与关连接是死代码runner 第 840 行watch.py 对紧急单的「今日投余废」口径与执行器不一致会双扣watch.py 的交叉核对拿最近两百条未归档指令当全集会误报migrate_archived_at 对查列失败的表可能报全绿ws_smoke 的持仓快照读取失败会打出两句矛盾的诊断;策略侧买入暂停表同票单条先写先赢,风控来源的暂停可能被吸筹来源吞掉后又被自动解除;用户手动暂停的网格会被自动接力撤掉换挂止盈;网格卖出在发单时就弹档扣钱、未成交不回滚;主板零股尾巴全清后永久滞留;做 T 平回数量未整百取整会被拒单循环;派发定性的票仍会被挂上止盈。
---
## 九、验证命令
在你的开发机上,进入项目目录执行。每条命令后面写了预期读数。
验证第 1 条(联调单闸失效)的两个事实:
```bash
cd /Users/baobao/Documents/work/project/tradingSystem
grep -n 'parent_instruction_id' app/services/ledger_service.py
grep -n 'parent_id' ddl_pms_v1.sql | head -3
```
预期:第一条命令能看到第 127 行在读 `parent_instruction_id`;第二条命令能看到表列名是 `parent_id`。两个名字对不上,就是问题本身。
验证第 5 条(指令编号撞车的前提):
```bash
cd /Users/baobao/Documents/work/project/tradingSystem
grep -n 'seq = 0' app/services/executor.py
grep -n 'instruction_id.*UNIQUE\|uk_ins' ddl_pms_v1.sql | head -3
```
预期:能看到 executor 里序号从零起数,且指令编号在表上有唯一键。
验证第 11 条resync 标记被覆盖):
```bash
cd /Users/baobao/Documents/work/project/tradingSystem
grep -n 'set_conn' app/ws/runner.py | head -5
```
预期:能看到握手路径上那次调用把 resync 变量原样传入,正常重连时它是否值,于是写回零。
复现整套单测通过(说明上述问题都在单测覆盖之外,修复时每条都该补用例)。这条在部署服务器 tlai4090 上执行:
```bash
make test
```
预期读数ALL SUITES PASS。
---
## 十、修复建议的先后次序
以下只是建议,改不改、怎么改由你拍板。
第一批(真金白银直接相关,建议最先修):第 1 条联调单闸、第 3 条入账幂等、第 4 条撤销不撤下游、第 11 条 resync 标记、第 14 条止盈级联、第 13 条网格上半区买入。
第二批(会造成假状态和错误决策):第 2 条回放游标、第 6 条进度回写、第 7 条盘前清仓判完成、第 10 条除权、第 5 条指令编号、第 12 条科创板手数、第 8 条建仓超配、第 9 条信号试算。
第三批(权限与页面):第 15 条留痕冒名、第 16 条环境模板、前端第一二三条。
第四批:其余中低项与测试补桩。
修复时的两条纪律,来自这次审查本身的教训:每修一条,先补一个用真实列名、真实前缀、真实时序的用例,让它先红后绿;桩层的键名与真表列名做一次全面对齐。