2026-09-17 11:24:02 +08:00
|
|
|
|
# 评审记录(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 的前列。
|
|
|
|
|
|
|
|
|
|
|
|
这在质地是次级项的时候是小事,在质地成为主轴之后不是。四周复核比的是"新序前二十与旧序前二十谁更好",两份名单都应该是当天真正出过的那份。快照落盘时的注释写着"它是复盘与对账的唯一底本",周报却没有读它。
|
|
|
|
|
|
|
2026-09-17 11:31:16 +08:00
|
|
|
|
我的建议是周报改成先读当日快照的全量行,没有快照的日子才回落到重新装配,加一个开关默认读快照。改动小,但它改变周报所有既有名单的取数方式,从 09-02 有快照那天起的读数会与旧周报的口径不同,所以要用户拍板。写在工作方案工作包丙与拍板点一。2026-09-17 用户拍板:做,随修复小包同一个提交。
|
2026-09-17 11:24:02 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 七、答开发者的两个问题
|
|
|
|
|
|
|
|
|
|
|
|
**要不要现在推送。** 复审已经做完,不必等 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 11:58:56 +08:00
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 九、修复小包复审补记(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 13:28:37 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
## 十、工作包乙复审(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` 改成"强传导,弱传导",页面改、不用代码,台账补一行。不改的话四周复核要说明这一处差异。
|
|
|
|
|
|
|