From b007fb099bfc24ae484cd8af6f9a0424a8a75c9d Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Fri, 12 Jun 2026 15:37:53 +0800 Subject: [PATCH 1/3] feat: P3-8 CSI500 independent generalization check (config + report naming) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ask whether the P3-7 sign-level conclusion (value_ep/value_bp positive, volatility_20 negative on the independent 2024-07..2026-05 window) generalizes outside the screened universes, specifically to CSI500 (000905.SH) — a cell independent in BOTH universe and time. Machinery is the unchanged P3-7 independent-validation layer: same factor groups, same cost scenarios, no new factors, no tuning, no alpha/portfolio/ execution/OOS-slicing change. - config/phase3_real_csi500_generalization.yaml: screened anchor SSE50|2022-2024 (must reproduce P3-6/P3-7), independent anchor SSE50|2024-2026 (must reproduce the P3-7 verdict numbers), and the NEW independent cell 000905.SH|2024-2026; CSI500|2022-2024 skipped + disclosed (runtime budget). Data feasibility probed: monthly 500-name index_weight snapshots through 2026-05-29 incl. a pre-start 2024-06-28 snapshot; 645 distinct in-window constituents. - output.subset_report_name (baseline_report_name precedent): each subset-validation study owns its report filename; None keeps the historical default bitwise (locked by tests) — a P3-8 run no longer clobbers the accepted P3-7 artifact. - tests: +4 (CSI500 config cells/groups/scenarios/hypotheses, sample classes, default report filename preserved, P3-8 owns its filename) -> 379 passed; ruff clean; 11 configs validate; demo run-phase0 unchanged (ic 0.96 / annual 0.8408) --- config/phase3_real_csi500_generalization.yaml | 203 ++++++++++++++++++ qt/config.py | 5 + qt/subset_validation.py | 15 +- tests/test_csi500_generalization.py | 84 ++++++++ 4 files changed, 305 insertions(+), 2 deletions(-) create mode 100644 config/phase3_real_csi500_generalization.yaml create mode 100644 tests/test_csi500_generalization.py diff --git a/config/phase3_real_csi500_generalization.yaml b/config/phase3_real_csi500_generalization.yaml new file mode 100644 index 0000000..1a272b9 --- /dev/null +++ b/config/phase3_real_csi500_generalization.yaml @@ -0,0 +1,203 @@ +# Phase 3-8 — REAL CSI500 independent generalization check (EXPLORATORY). +# +# Asks whether the P3-7 conclusion (value_ep/value_bp positive, volatility_20 +# negative — sign-level SUPPORTED on the independent 2024-07..2026-05 window +# over SSE50/CSI300) GENERALIZES outside the screened universes, specifically +# to CSI500 (000905.SH, mid-caps): a cell that is independent in BOTH universe +# and time. Machinery is the unchanged P3-7 independent-validation layer; same +# factor groups, same cost scenarios, no new factors, no tuning. +# +# Cell roles (explicitly labeled, never averaged together): +# * 000016.SH|2022-2024 -> SCREENED anchor: must reproduce P3-6/P3-7 exactly. +# * 000016.SH|2024-2026 -> INDEPENDENT anchor: must reproduce the P3-7 +# verdict numbers exactly (in-run reproducibility check). +# * 000905.SH|2024-2026 -> INDEPENDENT, NEW universe — the P3-8 question. +# * 000905.SH|2022-2024 -> skipped (runtime budget; ~645 names x 2y through +# the rate-limited SDK would add ~3h; disclosed in the report). +# +# Data feasibility (probed 2026-06-12): index_weight 000905.SH has monthly +# 500-name snapshots through 2026-05-29 incl. a 2024-06-28 pre-start snapshot; +# 645 distinct constituents in-window. Expected runtime ~3.5h total +# (CSI500 cell ~3h dominates). +# +# output.subset_report_name keeps this run from clobbering the accepted P3-7 +# artifact (both runs share the run-phase3-subset mode). +# +# Run: python -m qt.cli run-phase3-subset --config config/phase3_real_csi500_generalization.yaml +# (report: artifacts/reports/phase3_csi500_generalization.md; needs the +# tushare token; HEAVY ~3.5h.) + +project: + name: quantitative_trading_phase3_csi500_generalization + timezone: Asia/Shanghai + +data: + source: tushare + freq: D + start: "2022-07-01" + end: "2026-05-31" + external_secret_file: "/home/shaofl/Projects/financial_projects/.config.json" + tushare_token_key: "tushare.token" + output_name: phase3cg_daily + +universe: + type: index + index_code: "000016.SH" + symbols: [] + min_listing_days: 60 + filters: + missing_close: true + suspended: true + st: true + limit_up_down: true + +factors: + # FULL 11-factor pack (P3-5..P3-7, verbatim — factor math unchanged). + - name: momentum_20 + enabled: true + params: + window: 20 + price_col: close + - name: roe + enabled: true + params: {} + - name: netprofit_yoy + enabled: true + params: {} + - name: reversal_5 + enabled: true + params: + window: 5 + - name: reversal_20 + enabled: true + params: + window: 20 + - name: volatility_20 + enabled: true + params: + window: 20 + - name: liquidity_20 + enabled: true + params: + window: 20 + - name: overnight_mom_20 + enabled: true + params: + window: 20 + - name: value_ep + enabled: true + params: {} + - name: value_bp + enabled: true + params: {} + - name: grossprofit_margin + enabled: true + params: {} + +processing: + drop_missing: true + standardize: + enabled: true + method: zscore + winsorize: + enabled: false + method: mad + n: 3.0 + neutralize: + enabled: true + industry_col: industry + size_col: market_cap + industry_level: L1 + +alpha: + model: ic_weighted + params: + window: 60 + min_periods: 20 + mode: rolling + +portfolio: + constructor: topn_equal_weight + top_n: 20 + long_only: true + max_weight: null + turnover_cap: null + +backtest: + initial_nav: 1.0 + rebalance: monthly + event_order: close_to_next_period + cash_return: 0.0 + +cost: + fee_rate: 0.001 + slippage_rate: 0.0 + turnover_formula: l1 + +analytics: + forward_return_periods: + - 1 + - 5 + - 20 + quantiles: 5 + benchmark: null + +output: + root_dir: artifacts + data_dir: artifacts/data + factor_dir: artifacts/factors + report_dir: artifacts/reports + log_dir: artifacts/logs + overwrite: true + subset_report_name: phase3_csi500_generalization.md + +oos: + split_date: "2023-07-01" # base-config validation only; per-cell splits below + +robustness: + universes: + - "000016.SH" # SSE50 (anchors: screened 2022-2024 + independent 2024-2026) + - "000905.SH" # CSI500 (the NEW independent universe) + windows: + - label: "2022-2024" # SCREENED window (P3-3..P3-6 all used it) + start: "2022-07-01" + end: "2024-06-30" + split: "2023-07-01" + - label: "2024-2026" # INDEPENDENT window: postdates all screening + start: "2024-07-01" + end: "2026-05-31" + split: "2025-07-01" + skip_cells: + - universe: "000905.SH" + window: "2022-2024" # runtime budget (~645 names x 2y); disclosed + +subset_validation: + # P3-6/P3-7 group comparison reused VERBATIM (groups/cost logic unchanged). + groups: + - label: legacy_trio + factors: [momentum_20, roe, netprofit_yoy] + - label: full_pack + factors: [momentum_20, roe, netprofit_yoy, reversal_5, reversal_20, + volatility_20, liquidity_20, overnight_mom_20, value_ep, + value_bp, grossprofit_margin] + - label: value_lowvol + factors: [value_ep, value_bp, volatility_20] + - label: value_lowvol_liq + factors: [value_ep, value_bp, volatility_20, liquidity_20] + cost_scenarios: + - label: base + fee_multiplier: 1.0 + - label: 2x + fee_multiplier: 2.0 + - label: high_cost + fee_multiplier: 4.0 + independent_cells: + - universe: "000016.SH" + window: "2024-2026" # P3-7 reproducibility anchor + - universe: "000905.SH" + window: "2024-2026" # the P3-8 generalization cell + hypotheses: + value_ep: positive + value_bp: positive + volatility_20: negative + min_rebalances: 8 diff --git a/qt/config.py b/qt/config.py index 2e357f1..a468c65 100644 --- a/qt/config.py +++ b/qt/config.py @@ -182,6 +182,11 @@ class OutputCfg(_Strict): # historical default 'phase2_real_baseline.md'; a multi-factor baseline config # sets its own name so it never overwrites the phase2 report (P3-1). baseline_report_name: str | None = None + # Filename for the subset-validation report (run-phase3-subset). None keeps + # the historical default 'phase3_subset_validation.md'; a config sets its + # own name so different studies sharing the run mode never overwrite each + # other's report (P3-8; the same precedent as baseline_report_name). + subset_report_name: str | None = None class OOSCfg(_Strict): diff --git a/qt/subset_validation.py b/qt/subset_validation.py index f2742f1..b1a8c22 100644 --- a/qt/subset_validation.py +++ b/qt/subset_validation.py @@ -67,7 +67,8 @@ __all__ = ["SubsetCellResult", "SubsetValidationResult", "run_phase3_subset", "render_subset_validation", "check_subset_preconditions", "process_group", "subperiod_cost", "summarize_subset_matrix", - "sample_class", "independent_verdict", "summarize_by_sample"] + "sample_class", "independent_verdict", "summarize_by_sample", + "subset_report_filename"] _LOGGER_NAME = "qt.run_phase3_subset" @@ -357,6 +358,16 @@ def summarize_by_sample( } +def subset_report_filename(cfg: RootConfig) -> str: + """The subset-validation report filename for this config (P3-8). + + ``output.subset_report_name`` lets each study own its report file; None + keeps the historical default — behaviour-preserving for the P3-6/P3-7 + configs (locked by tests), so old runs' paths never silently change. + """ + return cfg.output.subset_report_name or "phase3_subset_validation.md" + + def check_subset_preconditions( cfg: RootConfig, runner: str = "run-phase3-subset" ) -> None: @@ -651,7 +662,7 @@ def run_phase3_subset(config_path: str) -> SubsetValidationResult: cell_runtimes=runtimes, skipped_cells=skipped, summary=summarize_subset_matrix(plain, base_scenario=base_scenario), - report_path=Path(cfg.output.report_dir) / "phase3_subset_validation.md", + report_path=Path(cfg.output.report_dir) / subset_report_filename(cfg), log_path=log_path, cell_samples=cell_samples, sample_summaries=summarize_by_sample( diff --git a/tests/test_csi500_generalization.py b/tests/test_csi500_generalization.py new file mode 100644 index 0000000..07b929e --- /dev/null +++ b/tests/test_csi500_generalization.py @@ -0,0 +1,84 @@ +"""P3-8: CSI500 independent generalization config + report-name (network-free). + +Locks the P3-8 contract on top of the unchanged P3-7 machinery: + * the CSI500 config validates and labels its cells correctly (SSE50|2022-2024 + = screened anchor; SSE50|2024-2026 and 000905.SH|2024-2026 = independent + holdouts; CSI500|2022-2024 skipped + disclosed); + * ``output.subset_report_name`` lets a subset-validation config own its + report filename (default None keeps the historical + ``phase3_subset_validation.md`` — behaviour-preserving, the + ``baseline_report_name`` precedent), so a P3-8 run no longer clobbers the + accepted P3-7 artifact; + * the P3-6/P3-7 configs keep validating unchanged (their own test files lock + the report shape; nothing else in the machinery moves). +""" + +from __future__ import annotations + +from pathlib import Path + +from qt.config import load_config + +_CSI500_CONFIG = str( + Path(__file__).resolve().parents[1] + / "config" / "phase3_real_csi500_generalization.yaml" +) +_P37_CONFIG = str( + Path(__file__).resolve().parents[1] + / "config" / "phase3_real_independent_validation.yaml" +) + + +def test_csi500_config_validates_with_expected_cells(): + cfg = load_config(_CSI500_CONFIG) + assert "000905.SH" in cfg.robustness.universes + assert "000016.SH" in cfg.robustness.universes + skipped = {(s.universe, s.window) for s in cfg.robustness.skip_cells} + assert ("000905.SH", "2022-2024") in skipped # runtime budget, disclosed + sv = cfg.subset_validation + indep = {(c.universe, c.window) for c in sv.independent_cells} + assert ("000905.SH", "2024-2026") in indep # THE new cell + assert ("000016.SH", "2024-2026") in indep # P3-7 reproducibility anchor + assert ("000016.SH", "2022-2024") not in indep # screened anchor + # same hypotheses / groups / scenarios as P3-7 (no tuning, no new factors) + assert sv.hypotheses == { + "value_ep": "positive", "value_bp": "positive", "volatility_20": "negative", + } + assert [g.label for g in sv.groups] == [ + "legacy_trio", "full_pack", "value_lowvol", "value_lowvol_liq", + ] + assert {s.label: s.fee_multiplier for s in sv.cost_scenarios} == { + "base": 1.0, "2x": 2.0, "high_cost": 4.0, + } + + +def test_csi500_cells_sample_classes(): + from qt.subset_validation import sample_class + + cfg = load_config(_CSI500_CONFIG) + assert sample_class(cfg, "000016.SH", "2022-2024") == "screened" + assert sample_class(cfg, "000016.SH", "2024-2026") == "independent" + assert sample_class(cfg, "000905.SH", "2024-2026") == "independent" + + +def test_subset_report_name_default_preserves_old_filename(): + """Configs without subset_report_name keep the historical report path.""" + from qt.subset_validation import subset_report_filename + + p36 = load_config(str( + Path(__file__).resolve().parents[1] + / "config" / "phase3_real_subset_costs.yaml" + )) + p37 = load_config(_P37_CONFIG) + assert subset_report_filename(p36) == "phase3_subset_validation.md" + assert subset_report_filename(p37) == "phase3_subset_validation.md" + + +def test_csi500_config_owns_its_report_filename(): + """The P3-8 run must not clobber the accepted P3-7 artifact.""" + from qt.subset_validation import subset_report_filename + + cfg = load_config(_CSI500_CONFIG) + name = subset_report_filename(cfg) + assert name == "phase3_csi500_generalization.md" + assert name != "phase3_subset_validation.md" From 36b6f6bd6e939e7ac2a11171041118a7adf1e064 Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Fri, 12 Jun 2026 19:17:22 +0800 Subject: [PATCH 2/3] docs: record P3-8 real CSI500 generalization results and verdicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CLAUDE.md / AGENTS.md (verbatim copy): P3-8 progress entry — both anchors reconciled exactly (screened raw ICs 22/22 == P3-5; the independent SSE50 verdict ICs identical to P3-7, a second reproducibility confirmation); CSI500|2024-2026 verdict SUPPORTED (value_ep +0.0083/+0.0145, value_bp +0.0230/+0.0127, volatility_20 -0.0350/-0.0272; 21 settled rebalances vs minimum 8) with LESS attenuation than the SSE50/CSI300 holdouts -> the P3-7 sign-level conclusion GENERALIZES outside the screened universes; CSI500 portfolio results positive across all groups and the full cost ladder (honest caveat: legacy_trio +17.8% is another small-sample/regime ranking flip; not a return claim); gates updated to 379 passed - TEST_REPORT.md: P3-8 per-file breakdown (4) + real-data validation entry (anchor reconciliation, verdict, preserved P3-7 artifact, secret scan 0) - RUNBOOK.md: Phase 3-8 section (cell roles, subset_report_name, report-type title note, skip disclosure) --- AGENTS.md | 12 +++++++++--- CLAUDE.md | 12 +++++++++--- RUNBOOK.md | 24 ++++++++++++++++++++++++ TEST_REPORT.md | 18 +++++++++++++----- 4 files changed, 55 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c7b2e4d..dc6835d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -148,6 +148,12 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri - combo_ic_weighted 独立 test IC **8/8 正**(4 组×2 cells,0.0060~0.0238)——walk-forward IC 加权在 holdout 上仍有效。 - **组合净值仍未确立**:独立 base ic test——legacy_trio **+1.15%/+8.13%**(2/2 正,恰是此前实证"无信号"的组!)、full_pack +7.30%/−1.93%、value_lowvol +1.72%/−4.68%、liq +3.53%/−0.39%——组间排名跨 cell 翻转,~21 rebalances 小样本,**IC 符号确认 ≠ 组合盈利**;成本阶梯仍全单调。非收益声明。 - secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 -- ✅ 质量门:`pytest` **375 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle);`ruff` clean;`validate-config`(全部 10 配置)+ `run-phase0`(demo)均 OK。 -- ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据、旧三因子无信号(P3-3/P3-4 实证;但其组合在 2024-2026 holdout 上 2/2 正——小样本翻转的又一例证);value/低波信号获得**独立样本符号级确认但量级衰减**(P3-7),组合级盈利能力未确立;subset 报告文件名跨 config 共用(后跑覆盖前跑)。 -- 路线图下一步:更长 holdout 积累(2026-06 之后滚动复检)/ 新 universe(中证500)/ 成本模型细化(印花税卖侧不对称、冲击成本),或分钟级(architecture.html §11)。 +- ✅ **Phase 3-8 CSI500 独立泛化检验**(分支 `p3-csi500-value-lowvol-generalization`,**EXPLORATORY**):P3-7 的符号级结论是否泛化到筛选 universe 之外——新增独立 cell **CSI500(000905.SH)|2024-2026**(universe+时间双独立)。**机器零改动**复用 P3-7 层(同组/同成本场景/同假设,不加因子不调参);唯一代码变更 `output.subset_report_name`(沿 `baseline_report_name` 先例,默认 None 保持旧文件名 bitwise,测试锁定)——P3-8 报告独立成文件,**不再覆盖已验收的 P3-7 artifact**。 + - cell 设计:SSE50|2022-2024(screened 锚,须 ≡P3-6/P3-7)+ SSE50|2024-2026(independent 锚,须 ≡P3-7 verdict 数字)+ CSI500|2024-2026(主问题);CSI500|2022-2024 skip 披露(~645 名×2y 再加 ~3h 不值)。数据可行性先实测:index_weight 月度 500 名快照到 2026-05-29 含 2024-06-28 pre-start 锚。 + - **真实 run(3 cells/~3.55h,CSI500 735 distinct 名主导;一次跑通)双锚对账 ✓**:screened raw IC 22/22 ≡ P3-5 报告;independent SSE50 verdict IC 逐数 ≡ P3-7(+0.0322/+0.0134、+0.0379/+0.0033、−0.0320/−0.0120)——复现性二度确认。 + - **CSI500 verdict:SUPPORTED**(21 settled vs min 8):value_ep **+0.0083/+0.0145**(train/test)、value_bp **+0.0230/+0.0127**、volatility_20 **−0.0350/−0.0272**——三假设双子期全保持。**且衰减更小**:CSI500 test 子期量级(0.0127~0.0272)明显高于 SSE50/CSI300 holdout 的后段(0.003~0.016)——value/低波信号在中盘股上更强,**P3-7 结论泛化成立(GENERALIZES,未减弱)**。 + - combo_ic_weighted CSI500 test IC 全 4 组正(0.0243/0.0294/0.0286/0.0285,为三个 holdout cells 最高);**组合净值 CSI500 base 全组正且 4× 高成本仍全正**(trio +17.80%→+10.57% / full_pack +7.83%→+2.95% / value_lowvol +4.19%→+1.14% / liq +4.02%→+0.84%)——首个全成本阶梯为正的 cell。⚠️ 仍然诚实:trio +17.80% 又是组间排名跨 cell 翻转的例证(中盘动量 regime);单窗口 ~21 调仓小样本;**非收益声明**。 + - 报告标题沿用 P3-7 机器的报告类型名(study 由 project 行与 cell 标签标识);文案/分区检查全过(无 stale P3-6 措辞、verdict 节只含独立 cells);secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 +- ✅ 质量门:`pytest` **379 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle / P3-8=4);`ruff` clean;`validate-config`(全部 11 配置)+ `run-phase0`(demo)均 OK。 +- ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据、旧三因子无信号(P3-3/P3-4 实证;但其组合在 2024-2026 holdout 上 SSE50/CSI300/CSI500 全正、CSI500 高达 +17.8%——小样本/regime 翻转的持续例证);value/低波信号获得**独立样本符号级确认**(P3-7 SSE50/CSI300 量级衰减;P3-8 CSI500 泛化成立且更强),组合级盈利能力仍未确立(排名跨 cell 翻转);subset 报告文件名已可配置(P3-8 起不再互覆盖)。 +- 路线图下一步:更长 holdout 积累(2026-06 之后滚动复检)/ 中证1000 或全市场扩展 / 成本模型细化(印花税卖侧不对称、冲击成本),或分钟级(architecture.html §11)。 diff --git a/CLAUDE.md b/CLAUDE.md index c7b2e4d..dc6835d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -148,6 +148,12 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri - combo_ic_weighted 独立 test IC **8/8 正**(4 组×2 cells,0.0060~0.0238)——walk-forward IC 加权在 holdout 上仍有效。 - **组合净值仍未确立**:独立 base ic test——legacy_trio **+1.15%/+8.13%**(2/2 正,恰是此前实证"无信号"的组!)、full_pack +7.30%/−1.93%、value_lowvol +1.72%/−4.68%、liq +3.53%/−0.39%——组间排名跨 cell 翻转,~21 rebalances 小样本,**IC 符号确认 ≠ 组合盈利**;成本阶梯仍全单调。非收益声明。 - secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 -- ✅ 质量门:`pytest` **375 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle);`ruff` clean;`validate-config`(全部 10 配置)+ `run-phase0`(demo)均 OK。 -- ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据、旧三因子无信号(P3-3/P3-4 实证;但其组合在 2024-2026 holdout 上 2/2 正——小样本翻转的又一例证);value/低波信号获得**独立样本符号级确认但量级衰减**(P3-7),组合级盈利能力未确立;subset 报告文件名跨 config 共用(后跑覆盖前跑)。 -- 路线图下一步:更长 holdout 积累(2026-06 之后滚动复检)/ 新 universe(中证500)/ 成本模型细化(印花税卖侧不对称、冲击成本),或分钟级(architecture.html §11)。 +- ✅ **Phase 3-8 CSI500 独立泛化检验**(分支 `p3-csi500-value-lowvol-generalization`,**EXPLORATORY**):P3-7 的符号级结论是否泛化到筛选 universe 之外——新增独立 cell **CSI500(000905.SH)|2024-2026**(universe+时间双独立)。**机器零改动**复用 P3-7 层(同组/同成本场景/同假设,不加因子不调参);唯一代码变更 `output.subset_report_name`(沿 `baseline_report_name` 先例,默认 None 保持旧文件名 bitwise,测试锁定)——P3-8 报告独立成文件,**不再覆盖已验收的 P3-7 artifact**。 + - cell 设计:SSE50|2022-2024(screened 锚,须 ≡P3-6/P3-7)+ SSE50|2024-2026(independent 锚,须 ≡P3-7 verdict 数字)+ CSI500|2024-2026(主问题);CSI500|2022-2024 skip 披露(~645 名×2y 再加 ~3h 不值)。数据可行性先实测:index_weight 月度 500 名快照到 2026-05-29 含 2024-06-28 pre-start 锚。 + - **真实 run(3 cells/~3.55h,CSI500 735 distinct 名主导;一次跑通)双锚对账 ✓**:screened raw IC 22/22 ≡ P3-5 报告;independent SSE50 verdict IC 逐数 ≡ P3-7(+0.0322/+0.0134、+0.0379/+0.0033、−0.0320/−0.0120)——复现性二度确认。 + - **CSI500 verdict:SUPPORTED**(21 settled vs min 8):value_ep **+0.0083/+0.0145**(train/test)、value_bp **+0.0230/+0.0127**、volatility_20 **−0.0350/−0.0272**——三假设双子期全保持。**且衰减更小**:CSI500 test 子期量级(0.0127~0.0272)明显高于 SSE50/CSI300 holdout 的后段(0.003~0.016)——value/低波信号在中盘股上更强,**P3-7 结论泛化成立(GENERALIZES,未减弱)**。 + - combo_ic_weighted CSI500 test IC 全 4 组正(0.0243/0.0294/0.0286/0.0285,为三个 holdout cells 最高);**组合净值 CSI500 base 全组正且 4× 高成本仍全正**(trio +17.80%→+10.57% / full_pack +7.83%→+2.95% / value_lowvol +4.19%→+1.14% / liq +4.02%→+0.84%)——首个全成本阶梯为正的 cell。⚠️ 仍然诚实:trio +17.80% 又是组间排名跨 cell 翻转的例证(中盘动量 regime);单窗口 ~21 调仓小样本;**非收益声明**。 + - 报告标题沿用 P3-7 机器的报告类型名(study 由 project 行与 cell 标签标识);文案/分区检查全过(无 stale P3-6 措辞、verdict 节只含独立 cells);secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 +- ✅ 质量门:`pytest` **379 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle / P3-8=4);`ruff` clean;`validate-config`(全部 11 配置)+ `run-phase0`(demo)均 OK。 +- ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据、旧三因子无信号(P3-3/P3-4 实证;但其组合在 2024-2026 holdout 上 SSE50/CSI300/CSI500 全正、CSI500 高达 +17.8%——小样本/regime 翻转的持续例证);value/低波信号获得**独立样本符号级确认**(P3-7 SSE50/CSI300 量级衰减;P3-8 CSI500 泛化成立且更强),组合级盈利能力仍未确立(排名跨 cell 翻转);subset 报告文件名已可配置(P3-8 起不再互覆盖)。 +- 路线图下一步:更长 holdout 积累(2026-06 之后滚动复检)/ 中证1000 或全市场扩展 / 成本模型细化(印花税卖侧不对称、冲击成本),或分钟级(architecture.html §11)。 diff --git a/RUNBOOK.md b/RUNBOOK.md index 12e1c9a..870d7e8 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -471,6 +471,30 @@ Semantics worth knowing before reading the report: `artifacts/reports/phase3_subset_validation.md` — a P3-7 run overwrites a P3-6 report (regenerable; numbers live in CLAUDE.md/TEST_REPORT). +## Phase 3-8 — CSI500 independent generalization check (EXPLORATORY) + +Asks whether the P3-7 sign-level conclusion generalizes OUTSIDE the screened +universes: the same `run-phase3-subset` machinery (unchanged) with a NEW +independent cell `000905.SH|2024-2026` (CSI500 — independent in BOTH universe +and time), plus the two SSE50 anchors (screened 2022-2024 must reproduce +P3-6/P3-7; independent 2024-2026 must reproduce the P3-7 verdict numbers). +Documented by `config/phase3_real_csi500_generalization.yaml`. + +```bash +# validate (no network) +... -m qt.cli validate-config --config config/phase3_real_csi500_generalization.yaml +# run (network + token; HEAVY ~3.5h — the 735-name CSI500 cell dominates) +... -m qt.cli run-phase3-subset --config config/phase3_real_csi500_generalization.yaml +``` + +- `output.subset_report_name` (the `baseline_report_name` precedent) gives this + study its own report file (`phase3_csi500_generalization.md`), so the run no + longer clobbers the accepted P3-7 artifact; configs without the key keep the + historical filename bitwise (locked by tests). +- The report title carries the REPORT TYPE (the P3-7 independent-validation + machinery); the study is identified by the project name line and the cell + labels. CSI500|2022-2024 is skip_cells-listed (runtime budget) and disclosed. + ## Quality gate ```bash diff --git a/TEST_REPORT.md b/TEST_REPORT.md index e310bab..43a8965 100644 --- a/TEST_REPORT.md +++ b/TEST_REPORT.md @@ -1,4 +1,4 @@ -# TEST_REPORT — Phase 0 + Phase 1 + Phase 2 + Phase 3 (bias-boundary → execution realism → PIT industry → standard analytics → multi-factor → walk-forward IC alpha → OOS stability → robustness matrix → factor candidates → subset + cost sensitivity → independent validation) +# TEST_REPORT — Phase 0 + Phase 1 + Phase 2 + Phase 3 (bias-boundary → execution realism → PIT industry → standard analytics → multi-factor → walk-forward IC alpha → OOS stability → robustness matrix → factor candidates → subset + cost sensitivity → independent validation → CSI500 generalization) ## Commands @@ -17,6 +17,7 @@ Run from the repo root with the project python (env `quant_mf`): /home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/phase3_real_factor_candidates.yaml /home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/phase3_real_subset_costs.yaml /home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/phase3_real_independent_validation.yaml +/home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli validate-config --config config/phase3_real_csi500_generalization.yaml /home/shaofl/Development/env_tools/envs/quant_mf/bin/python -m qt.cli run-phase0 --config config/example.yaml ``` @@ -24,12 +25,12 @@ Run from the repo root with the project python (env `quant_mf`): | Gate | Command | Result | |---|---|---| -| Unit + integration | `pytest -q` | **375 passed, 0 failed** | +| Unit + integration | `pytest -q` | **379 passed, 0 failed** | | Lint | `ruff check .` | **All checks passed** | -| Config validation | `validate-config` (demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml` + `phase3_real_ic_weighted.yaml` + `phase3_real_oos_stability.yaml` + `phase3_real_robustness_matrix.yaml` + `phase3_real_factor_candidates.yaml` + `phase3_real_subset_costs.yaml` + `phase3_real_independent_validation.yaml`) | exit `0`, prints `OK` | +| Config validation | `validate-config` (demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml` + `phase3_real_ic_weighted.yaml` + `phase3_real_oos_stability.yaml` + `phase3_real_robustness_matrix.yaml` + `phase3_real_factor_candidates.yaml` + `phase3_real_subset_costs.yaml` + `phase3_real_independent_validation.yaml` + `phase3_real_csi500_generalization.yaml`) | exit `0`, prints `OK` | | End-to-end run | `run-phase0` (demo) | exit `0`, writes `artifacts/reports/phase0_summary.md` | -Counts below are the actual per-file `pytest` numbers (sum = 375). +Counts below are the actual per-file `pytest` numbers (sum = 379). ## Per-file breakdown — Phase 0 core (97) @@ -147,7 +148,14 @@ Counts below are the actual per-file `pytest` numbers (sum = 375). |---|---|---| | `test_independent_validation.py` | 25 | independent-cells config validation (must reference declared robustness universes/windows; a skip-listed cell cannot be declared independent — a holdout that never runs is a contradiction; hypotheses must reference ENABLED factors with a positive/negative literal; min_rebalances > 0; **the P3-6 config still validates with inert defaults**); explicit sample-class labeling (undeclared → screened, the conservative default); **verdict logic** (HOLDS iff expected IC sign in BOTH subperiods; SUPPORTED / PARTIAL / NOT SUPPORTED; **INSUFFICIENT-DATA overrides the sign check** with n_settled vs threshold disclosed; NaN or missing IC never holds); **per-class summaries never mix** (screened cell attributions never appear under independent and vice versa); report renders sample column + per-class cross-cell sections + a verdict section containing ONLY independent cells + INSUFFICIENT-DATA disclosure; a P3-6-era result (defaults) renders the old report shape; no secret; **sample-aware title/framing/caveats** (a run with independent cells must not carry the P3-6 'same windows / not independent confirmation' framing — review HIGH x2; P3-6-era rendering and downgrades text unchanged, locked by regression tests) | | `test_tushare_throttle.py` (+1) | 1 | the DEFAULT retry budget survives a multi-failure transient outage (6 attempts ≈ 23s of capped exponential backoff; two real ~2h runs died on ConnectionError under the old 3-attempt ≈ 3s budget) | -| **Total (P0 + P1 + P2-1..P2-4 + P3-1..P3-7)** | **375** | | +| **Total through P3-7** | **375** | | + +## Per-file breakdown — Phase 3-8 CSI500 generalization (4) + +| Test file | Tests | Red-line / feature | +|---|---|---| +| `test_csi500_generalization.py` | 4 | CSI500 config validates with the expected cell roles (screened anchor SSE50|2022-2024; independent SSE50|2024-2026 + 000905.SH|2024-2026; CSI500|2022-2024 skipped+disclosed; same groups/scenarios/hypotheses as P3-7 — no tuning); sample classes labeled correctly; **`output.subset_report_name`** lets each subset-validation study own its report file (default None keeps the historical `phase3_subset_validation.md` bitwise — the P3-6/P3-7 configs are locked unchanged), so a P3-8 run never clobbers the accepted P3-7 artifact | +| **Total (P0 + P1 + P2-1..P2-4 + P3-1..P3-8)** | **379** | | ## Real-data validation (manual, not in CI — TEST-002 keeps the suite network-free) From 5b5bf20e77546a5c141de633f79dd3523a65af78 Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Sat, 13 Jun 2026 11:56:14 +0800 Subject: [PATCH 3/3] fix: config-drive the subset report H1 title; CSI500 names its study (review HIGH) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The P3-8 CSI500 report rendered with the hardcoded "Phase 3-7 — Independent-Sample Validation" H1 (qt/reports.py only branched the title on has_independent), contradicting the config and docs that identify it as the P3-8 CSI500 generalization study — the same class of stale report wording flagged on P3-7. - config: OutputCfg gains `subset_report_title` (the `subset_report_name` precedent — config owns the study identity). None keeps the renderer's sample-aware default (P3-7 independent / P3-6 post-hoc). - render_subset_validation: the H1 is config-driven when output.subset_report_title is set, else the existing sample-aware default. The body framing (screened-vs-independent mechanics) stays sample-aware — correct for any such study, so only the H1 changes. - config/phase3_real_csi500_generalization.yaml: sets the title to "Phase 3-8 — CSI500 Independent Generalization Check ...". - tests: +4 (config sets title; P3-6/P3-7 leave it unset; rendered CSI500 H1 starts with Phase 3-8 + contains CSI500 + is NOT Phase 3-7; default title preserved without the override) -> 383 passed. - artifact: regenerated deterministically — only the H1 line changes with this fix, so the accepted real numbers are untouched (the 3.5h matrix is NOT rerun); diff is exactly the 1 title line; secret scan 0. - docs: RUNBOOK/TEST_REPORT/CLAUDE.md/AGENTS.md note the config-driven title and the corrected "title carries report type" wording. --- AGENTS.md | 4 +- CLAUDE.md | 4 +- RUNBOOK.md | 8 ++- TEST_REPORT.md | 10 ++-- config/phase3_real_csi500_generalization.yaml | 4 ++ qt/config.py | 6 ++ qt/reports.py | 14 ++++- tests/test_csi500_generalization.py | 55 +++++++++++++++++++ 8 files changed, 90 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index dc6835d..63e1ff6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -153,7 +153,7 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri - **真实 run(3 cells/~3.55h,CSI500 735 distinct 名主导;一次跑通)双锚对账 ✓**:screened raw IC 22/22 ≡ P3-5 报告;independent SSE50 verdict IC 逐数 ≡ P3-7(+0.0322/+0.0134、+0.0379/+0.0033、−0.0320/−0.0120)——复现性二度确认。 - **CSI500 verdict:SUPPORTED**(21 settled vs min 8):value_ep **+0.0083/+0.0145**(train/test)、value_bp **+0.0230/+0.0127**、volatility_20 **−0.0350/−0.0272**——三假设双子期全保持。**且衰减更小**:CSI500 test 子期量级(0.0127~0.0272)明显高于 SSE50/CSI300 holdout 的后段(0.003~0.016)——value/低波信号在中盘股上更强,**P3-7 结论泛化成立(GENERALIZES,未减弱)**。 - combo_ic_weighted CSI500 test IC 全 4 组正(0.0243/0.0294/0.0286/0.0285,为三个 holdout cells 最高);**组合净值 CSI500 base 全组正且 4× 高成本仍全正**(trio +17.80%→+10.57% / full_pack +7.83%→+2.95% / value_lowvol +4.19%→+1.14% / liq +4.02%→+0.84%)——首个全成本阶梯为正的 cell。⚠️ 仍然诚实:trio +17.80% 又是组间排名跨 cell 翻转的例证(中盘动量 regime);单窗口 ~21 调仓小样本;**非收益声明**。 - - 报告标题沿用 P3-7 机器的报告类型名(study 由 project 行与 cell 标签标识);文案/分区检查全过(无 stale P3-6 措辞、verdict 节只含独立 cells);secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 -- ✅ 质量门:`pytest` **379 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle / P3-8=4);`ruff` clean;`validate-config`(全部 11 配置)+ `run-phase0`(demo)均 OK。 + - **报告标题配置化**(`output.subset_report_title`,沿 `subset_report_name` 先例):P3-8 报告 H1 自报 study 名「Phase 3-8 — CSI500 Independent Generalization Check」而非机器默认 P3-7 标签(review 同类 stale-wording 问题修复,测试断言首行非 P3-7;P3-6/P3-7 配置不设此项、保持 sample-aware 默认,回归锁定);文案/分区检查全过(无 stale P3-6 措辞、verdict 节只含独立 cells);secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 +- ✅ 质量门:`pytest` **383 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle / P3-8=8);`ruff` clean;`validate-config`(全部 11 配置)+ `run-phase0`(demo)均 OK。 - ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据、旧三因子无信号(P3-3/P3-4 实证;但其组合在 2024-2026 holdout 上 SSE50/CSI300/CSI500 全正、CSI500 高达 +17.8%——小样本/regime 翻转的持续例证);value/低波信号获得**独立样本符号级确认**(P3-7 SSE50/CSI300 量级衰减;P3-8 CSI500 泛化成立且更强),组合级盈利能力仍未确立(排名跨 cell 翻转);subset 报告文件名已可配置(P3-8 起不再互覆盖)。 - 路线图下一步:更长 holdout 积累(2026-06 之后滚动复检)/ 中证1000 或全市场扩展 / 成本模型细化(印花税卖侧不对称、冲击成本),或分钟级(architecture.html §11)。 diff --git a/CLAUDE.md b/CLAUDE.md index dc6835d..63e1ff6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,7 +153,7 @@ data → universe → factors(特征) → alpha(合成/预测) → portfolio(+ri - **真实 run(3 cells/~3.55h,CSI500 735 distinct 名主导;一次跑通)双锚对账 ✓**:screened raw IC 22/22 ≡ P3-5 报告;independent SSE50 verdict IC 逐数 ≡ P3-7(+0.0322/+0.0134、+0.0379/+0.0033、−0.0320/−0.0120)——复现性二度确认。 - **CSI500 verdict:SUPPORTED**(21 settled vs min 8):value_ep **+0.0083/+0.0145**(train/test)、value_bp **+0.0230/+0.0127**、volatility_20 **−0.0350/−0.0272**——三假设双子期全保持。**且衰减更小**:CSI500 test 子期量级(0.0127~0.0272)明显高于 SSE50/CSI300 holdout 的后段(0.003~0.016)——value/低波信号在中盘股上更强,**P3-7 结论泛化成立(GENERALIZES,未减弱)**。 - combo_ic_weighted CSI500 test IC 全 4 组正(0.0243/0.0294/0.0286/0.0285,为三个 holdout cells 最高);**组合净值 CSI500 base 全组正且 4× 高成本仍全正**(trio +17.80%→+10.57% / full_pack +7.83%→+2.95% / value_lowvol +4.19%→+1.14% / liq +4.02%→+0.84%)——首个全成本阶梯为正的 cell。⚠️ 仍然诚实:trio +17.80% 又是组间排名跨 cell 翻转的例证(中盘动量 regime);单窗口 ~21 调仓小样本;**非收益声明**。 - - 报告标题沿用 P3-7 机器的报告类型名(study 由 project 行与 cell 标签标识);文案/分区检查全过(无 stale P3-6 措辞、verdict 节只含独立 cells);secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 -- ✅ 质量门:`pytest` **379 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle / P3-8=4);`ruff` clean;`validate-config`(全部 11 配置)+ `run-phase0`(demo)均 OK。 + - **报告标题配置化**(`output.subset_report_title`,沿 `subset_report_name` 先例):P3-8 报告 H1 自报 study 名「Phase 3-8 — CSI500 Independent Generalization Check」而非机器默认 P3-7 标签(review 同类 stale-wording 问题修复,测试断言首行非 P3-7;P3-6/P3-7 配置不设此项、保持 sample-aware 默认,回归锁定);文案/分区检查全过(无 stale P3-6 措辞、verdict 节只含独立 cells);secret scan 报告+日志 0 处;demo 0.96/0.84 不变。 +- ✅ 质量门:`pytest` **383 passed**(P0=97 / P1=78 / P2-1=22 / P2-2=22 / P2-3=14 / P2-4=8 / P3-1=10 / P3-2=18 / P3-3=16 / P3-4=15 / P3-5=22 / P3-6=27 / P3-7=25+1 throttle / P3-8=8);`ruff` clean;`validate-config`(全部 11 配置)+ `run-phase0`(demo)均 OK。 - ⚠️ 剩余(已显式披露):日线 only、demo 路径非真数据、旧三因子无信号(P3-3/P3-4 实证;但其组合在 2024-2026 holdout 上 SSE50/CSI300/CSI500 全正、CSI500 高达 +17.8%——小样本/regime 翻转的持续例证);value/低波信号获得**独立样本符号级确认**(P3-7 SSE50/CSI300 量级衰减;P3-8 CSI500 泛化成立且更强),组合级盈利能力仍未确立(排名跨 cell 翻转);subset 报告文件名已可配置(P3-8 起不再互覆盖)。 - 路线图下一步:更长 holdout 积累(2026-06 之后滚动复检)/ 中证1000 或全市场扩展 / 成本模型细化(印花税卖侧不对称、冲击成本),或分钟级(architecture.html §11)。 diff --git a/RUNBOOK.md b/RUNBOOK.md index 870d7e8..809ccf4 100644 --- a/RUNBOOK.md +++ b/RUNBOOK.md @@ -491,9 +491,11 @@ Documented by `config/phase3_real_csi500_generalization.yaml`. study its own report file (`phase3_csi500_generalization.md`), so the run no longer clobbers the accepted P3-7 artifact; configs without the key keep the historical filename bitwise (locked by tests). -- The report title carries the REPORT TYPE (the P3-7 independent-validation - machinery); the study is identified by the project name line and the cell - labels. CSI500|2022-2024 is skip_cells-listed (runtime budget) and disclosed. +- `output.subset_report_title` sets the report's H1 so it names THIS study + ("Phase 3-8 — CSI500 Independent Generalization Check") instead of the + machinery's default phase label; configs without it keep the renderer's + sample-aware default (P3-7 independent / P3-6 post-hoc), locked by tests. + CSI500|2022-2024 is skip_cells-listed (runtime budget) and disclosed. ## Quality gate diff --git a/TEST_REPORT.md b/TEST_REPORT.md index 43a8965..87088c0 100644 --- a/TEST_REPORT.md +++ b/TEST_REPORT.md @@ -25,12 +25,12 @@ Run from the repo root with the project python (env `quant_mf`): | Gate | Command | Result | |---|---|---| -| Unit + integration | `pytest -q` | **379 passed, 0 failed** | +| Unit + integration | `pytest -q` | **383 passed, 0 failed** | | Lint | `ruff check .` | **All checks passed** | | Config validation | `validate-config` (demo + `example_tushare.yaml` + `phase2_real_baseline.yaml` + `phase3_real_multifactor.yaml` + `phase3_real_ic_weighted.yaml` + `phase3_real_oos_stability.yaml` + `phase3_real_robustness_matrix.yaml` + `phase3_real_factor_candidates.yaml` + `phase3_real_subset_costs.yaml` + `phase3_real_independent_validation.yaml` + `phase3_real_csi500_generalization.yaml`) | exit `0`, prints `OK` | | End-to-end run | `run-phase0` (demo) | exit `0`, writes `artifacts/reports/phase0_summary.md` | -Counts below are the actual per-file `pytest` numbers (sum = 379). +Counts below are the actual per-file `pytest` numbers (sum = 383). ## Per-file breakdown — Phase 0 core (97) @@ -150,12 +150,12 @@ Counts below are the actual per-file `pytest` numbers (sum = 379). | `test_tushare_throttle.py` (+1) | 1 | the DEFAULT retry budget survives a multi-failure transient outage (6 attempts ≈ 23s of capped exponential backoff; two real ~2h runs died on ConnectionError under the old 3-attempt ≈ 3s budget) | | **Total through P3-7** | **375** | | -## Per-file breakdown — Phase 3-8 CSI500 generalization (4) +## Per-file breakdown — Phase 3-8 CSI500 generalization (8) | Test file | Tests | Red-line / feature | |---|---|---| -| `test_csi500_generalization.py` | 4 | CSI500 config validates with the expected cell roles (screened anchor SSE50|2022-2024; independent SSE50|2024-2026 + 000905.SH|2024-2026; CSI500|2022-2024 skipped+disclosed; same groups/scenarios/hypotheses as P3-7 — no tuning); sample classes labeled correctly; **`output.subset_report_name`** lets each subset-validation study own its report file (default None keeps the historical `phase3_subset_validation.md` bitwise — the P3-6/P3-7 configs are locked unchanged), so a P3-8 run never clobbers the accepted P3-7 artifact | -| **Total (P0 + P1 + P2-1..P2-4 + P3-1..P3-8)** | **379** | | +| `test_csi500_generalization.py` | 8 | CSI500 config validates with the expected cell roles (screened anchor SSE50|2022-2024; independent SSE50|2024-2026 + 000905.SH|2024-2026; CSI500|2022-2024 skipped+disclosed; same groups/scenarios/hypotheses as P3-7 — no tuning); sample classes labeled correctly; **`output.subset_report_name`** lets each subset-validation study own its report file (default None keeps the historical `phase3_subset_validation.md` bitwise — the P3-6/P3-7 configs are locked unchanged), so a P3-8 run never clobbers the accepted P3-7 artifact; **`output.subset_report_title`** config-drives the report H1 so the CSI500 study names itself ("Phase 3-8 — CSI500 Independent Generalization Check", asserted NOT to start with Phase 3-7) while the P3-6/P3-7 configs leave it unset and keep the renderer's sample-aware default title (regression-locked) | +| **Total (P0 + P1 + P2-1..P2-4 + P3-1..P3-8)** | **383** | | ## Real-data validation (manual, not in CI — TEST-002 keeps the suite network-free) diff --git a/config/phase3_real_csi500_generalization.yaml b/config/phase3_real_csi500_generalization.yaml index 1a272b9..b1fca6e 100644 --- a/config/phase3_real_csi500_generalization.yaml +++ b/config/phase3_real_csi500_generalization.yaml @@ -150,6 +150,10 @@ output: log_dir: artifacts/logs overwrite: true subset_report_name: phase3_csi500_generalization.md + # The report reuses the P3-7 independent-validation machinery, so its H1 must + # name THIS study (not the default P3-7 phase label) — config owns the study + # identity, the same precedent as subset_report_name. + subset_report_title: "Phase 3-8 — CSI500 Independent Generalization Check (value/lowvol, screened vs independent holdout cells)" oos: split_date: "2023-07-01" # base-config validation only; per-cell splits below diff --git a/qt/config.py b/qt/config.py index a468c65..837bb4a 100644 --- a/qt/config.py +++ b/qt/config.py @@ -187,6 +187,12 @@ class OutputCfg(_Strict): # own name so different studies sharing the run mode never overwrite each # other's report (P3-8; the same precedent as baseline_report_name). subset_report_name: str | None = None + # H1 title for the subset-validation report (run-phase3-subset). None keeps + # the renderer's sample-aware default (P3-7 independent / P3-6 post-hoc); a + # config that reuses the run mode for a DIFFERENT study (e.g. the P3-8 CSI500 + # generalization check) sets its own title so the report header names the + # actual study instead of the machinery's default phase label (P3-8). + subset_report_title: str | None = None class OOSCfg(_Strict): diff --git a/qt/reports.py b/qt/reports.py index b9c095d..3ea8ae5 100644 --- a/qt/reports.py +++ b/qt/reports.py @@ -990,18 +990,26 @@ def _subset_group_summary_block(glabel: str, gsum: dict, level: str = "###") -> def render_subset_validation(result) -> str: """Build the phase3 subset-validation markdown (pure; no I/O, no secrets). - The title, the framing blockquote and the closing caveat are SAMPLE-AWARE: - a run whose cells include declared independent holdouts is a P3-7 + The H1 title, the framing blockquote and the closing caveat are + SAMPLE-AWARE: a run whose cells include declared independent holdouts is an independent validation and must NOT carry the P3-6 "same windows / not independent confirmation" framing (it would contradict the verdict section); a run with no independent cells keeps the P3-6 framing verbatim. + The H1 title is additionally CONFIG-DRIVEN: ``output.subset_report_title`` + overrides it so a config that reuses this run mode for a DIFFERENT study + (e.g. the P3-8 CSI500 generalization check) names the actual study instead + of the machinery's default phase label. The body framing stays sample-aware + (it describes screened-vs-independent mechanics, correct for any such study). """ cfg = result.config has_independent = "independent" in ( getattr(result, "cell_samples", None) or {} ).values() lines: list[str] = [] - if has_independent: + configured_title = getattr(cfg.output, "subset_report_title", None) + if configured_title: + lines.append(f"# {configured_title}\n") + elif has_independent: lines.append("# Phase 3-7 — Independent-Sample Validation " "(factor groups × cost scenarios; screened vs " "independent holdout cells)\n") diff --git a/tests/test_csi500_generalization.py b/tests/test_csi500_generalization.py index 07b929e..897d0f5 100644 --- a/tests/test_csi500_generalization.py +++ b/tests/test_csi500_generalization.py @@ -82,3 +82,58 @@ def test_csi500_config_owns_its_report_filename(): name = subset_report_filename(cfg) assert name == "phase3_csi500_generalization.md" assert name != "phase3_subset_validation.md" + + +def test_csi500_config_sets_its_report_title(): + cfg = load_config(_CSI500_CONFIG) + assert cfg.output.subset_report_title is not None + assert "Phase 3-8" in cfg.output.subset_report_title + assert "CSI500" in cfg.output.subset_report_title + + +def test_p36_p37_configs_leave_report_title_unset(): + """The pre-P3-8 configs must keep the renderer's sample-aware default title.""" + p36 = load_config(str( + Path(__file__).resolve().parents[1] + / "config" / "phase3_real_subset_costs.yaml" + )) + p37 = load_config(_P37_CONFIG) + assert p36.output.subset_report_title is None + assert p37.output.subset_report_title is None + + +def _csi500_synthetic_result(): + """A P3-7-shaped synthetic result re-pointed at the CSI500 config (which + sets subset_report_title) — exercises the config-driven H1 title.""" + import dataclasses + + from tests.test_independent_validation import _synthetic_independent_result + + return dataclasses.replace( + _synthetic_independent_result(), config=load_config(_CSI500_CONFIG) + ) + + +def test_csi500_report_title_is_p38_not_p37(): + """The generated CSI500 report's first line must name THIS study, not the + default P3-7 phase label (the review finding: hardcoded P3-7 title).""" + from qt.reports import render_subset_validation + + md = render_subset_validation(_csi500_synthetic_result()) + first_line = md.split("\n", 1)[0] + assert first_line.startswith("# Phase 3-8") + assert "CSI500" in first_line + assert "Phase 3-7" not in first_line + # the body framing stays sample-aware (screened vs independent) — still correct + assert "independent holdout" in md.lower() and "screened" in md.lower() + assert "## Independent holdout verdict" in md + + +def test_default_subset_title_preserved_without_config_override(): + """A result whose config sets no subset_report_title keeps the sample-aware + default (P3-7 for an independent run) — regression guard for P3-6/P3-7.""" + from qt.reports import render_subset_validation + from tests.test_independent_validation import _synthetic_independent_result + + md = render_subset_validation(_synthetic_independent_result()) + assert md.split("\n", 1)[0].startswith("# Phase 3-7")