tradingSystem/docs/评审记录_2026-09-17.md

154 lines
20 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-09-17选股计划排序轴换质地提交 4d7ca55
> 评审对象:选股系统仓库 `main` 上的提交 4d7ca55开发者 2026-09-17 上午交付,交付时未推送)。基线 6138c01。
> 评审依据:`docs/选股计划调整方案_2026-09-17.md` 第十节"评审方式"。评审者只看证据。
> 写法遵守 `.claude/output-styles/readable-chinese.md`。本文件是四个仓库逐字同步的副本之一。
---
## 一、结论
**有条件通过。** 代码主体与规格书一致五个测试文件本地全绿四个开关设旧值时逐字回旧成立分数编码与三门槛一确认三风险都没有动。三处必改合成一个修复提交09-21 拉代码之前做完并复审。两条建议级不阻塞。一条拍板项。
三处必改各一句话。
1. 装配路与快照路对"已启动成员"的主题限额判定不一致。同一天的计划,早上落盘的主榜前二十与接口读快照给出的前二十可能不是同一批。
2. 计划里的 `encoding` 说明句在旧轴下也写"名次按质地带优先",与旧轴的实际次序相反。
3. 交接文档一段没写,规格书工作包五第三条没有完成。
修复小包的规格写在《上线与协同阶段工作方案与规格书_2026-09-17》工作包甲。本记录只写核对结果与理由。
---
## 二、我亲手跑出的证据
**本地单测(开发机,选股系统仓库)。** 五个文件末行全部 ALL OK`test_plan_rank_axis.py`、`test_plan_quality_rank.py`、`test_card.py`、`test_pool_logic.py`、`test_plan_snapshot.py`。四个开关设旧值再跑 `test_plan_rank_axis.py`,仍 ALL OK。七个改动的模块 `py_compile` 通过。开发者说只能在部署机跑的五个测试文件,我本机同样跑不起来,原因与他说的一致:缺 `psycopg``fastapi`,或本机 Python 3.9 不支持联合类型注解。这是既有状况,不是本次引入。
一条要说明:`test_plan_snapshot.py` 里"拿真实快照逐行比对"那一例在本机是**跳过**的,因为本机没有快照文件。它只有在部署机上跑才真正生效。必改一正是它会查出的那件事。
**旧路径等价(开发机)。** 入池的 `tier``candidate` 两个分支从内联代码抽成了纯函数 `select_plan_rows`。我用随机合成的主榜行做了三百轮对照,每轮六种组合(两个来源乘三种档位白名单),新函数与旧内联逻辑的输出零差异。
**部署机只读读数选股系统机器09-16 快照)。** 仓库停在 3d3f317与规格书基线一致。`.env` 里有 `POOL_TOP=50`、`POOL_MAX=100`、`CARD_REQUIRE_STARTED=0`,没有 `POOL_SOURCE`、`PLAN_RANK_AXIS`、`POOL_THEME_CAP`,所以拉代码后默认值直接生效,与规格书第五节说的一致。
09-16 快照主榜全量五百三十二行。质地分布:好十六、中四十八、差一百三十四、无评析三百三十四。我按规格书的词典序在这份快照上模拟了一遍质地序,前二十里好十六、中四。前二十里有六行没有传导主题,正好落在"第六只起被限额顶掉"的位置,规划代理那条发现在真实数据上成立。前二十里"储能"主题五只,正好碰到限额边界。
"已启动成员"的行(证据由已动成员视图补上的行)全量八行,旧序名次八十二、三百五十三、三百九十、四百九十一、五百、五百二十、五百二十六、五百三十。旧序前二十里没有它们;模拟质地序前二十里有一行。这就是必改一在真实数据上的样子。
---
## 三、逐项核对
按规格书第十节的核对步骤逐条记。
1. **diff 对照附录己的文件清单。** 十五个文件改动,全在清单内。`factors.py` 与 `api.py` 没有出现在 `git show --stat` 里。清单里的"交接文档"没有改,见必改三。
2. **本地跑测试文件。** 见第二节。
3. **四个开关设旧值时的逐字回旧断言是不是真断言。** 是,但覆盖方式要说清。第一例只断言 `_rerank` 在旧轴下与 09-14 的 `_rerank_by_quality` 序列与分值逐字相同,它验的是分派。另外三个开关各有自己的旧路径断言:入池旧路径由我的三百轮对照补上,评析名单的 `score` 分支是第七例,卡内序关掉是第八例,主题限额例外关掉是第九例。四条合起来才是"四开关逐字回旧"。
4. **主题限额的例外只在质地轴下生效。** 是。装配时按 `config.PLAN_RANK_AXIS` 传入,快照路按行上的 `rank_axis` 推断。回退那天读旧快照仍按质地轴截取,这是对的,因为那份快照的序就是质地序。但两条路对"已启动成员"的判定不一致,见必改一。
5. **卡内序、入池、评析名单三处的次级键与去重。** 卡内序在判决与涨幅之间插入负的质地带,关掉时元组与旧实现逐字相同。入池 `rank` 源只取被指向的行、按新序截前五十,备用开关并进旧序强传导前五十、去重、受 `POOL_MAX` 兜底。评析名单两路并集去重后按新序名次排,`score` 分支按原始分。三处都有确定的末位键。
6. **`_row` 只加键。** 是。加了 `quality_band`、`rank_axis`、`pointed` 三键,质地轴下再加 `rank_old`。`score`、`score_adj`、`tier` 的值与含义没有变。顶层 `encoding` 的值变了,见必改二。
7. **分数编码。** `factors.py` 一字未动。组内分从原始分反解,档位用整除,与编码一致,我逐个区间验过。
8. **旧序名次。** `rank_old` 取自全量序,不是截断后的序。主榜与观察档两个序列的代码不重叠,两张名次表合并不会互相覆盖。
9. **一百二十天守卫。** 守卫是活的:评析摘要里的 `age_days` 由数据日减报告运行日算出,取值真实。守卫对质地带、质地门、卡内序三处生效。它**不对**旧轴的加减八分生效,`score_adj` 也不受守卫。这与规格书第五节表里那一行的写法不一致,但代码是对的,因为规格书交付要求第四条要求 `score_adj` 的值不变。规格书这一行我已改正,见第五节建议三。
10. **入池不变式的验证命令是否成立。** 成立。入池走 `plan.collect` 要五百行的宽池、主题限额取 `POOL_THEME_CAP`(默认零),再交给纯函数选行;规格书第六节那条命令拿快照全量行喂同一个纯函数,两边看到的是同一个序。
11. **评析目标名单拿到的是全量行。** 是。`persist` 收到的是落盘快照,`main` 是全量新序行,每行带原始分与新序名次。
12. **周报的底本。** 周报不读快照,它用 `plan.collect` 把当日重新装配一遍再取全量行。两份对照名单都从这份重算取。这是既有做法,不是本次引入,但它决定了四周复核读到的是什么,见第六节拍板项。
---
## 四、必改
### 必改一:装配路与快照路对"已启动成员"的主题限额判定不一致
**是什么。** 主榜截取有两条路。装配路是 `collect` 里的内部函数 `_pick`,早上出计划时用它截主榜前二十,写进 md 与快照的 `main_shown`。快照路是模块级函数 `pick_rows`,接口读快照时用它按请求参数截取,持仓管理系统要的三百只前缀从这里来。两条路本该同一套规则,`test_plan_snapshot.py` 拿真实快照逐行比对钉住这件事。
这次的例外判定在两条路上写法不同。装配路看的是传导视图有没有这只票的证据行:没有证据行就当无主题、免限额。快照路看的是行上的 `evidence.theme`:没有主题才免限额。差别出在"已启动成员"上。已启动成员在传导视图里没有证据行,但装配时 `_row` 会用已动成员视图的目标环节把 `evidence.theme` 补上(`plan.py` 约 550 行)。于是同一只已启动成员,装配路把它当无主题免限额、不计数,快照路把它算进所在主题、受限额。
**为什么以前没炸。** 旧序下这些行排在八十名以后,碰不到前二十的限额。旧代码里两条路对它们其实也不一致(装配路把它们归进"无传导"桶计数快照路归进所在主题只是不影响前二十。质地序把被指向的已启动成员提到了前列09-16 快照模拟出来前二十里有一行,"储能"又正好五只碰到边界,差一只就会让两条路给出不同的第二十名。
**后果。** 同一天md 与快照里的 `main_shown` 是一份前二十,接口给持仓管理系统的是另一份。规格书第二节"同日重跑结果一致"这条原则在两条路之间失守。部署机上跑 `test_plan_snapshot.py` 会在第一个出事的交易日变红。
**修法。** 不在两处各打补丁,改成一处实现两处调用:把"按主题限额截取"抽成模块级纯函数,装配路与快照路都调它,主题的取法与 `_row``evidence` 的取法同源(含已动成员视图的补充)。这样装配路对已启动成员的判定也变成"算进所在主题",与快照路一致。这一步同时改正了旧轴下的既有分歧,改的只是 md 与 `main_shown` 这条路对已启动成员的计数,接口路(持仓管理系统读的那条)原样不动。逐字回旧那一例单测不受影响。细节与单测在工作方案附录甲。
### 必改二:`encoding` 句在旧轴下写错了
`collect` 返回的 `encoding` 一句无条件追加了"名次按质地带优先,传导只在带内作次级键(开关 PLAN_RANK_AXIS"。旧轴下名次不按质地带优先,这句话与实际相反。持仓管理系统把这句原样收进它的状态页给人看。修法是只在质地轴下追加,或把句子改成条件式写法。一行改动。
### 必改三:交接文档一段没写
规格书工作包五第三条要求交接文档一段并四仓库同步,提交里没有。修法是在最新一份交接文档 `docs/交接_2026-09-10.md` 末尾加一节带日期的补记,把台账那一行从"最新到第 057 条"改成 063四仓库同步。交接段的骨架在工作方案附录甲。
---
## 五、建议级(不阻塞)
1. **评析名单 `score` 分支不是逐字回旧。** 旧代码取快照序前一百,快照序是 09-14 的调整分序;新代码按原始分排前一百。差别只在第一百名附近被加减八分挪动的几只。评析名单只管覆盖,这点差别没有后果。不改,记在这里。
2. **两处小瑕疵。** `_rerank_by_axis``axis` 参数没有用到;`card.sort_key` 在函数体内导入 `config`。都不影响行为。
3. **规格书两处文字改正。** 第五节表里 `COMPANY_REVIEW_STALE_DAYS` 那一行说守卫对加减分也生效,工作包二第二条说"也不再加减分",与代码和交付要求第四条矛盾,按代码改正。附录乙说周报"从快照 `_full` 读主榜行",实际是现算后取 `_full`,按实际改正。这两处我已经改在规格书里,随本记录同步。
4. **周报里"主榜新序前二十"对改动前的日子。** 09-25 周报的窗口含 09-18 与 09-21 两个旧序日,那两天这一行的名单其实就是旧序前二十,标签会略有误导。旧序名单那一行对这两天正确地跳过了。周报一段注记即可,不改代码。
---
## 六、拍板项:周报的底本
周报用 `plan.collect` 把每个计划日重新装配一遍,而不是读当天落盘的快照。重新装配用的是跑周报那一刻的评析报告:查询取每只票最新一次运行,不按日期截断。于是 09-22 的"新序前二十"在 09-25 算出来时,用的是 09-25 手里的质地档。一家 09-24 才有报告的公司,会被回头算进 09-22 的前列。
这在质地是次级项的时候是小事,在质地成为主轴之后不是。四周复核比的是"新序前二十与旧序前二十谁更好",两份名单都应该是当天真正出过的那份。快照落盘时的注释写着"它是复盘与对账的唯一底本",周报却没有读它。
我的建议是周报改成先读当日快照的全量行,没有快照的日子才回落到重新装配,加一个开关默认读快照。改动小,但它改变周报所有既有名单的取数方式,从 09-02 有快照那天起的读数会与旧周报的口径不同所以要用户拍板。写在工作方案工作包丙与拍板点一。2026-09-17 用户拍板:做,随修复小包同一个提交。
---
## 七、答开发者的两个问题
**要不要现在推送。** 复审已经做完,不必等 09-19。提交 4d7ca55 我随本记录一起推到远端,修复小包在它上面再提一个。选股系统机器只在 09-21 收盘后由人拉代码,中间远端有一个待修的提交没有风险。
**要不要现在补交接段。** 要,放进修复小包,与必改一、必改二同一个提交。
---
## 八、附:文件清单对照
提交改了十五个文件:`plan.py`、`card.py`、`pool.py`、`review_targets.py`、`plan_review.py`、`plan_reconcile.py`、`config.py`、`.env.example`、`README.md`、`docs/复盘决定台账.md`、`docs/选股计划入池_对接说明.md`、`docs/选股说明_下游对接.md`、新 `test_plan_rank_axis.py`、`test_plan_quality_rank.py`、`test_plan_snapshot.py`。附录己清单里只差交接文档。清单外没有文件被动。
---
## 九、修复小包复审补记2026-09-17 晚,评审者):通过
**对象。** 提交 e7cf0a9选股系统 `main`),工作包甲三处必改与工作包丙合成一个提交。开发者交付时未推送,复审通过后由我推送。
**证据。** 本地七个测试文件末行全部 ALL OK`test_plan_rank_axis.py`(十二例)、`test_plan_snapshot.py`、`test_plan_quality_rank.py`、新 `test_plan_review_source.py`、`test_card.py`、`test_pool_logic.py`、`test_plan_review_heads.py`。四个开关设旧值再跑 `test_plan_rank_axis.py` 仍 ALL OK`PLAN_REVIEW_SOURCE=live` 再跑 `test_plan_review_source.py` 仍 ALL OK三个改动模块 `py_compile` 通过。
**逐项核对(按工作方案第十节)。**
1. 截取规则只剩一处实现。模块级 `take_by_theme` 是唯一的循环,`collect` 里的 `_pick` 与模块级 `pick_rows` 都只剩一行调用。
2. 两条路的主题取法同源。装配路的 `_theme_of``_row``evidence` 的三行逐字对得上:传导视图有证据行取它,否则已启动成员用已动成员视图补的主题,否则空。快照路取行上的 `evidence.theme`,空串与缺失都当无主题,与装配路一致。
3. 旧轴下无主题行仍归"无传导"桶计数。`uncapped_no_theme` 为假时桶名取"(无传导)",第十二例钉住六条只放五条。
4. `encoding` 句两轴都对,改成按开关分别写清。
5. 交接段已写进 `docs/交接_2026-09-10.md` 第十一节,台账行改成 063。提交信息说"四仓库同步",但开发者只在选股系统仓库提交,另外三份副本由我随本补记同步。
6. 工作包丙:无快照日回落到现算并注记"底本:现算",开关设 live 时有快照也现算,单测三条都钉住。`run` 从计划字典里只取 `_full`、`candidates`、`watch` 三个键,快照整理出的字典正好齐这三个,没有漏掉的键。
**建议级,不阻塞。** 一、`_row` 里那三行可以改成直接调 `_theme_of`,让取法只剩一份文字,眼下是两份逐字相同的文字。二、第十一例在测试里抄了一遍 `_theme_of` 的逻辑而不是调真函数,`_theme_of` 若改成模块级纯函数,测试就能调真的。都留到下次顺手改。
**下一步。** e7cf0a9 已推送09-21 收盘后按计划拉。开发者转做工作包乙规格在工作方案附录乙09-19 交付。09-22 早上按工作方案第六节核"两条路同一份前二十"。
---
## 十、工作包乙复审2026-09-17 下午,评审者):持仓管理系统通过,选股系统小改退回一处
**对象。** 持仓管理系统仓库 f5cc1b8台账 016与选股系统仓库 4ae6378都在各自 `main`、交付时未推送。
**持仓管理系统 f5cc1b8通过。** 开发者本机跑不了它的单测(缺 pydantic 等依赖,本机 Python 3.9,无 docker只做了编译与逐行对照如实说了。我把这个提交用 `git archive` 打包、拷到选股系统机器的临时目录,用 `pms:latest` 镜像起一个一次性容器把代码目录挂进去跑 `scripts/run_tests.py`,末行 ALL SUITES PASS`test_batch29_units.py` 三例新用例全部 ok跑完删掉临时目录与包。运行中的容器一个没碰。这是今后复审持仓管理系统代码的办法写进工作方案第六节。
逐项核对(按工作方案第十节四件事):自门控只看计划顶层的 `rank_axis`,由 `_rank_mode` 一处判定,候选选择与名册元数据都用它;上游序下排序键只有名次与原始分,立场桶不参与;上游没带 `rank_axis` 时两个既有分支逐字不动,第二例钉住;名册元数据写了 `rank_mode`,在拉计划处按参数算好传入,与候选选择同一份参数同一份计划。四个新键归一、透传,顶层 `rank_axis`、`rank_rule` 带进来,参数登记默认开,接入说明补了前缀效应,台账 016 五段齐。
一处要改口的是台账 016 与提交信息里的"重建镜像加 force-recreate"。选股系统机器上持仓管理系统的源码是挂载进容器的(`docker-compose.override.yml` 把仓库目录挂到 `/app`,我只读核过),依赖没改,上线只需重启进程:`docker compose --profile sched --profile ws restart -t 30`。重建镜像会覆盖镜像标签、丢容器里的定时任务表、再跑一遍建表脚本,没必要。台账那句留着不改,以工作方案第六节为准。
**选股系统 4ae6378退回一处。** `pms_roster` 的上游序分支去掉了"档位强传导"过滤,提交信息说是"与持仓管理系统的上游分支同口径"。这不对。持仓管理系统的档位白名单(`select_candidates` 里的 `tiers` 门)与排序模式无关,任何模式下都先过它再截断。选股系统机器上 `pms_runtime_param` 表里没有 `PMS_PLAN_TIERS` 这一行,`param_store.get` 退回 `config/settings.py` 的初值"强传导",所以持仓管理系统在上游序下仍然只取强传导行。周报按 4ae6378 还原出来的"生产名单"会多出弱传导与无传导的行,与它真正交付的那份对不上,四周复核那一行读数就失真了。
修法(工作方案附录乙补了一段):`pms_roster` 从 `pms_runtime_param``PMS_PLAN_TIERS`(缺行退回"强传导",与持仓管理系统的初值一致;读到空串按不过滤),两种模式都先按它过滤,再按 `rank_mode` 定序,上游序按名次升序、旧序按原始分降序。把"过滤加定序"抽成纯函数加一例单测不连库。一个小提交09-21 拉代码之前做完。
**顺带核出的三件事实,写下来免得再猜。** 一,持仓管理系统机器上 `PMS_PLAN_TOP_N` 是五十,不是文档里多处写的三十,与入池的 `POOL_TOP=50` 正好对上。二,名册快照按代码序落库,不是名次序,所以"名册前十名次递增"这样的读数无意义09-24 的读数改成在一次性容器里调候选选择函数看输出,命令在工作方案第六节。三,最近三天名册主榜只有六十三、九十九、九十八行,不是三百。原因查清了:持仓管理系统的运行参数 `PMS_PLAN_THEME_CAP` 在 08-13 被设成零,零的含义是不向上游传主题限额、用上游默认值,上游默认每主题五只,所以它拿到的是每主题五只后的六十到一百行。这是既有设置,不是本次的问题。但 09-22 起会变:质地轴下无传导行不受上游限额,它会拿到接近三百行,多出来的绝大多数是无传导、判仅展示的行,被它自己的按判决分流剔掉,候选只数不受影响,页面上的"考虑只数"会跳一次,写进工作方案第十一节。
**一条要拍板(工作方案拍板点六)。** 持仓管理系统的档位白名单"强传导"是一道硬闸:上游序下它拿到的前五十是"强传导行里的质地序",被指向但只算弱传导的好公司永远进不来。这与"传导降为雷达门、被指向即可"不一致,也与入池(被指向即可,不看档位)不同源。建议 09-23 随工作包乙上线时把运行参数 `PMS_PLAN_TIERS` 改成"强传导,弱传导",页面改、不用代码,台账补一行。不改的话四周复核要说明这一处差异。