diff --git a/AGENTS.md b/AGENTS.md index a80c47d..572f32c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -66,15 +66,15 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri | 项 | 值 | |---|---| -| `main` | 下表全部 gate 实测于 `4e73ac1`(PR #116,D5 C6 收口) | -| `pytest -p no:warnings` | **2669 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | +| `main` | 下表全部 gate 实测于 `8aa11c1`(PR #119) | +| `pytest -p no:warnings` | **2705 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | | `ruff check .` | clean | | phase0 锚(`run-phase0 --config config/example.yaml`) | **ic 0.9600 / annual 0.8408**(自 P0 起从未变过,任何改动都不许动它) | | `qt.exec_baseline_freeze --verify` | **77/77 @ `45c14aa`** | | `validate-config` | 32/32 | | 唯一 OPEN 的 PR | #38(历史遗留,保持 OPEN) | -**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**下一步 = Step 5 / D6a–d 存量收口**(`aggregate → factors.minute` 的 shim 日落、`asof_daily_features` 的 `mmp_ew` hook 需要新家),然后 D7 收官全量重评估(**exec-only**)。 +**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**D6a 已收口(#118)**:phase0/phase2 切 FactorService,真实 phase2 逐格 `max_abs_diff=0.0`、锚未动(R10:新锚 D6d 才转正);顺带堵死一个**已经在发生**的红线 #6 违反(demo 值写进共享 store)。**下一步 = D6b**(oos/subset/robustness 切服务,**必须先抓 legacy 再切**——归档数字已不复现,见 D6a-5),然后 D6c / D6d / D7 收官全量重评估(**exec-only**)。 > **接手必读(唯一入口)**:[`tmp/design/HANDOFF_2026-07-30_claude_code.md`](tmp/design/HANDOFF_2026-07-30_claude_code.md) > ——含当前截面、F1–F5 裁定速查表、六步路线、派发/评审/限额的具体操作法、陷阱清单。 @@ -146,6 +146,6 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri ## 路线图 -1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 改造 (#115) + C6 删除 (#116)~~ → **D6a–d 存量收口(下一步)** → D7 收官全量重评估(**exec-only**)。 +1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 (#115+#116)~~ → ~~D6a (#118)~~ → **D6b(下一步)** → D6c → D6d → D7 收官全量重评估(**exec-only**)。 2. **因子研究**:十一因子无一到 Adopt ⇒ 下一步优先做**换手/成本敏感的组合层验证**与更长 holdout,而不是继续堆新因子;补 PR #79 的 review;修订 HTML compendium。 3. **可选、须单独显式 goal**:I5g(强制 partial-fill / volume-cap);数据层 D6(`PanelStore` append/partition,仅当因子研究需要可复用派生面板时才启动);schema 守卫接 live `data-update`。 diff --git a/CLAUDE.md b/CLAUDE.md index a80c47d..572f32c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,15 +66,15 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri | 项 | 值 | |---|---| -| `main` | 下表全部 gate 实测于 `4e73ac1`(PR #116,D5 C6 收口) | -| `pytest -p no:warnings` | **2669 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | +| `main` | 下表全部 gate 实测于 `8aa11c1`(PR #119) | +| `pytest -p no:warnings` | **2705 passed + 1 skipped**(⚠️ 不要再传 `-q`:`pyproject.toml` 已含 `addopts="-q"`,叠成 `-qq` 会吞掉摘要行) | | `ruff check .` | clean | | phase0 锚(`run-phase0 --config config/example.yaml`) | **ic 0.9600 / annual 0.8408**(自 P0 起从未变过,任何改动都不许动它) | | `qt.exec_baseline_freeze --verify` | **77/77 @ `45c14aa`** | | `validate-config` | 32/32 | | 唯一 OPEN 的 PR | #38(历史遗留,保持 OPEN) | -**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**下一步 = Step 5 / D6a–d 存量收口**(`aggregate → factors.minute` 的 shim 日落、`asof_daily_features` 的 `mmp_ew` hook 需要新家),然后 D7 收官全量重评估(**exec-only**)。 +**当前主线 = 因子层深度重构(设计 v3.2,D0–D7)。D5 已全部收口**:C5 四腿对账 33 格全绿(#113),**C6 完成(#115 改造 + #116 删除)——11 个旧 runner 归零,决策③「杀第二条因子取数路径」收尾**,三个重生成工具退役为 verify-only、D1 基线 frozen-forever、校验已接进 C5 harness 的三个读入点。**D6a 已收口(#118)**:phase0/phase2 切 FactorService,真实 phase2 逐格 `max_abs_diff=0.0`、锚未动(R10:新锚 D6d 才转正);顺带堵死一个**已经在发生**的红线 #6 违反(demo 值写进共享 store)。**下一步 = D6b**(oos/subset/robustness 切服务,**必须先抓 legacy 再切**——归档数字已不复现,见 D6a-5),然后 D6c / D6d / D7 收官全量重评估(**exec-only**)。 > **接手必读(唯一入口)**:[`tmp/design/HANDOFF_2026-07-30_claude_code.md`](tmp/design/HANDOFF_2026-07-30_claude_code.md) > ——含当前截面、F1–F5 裁定速查表、六步路线、派发/评审/限额的具体操作法、陷阱清单。 @@ -146,6 +146,6 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri ## 路线图 -1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 改造 (#115) + C6 删除 (#116)~~ → **D6a–d 存量收口(下一步)** → D7 收官全量重评估(**exec-only**)。 +1. **因子层重构收尾(当前主线)**:~~F1 (#112)~~ → ~~C5 audit (#113)~~ → ~~C5 进度文档 (#114)~~ → ~~C6 (#115+#116)~~ → ~~D6a (#118)~~ → **D6b(下一步)** → D6c → D6d → D7 收官全量重评估(**exec-only**)。 2. **因子研究**:十一因子无一到 Adopt ⇒ 下一步优先做**换手/成本敏感的组合层验证**与更长 holdout,而不是继续堆新因子;补 PR #79 的 review;修订 HTML compendium。 3. **可选、须单独显式 goal**:I5g(强制 partial-fill / volume-cap);数据层 D6(`PanelStore` append/partition,仅当因子研究需要可复用派生面板时才启动);schema 守卫接 live `data-update`。 diff --git a/docs/progress/07_factor_layer_refactor.md b/docs/progress/07_factor_layer_refactor.md index f6a2894..e898886 100644 --- a/docs/progress/07_factor_layer_refactor.md +++ b/docs/progress/07_factor_layer_refactor.md @@ -59,3 +59,16 @@ - **方法论新增(本轮换来的)**:① **一个失败的 mutation 逼出了更好的设计**——首版针对接线的 mutation 没牙(只钉"路由"没钉"接线"),正是它促成单一网关重构;② **"把自己的守卫说弱"比"说强"更难被自己发现**——说强会被 mutation 打红,**说弱不会;没有任何 mutation 会因为文档太谦虚而失败** ⇒ 只能靠**拿措辞去对实测行为**,且必须由**别人**做;③ **控制组必须先 ACCEPTED**,否则后面每个 REFUSED 都是空的(与"mutation 必须先断言落地"是同一道理的两面)。 - ⚠️ **同一个协议洞复发三次**(评审两次发现**树在它测量期间被改动**:一次因 lead 收到主报告就派返工而评审仍有追加项、一次因实现方声明停手后自审出真缺陷**直接改了**)。**两次都是"与预期矛盾的观察"抓住的,不是测试。** 三方各补一条才闭合:**lead** 等全部条目交付且明确收工才派返工;**评审**每份报告显式声明是否收工;**实现方**声明停手后非经指派不得恢复工作——**哪怕自己发现真缺陷,也先报告再动手**。**两次污染都被逐条厘清射程而非笼统作废,无一轮重来**,靠两条定界规矩:**刻度是「哪次测量读了哪份文本」不是 commit 时间戳**(工作树**先于** commit 改变,实证一次探针跑在 commit 前 90 秒却已读到新文本);**「没动可执行代码」要用剥掉 docstring 后的 `ast.dump` 精确比来量**。 - gates(lead 独立重跑):pytest **2669 passed + 1 skipped** / ruff clean / phase0 锚 **0.9600·0.8408** / **32/32** configs / `exec_baseline --verify` **77/77 @ 45c14aa** / 三个重生成入口 **rc=1**(loud)/ 两个 `--verify` OK / 旧 CLI 名 **rc=1** + 迁移提示、新名 rc=0 / `git add -A --dry-run` 无可暂存项 / secret **0** / 冻结基线**自 2026-07-25 起 0 写入**。实现方 mutation **31/31 有牙**,评审两轮各自独立写 mutation 复现关键项。 +- ✅ **D6a(PR #118)+ 守卫射程修复(PR #119)—— phase0/phase2 切 FactorService**:D6 四子步的第一步(R17 拆分)。按 **R10 锚时间线**,旧锚 `ic 0.9600 / annual 0.8408` **本步不许动**,实测未动(新锚 D6d 才转正)。 + - **这一步的全部价值是「一次可证明为 no-op 的路径切换」** ⇒ 凡会让"某个数动了"变成多选题的改动**一律登记而不夹带**:`isinstance(FinancialFactor/ValueFactor)` 分派(D6a-2,红线 #5;`registry.requirements()` 零生产调用者)、三个 runner 漏传 `cache`(D6a-3)。**真实 phase2 逐格对账**(`phase2_real_baseline_cached.yaml`,cache-only):**16,388 cells / `max_abs_diff=0.0` / NaN mask 失配 0 / 索引集合差 0**,headline **全精度逐位相同**(21 个叶只 `elapsed_seconds` 不同)。⚠️ **顺序承重**:legacy 捕获跑在**未修改的 main** 上、切换后在冻结树重跑——切完就没有 legacy 可比(D5a 抓到过的形态)。⚠️ **诚实标注没覆盖到什么**:SSE50 面板恰好稠密(68×241=16,388)⇒ **这次真实跑一行都没走 reindex 路径**,覆盖它的是单测。**「类为空是测出来的」与「类不存在」是两回事。** + - **B1(CRITICAL,勘察期发现、已经在发生)**:`StoreKey` 无数据源维,而 `config/example.yaml` 的 demo universe 用**真实 A 股代码 + 真实日期** ⇒ **每跑一次 `pytest`(`test_provisional_anchor_d2` 调 `run_phase0`),DemoFeed 编造的 `momentum_20` 就写进共享 store**;而 **D6b 的 oos/subset 正是 CSI300 2024–2026 + momentum_20**,重叠 (date,symbol) 上会**命中 store、零重算、零报错地拿到编造值**(红线 #6)。**修法不是加固 key,是拒绝存储**:demo 价格由 `_close_path` 的 `arange(n)` 从 `data.start` 起算 ⇒ 同一 (date,symbol) 的值**随 `data.start` 而变**(实测 210.0/187.0/166.0)而 key 看不见它 ⇒ **一个身份无法被 key 捕获的值,按定义就不可存储**。`qt/factor_source.py`:合成源 ⇒ **ephemeral store**;持久 store 配合成源 ⇒ **构造期 raise**;**未分类的新 source 也 raise**。策略与切换**同一 commit**,无"先切后隔离"窗口。**三方独立证据**:①实现方与评审**各自算出同一 key** `momentum_20/7816ca515a5b9a1a__ca4d351df0bf40f6__close.parquet`;②评审从 PIT `index_weight` 实测 **2024 CSI300 与 demo 五票交集 = {000001.SZ, 000002.SZ}**(今天没撞上只因 SSE50/CSI500 交集为空);③**最硬**——真实 phase2 已把该 key 写到盘上(11:33、68 只全 .SH、零 demo 票),随后跑 demo 派生**同一 key** 而该文件 **mtime 纹丝未动**(lead 合并后又复现一次)。**(b) 给 key 加 provenance 维**登记 D6a-1(它多买到的是**真实源之间**的 vintage 漂移,属 **D3 既有性质**)。 + - **B2** store 根从 `output.root_dir` 派生、不新增旋钮(此前 16 个测试重定向 `output.*` 而 store 根不跟着走,**本身就是测试隔离缺陷**)。**B3** 服务返回整格 ⇒ 多出全 NaN footprint 行会动已发布诊断 `coverage` 的分母 ⇒ 按**红线 #8**(重构改实现不改定义)reindex 复原旧口径 + **丢掉的行数记进日志**(复原可以、隐形不行);面板有而服务没返回的 cell ⇒ **`ValueError`**(红线 #9)。 + - **LOW-2(评审)**:**D6a 是第一次把日频因子放进共享 store,而 key 没有 universe 维**;今天安全**只因为**每个日频因子都是 per-symbol,而**这条事实此前无测试无登记**(**D5b 那个 CRITICAL 的形态**;minute 面由 D4c 修掉,**日频面没继承**)。新增**行为式**守卫(5 票 vs 3 票子集,共有票逐位相同)。**为什么不做源码扫描是测出来的**:截面去均值写成 `x.unstack().sub(...).stack()` **不含 `.rank(` 也不含 `groupby(level="date")`**,扫描 token 数 **0** 而 census **变红**。红了是 **STOP-AND-REPORT**(修法是 D4c 的),**不是加豁免名单**。登记 D6a-0。 + - **LOW-3(评审)**:守卫的 `UNMIGRATED_RUNNERS` 是**手敲清单**。**MS4 是决定性证据**:第三个未登记调用者 + 改签名 + 两个已登记 runner 与可见测试**全部尽职更新** ⇒ **21 passed 全绿、完全静默**。改成**全仓派生**(`git ls-files` ∪ untracked-not-ignored;认直接/属性/别名调用;`getattr` 响亮拒绝;**运行时拼名的盲区写进 docstring**)+ **三层反空过** + ⚠️ **「D6b 会让它变红并被删掉」写进 docstring**、并明写**不许在函数还有调用者时清空期望集来"修"它**。⚠️ **一处故意的诚实**:期望集含 `tests/test_factor_source.py`(它确实调用旧入口)——**派生就是派生,不能"除了不方便的那个"**;评审实测:删掉旧入口唯一的执行覆盖 → census **变红**,**若当初豁免掉,这个警报就是静默的**。 + - **S1(实现方自审,且比它上轮报的更糟)**:一条名为 `..._preserves_every_value_...` 的测试实测的是确定性。它逐行核后发现:稀疏测试只断言**索引**、断言**值**的那条跑在**稠密**面板(reindex 是恒等)⇒ **"索引对、行错"的 reduction 全仓无人看得见**。⇒ 改名 + 把缺失的值断言补到它该在的地方。**M16 三向**(按位置 reindex:稠密值测试绿 / 稀疏索引与计数绿 / **新增稀疏值断言红**;**旧套件 21 passed 完全失明**)把"新覆盖 vs 重复"**从主张变成测量**。**NIT-2** 评审提出并标注未验证,实现方**先验证再修**(长度不变、每 date 平移一天 ⇒ 该测试 PASS、另三条红 ⇒ 坐实)。 + - **一处实现方拒绝 lead 指令并给了测量,lead 与评审都判它对**:lead 要求「清单内文件改 `ast.Attribute` 必须仍然红」。旧守卫只认 `ast.Name` ⇒ Attribute 形态让它**找到 0 个调用、栽在自己的 `assert calls` 上**,报出 **"no longer calls _compute_factor_panel"——而这句是假的**。⇒ **保住旧的红,就是保住一个「对合法重构报错、且报错理由是假话」的假阳性。** 新守卫认识该形态所以**正确地绿**;MS2 真正代理的性质由 **M17 更强地钉住**(Attribute + 改签名 → 红 ⇒ **Attribute 调用是被 bind 的**)。评审复核后判实现方对,**并认领源头是自己上一轮把这个红写成"一条好性质"**。 + - **D6a-5(会影响后续所有基线)**:**归档 phase2 数字在今天的 main 上已不复现**(annual **−10.19% → −9.41%**、sharpe/turnover 亦动,**而因子 IC 逐位不变**)⇒ 动的不是行情/因子,指向 D6a-3 那三个**实时抓**的协变量端点。**拿档案数字当今天的基线是错的。** 并写明对账的未言明前提:**其有效性依赖协变量在两次捕获之间未被上游修订**(双向危险:既能凭空造差异,也能掩盖真差异)。 + - **PR #119(合并后发现,非 D6a 缺陷)**:**`main` 上 `pytest` 是红的,而每个 worktree 里都是绿的**。`test_no_new_file_starts_instructing_people_to_run_a_retired_tool` 用 `rglob` 走文件系统 ⇒ 读进 `tmp/context/**` 里评审用 `git archive` 导出的**退役之前**的仓库副本(14 行,**其中每处命名退役工具都是合法的**)。⚠️ **值得记的是那个不对称**:**worktree 结构性地缺少两个会打破它的目录**(没有 `tmp/`;嵌套 `.claude/worktrees/` 只在主 checkout 下),而 **C6/D5/D6a 的每一次 gate 都跑在 worktree 里** ⇒ **守卫看不见会打破它的东西——从我们一贯运行它的地方看不见。** 修法照 D6a caller census 立下的先例**从 git 派生**(tracked ∪ untracked-not-ignored;**untracked 刻意包含**——新写的文件正是它存在的理由且通常未 staged;gitignored 刻意不可见,**限制写进 docstring**)。牙**验过**:新增未跟踪文件 → 红 / 移除 → 绿 / 新增 gitignored → 绿。 + - **方法论新增三条**:① **判断「某性质是否已被覆盖」必须指出是哪一条测试**——只说"路径被覆盖了"恰好是犯错的形状(那句含混的话**先骗了写它的人、再通过他的报告骗了 lead**);配套答法是**三向 mutation**。② **一条测试变红有两种可能:代码坏了,或测试瞎了**——只看红绿分不出,所以派生式守卫替掉手敲清单时要问的不是"旧的红还在吗",而是**"那条性质现在由谁直接钉住"**。③ **别把 node-ID 差分当覆盖率证明**——稀疏测试是"同一 node ID、断言变多",差分数的是测试个数不是性质。另:**派生式守卫比手敲清单更容易空过**(来源为空 ⇒ 一个断言都不执行、直接 PASS)⇒ 必须配非空断言,且**注定将来会变红的测试要自带"何时该红、那时该做什么"**。 + - **过程(第四次协议复发,lead 的新变体)**:把 S1/S2 **追加**给评审后**没有更新"什么算收工"的判据** ⇒ 评审为**原条目**报的"收工"被读成"全部完成",**返工与新一轮评审被并行派发**(评审原话:"同一个失效模式换了条路径")。规则再补两条:**追加条目必须同时说清新的收工判据**;**派返工前先确认评审没在同一棵树上开工,两者必须串行**。评审自加一条:**收工声明写死覆盖了哪些条目**。⚠️ **评审自报一次 `cd` 错误**(后台命令不继承前一条 `cd` ⇒ 它的"最终全量"跑在主 checkout、一度误报"树在我脚下变了")——**代价为零,而且正是那次误跑暴露了 `main` 变红**。评审在 E5 上换了个答法值得记:它无法审计"数字是否来自重跑那一轮",于是**自己重写全部八条从干净树重跑,让那个前提变得无关紧要**——**当一个前提只能靠对方的话支撑时,最强的动作是让结论不再依赖它。** + - gates(lead 独立重跑):pytest **2705 passed / 1 skipped**(node-ID 2691 → 2706,dropped 3 / added 18;**三条 dropped 经评审双向核过**:更强方向 + "没丢"方向)/ ruff clean / **锚 0.9600·0.8408 未动** / 32/32 configs / `--verify` 77/77 @ 45c14aa / secret 0 / 冻结基线自 2026-07-25 起 **0 写入** / **跑完 demo 后真实 store 的 `momentum_20` mtime 仍停在 11:33 ⇒ demo 零写入**。 diff --git a/docs/progress/pr_ledger.md b/docs/progress/pr_ledger.md index b87a335..ab7e85d 100644 --- a/docs/progress/pr_ledger.md +++ b/docs/progress/pr_ledger.md @@ -24,3 +24,6 @@ | #115 | D5 C6 改造:三个重生成工具退役为 verify-only + CLI 收敛 + R16 核销 | | #116 | D5 C6 删除:删 11 个旧 runner + 10 测试,校验接进 C5 harness 三个读入点 | | #117 | 进度文档(D5 C6) | +| #118 | D6a:phase0/phase2 切 FactorService(含 B1 demo-store 隔离、日频 universe 无关守卫、caller 派生普查) | +| #119 | 修 retired-invocation 守卫的射程:文件清单从 git 派生而非目录遍历(`main` 曾红而 worktree 绿) | +| #120 | 进度文档(D6a) |