From 31da639eb0c406d5532d1b928f7426bbdb0bcbfd Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Tue, 21 Jul 2026 15:39:19 -0700 Subject: [PATCH 1/2] fix(config): sweep every remaining stale description of the intraday gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review of #78 asked whether its guard was too narrow. It was, in a way worse than "a reworded sentence escapes the regex": the guard scanned only the two report modules it had just fixed, so SIX live instances of the original defect were structurally unreachable by it -- one in qt/config.py and five in the I5b/c/d/e/f config YAMLs. Those YAML comments are what an operator reads before enabling price_limit_check, a higher-stakes readership than a docstring. A guard scoped to the files you already fixed can only ever confirm what you already know. Widening it to the whole feature surface immediately found six MORE stale sites of a related kind -- "execute at the first valid 1min CLOSE in [14:51, 14:56:59]" in the I5a/I5b/I5f configs and the tail-framework module docstring, describing the FILL price, which PR #75 also moved to the bar VWAP. Twelve sites total: seven describing the gate, five describing the fill. Two restatements survive by necessity and now say so. runtime/backtest/ event_models.py and qt/config.py both sit UPSTREAM of qt.intraday_tail_framework in the import graph -- it imports them -- so neither can call limit_basis_phrase without a cycle. Both now point at it instead of restating it, and the scan is the net under them. Deliberately NOT swept, and checked individually: RUNBOOK.md and CLAUDE.md's "涨跌停用未复权 raw close 比 stk_limit" describe the DAILY universe tradability filter (universe/filters.py, data/clean/tradability.py) -- a separate, older mechanism that genuinely does compare a raw daily close. Sweeping those would have replaced a true statement with a false one. The test's docstring states its scope honestly: it catches "close" written near "execution minute", not a reworded synonym. Review proved seven such escapes and no lexical guard can close that. Composition is the answer where the import graph allows it; this scan covers the two places it does not. pytest 1776 passed, ruff clean, 31/31 configs validate, phase0 unchanged. --- config/phase_i5a_intraday_tail_framework.yaml | 3 +- ...se_i5b_intraday_execution_feasibility.yaml | 17 ++-- config/phase_i5c_mmp_minute_factor.yaml | 3 +- config/phase_i5d_mmp_quintile_5y.yaml | 5 +- ...5e_mmp_quintile_csi300_generalization.yaml | 5 +- ...se_i5f_intraday_liquidity_diagnostics.yaml | 5 +- qt/config.py | 7 +- qt/intraday_tail_framework.py | 3 +- runtime/backtest/event_models.py | 9 ++ tests/test_i5b_execution_feasibility.py | 91 ++++++++++--------- 10 files changed, 88 insertions(+), 60 deletions(-) diff --git a/config/phase_i5a_intraday_tail_framework.yaml b/config/phase_i5a_intraday_tail_framework.yaml index fdc9bc7..19a2bfa 100644 --- a/config/phase_i5a_intraday_tail_framework.yaml +++ b/config/phase_i5a_intraday_tail_framework.yaml @@ -2,7 +2,8 @@ # # Proves the shared event-driven backtest engine drives an IntradayTailEventModel # end-to-end on REAL SH/SZ minute data: decision at 14:50, execution at the first -# valid 1min close in [14:51, 14:56:59], exec-to-exec holding returns. The score is +# valid 1min bar in [14:51, 14:56:59] priced on the active execution_price_basis +# (the bar VWAP by default), exec-to-exec holding returns. The score is # a single PIT-safe I3 feature (intraday_ret_0930_1450), not a research alpha. # # Minute bars are read from the EXISTING intraday cache only (read-only; a cache diff --git a/config/phase_i5b_intraday_execution_feasibility.yaml b/config/phase_i5b_intraday_execution_feasibility.yaml index 590273b..5f810de 100644 --- a/config/phase_i5b_intraday_execution_feasibility.yaml +++ b/config/phase_i5b_intraday_execution_feasibility.yaml @@ -1,11 +1,12 @@ # Phase I5b — intraday execution-time price-limit feasibility (hardening, NOT research). # # Same intraday tail-rebalance smoke as I5a (decision 14:50, execute at the first -# valid 1min close in [14:51, 14:56:59], exec-to-exec holding returns, PIT-safe I3 +# valid 1min bar in [14:51, 14:56:59] priced on the active execution_price_basis, exec-to-exec holding returns, PIT-safe I3 # score) PLUS execution-time price-limit feasibility: a buy is blocked at the raw -# upper limit and a sell at the raw lower limit, comparing the selected -# execution-minute RAW 1min close to the raw stk_limit band (NEVER qfq / daily -# close / daily-close-derived flags). +# upper limit and a sell at the raw lower limit, comparing the RAW price that +# EXECUTES at the selected minute -- the bar VWAP under the default +# execution_price_basis -- to the raw stk_limit band (NEVER qfq / daily close / +# daily-close-derived flags). # # Minute bars are read from the EXISTING intraday cache only (read-only; a miss is # a hard blocker -> zero stk_mins live calls). Raw stk_limit flows through the same @@ -42,8 +43,9 @@ universe: filters: # Selection-level daily tradability flags stay DISABLED: at the 14:50 decision # the daily-close-derived limit flag is an EOD value not known yet. The honest - # direction-aware blocking happens at EXECUTION via raw stk_limit vs the raw - # execution-minute close (intraday.price_limit_check below). Disclosed in the report. + # direction-aware blocking happens at EXECUTION via raw stk_limit vs the RAW + # price that EXECUTES at that minute (intraday.price_limit_check below). + # Disclosed in the report. missing_close: true suspended: false st: false @@ -95,7 +97,8 @@ intraday: - '14:56:59' require_cache_coverage: true missing_execution: block - # I5b: execution-time raw price-limit feasibility (raw 1min close vs raw stk_limit). + # I5b: execution-time raw price-limit feasibility (the RAW price that EXECUTES + # at that minute vs raw stk_limit). price_limit_check: true require_price_limit_coverage: true limit_tolerance: 1.0e-06 diff --git a/config/phase_i5c_mmp_minute_factor.yaml b/config/phase_i5c_mmp_minute_factor.yaml index e0378d8..14f4ec7 100644 --- a/config/phase_i5c_mmp_minute_factor.yaml +++ b/config/phase_i5c_mmp_minute_factor.yaml @@ -42,7 +42,8 @@ universe: filters: # Selection-level daily tradability flags stay DISABLED (as in I5b): the daily # close limit flag is an EOD value not known at 14:50. The honest direction-aware - # blocking is execution-time raw stk_limit vs the raw execution-minute close. + # blocking is execution-time raw stk_limit vs the RAW price that EXECUTES at that minute + # (the bar VWAP under the default execution_price_basis). missing_close: true suspended: false st: false diff --git a/config/phase_i5d_mmp_quintile_5y.yaml b/config/phase_i5d_mmp_quintile_5y.yaml index cf42d12..0c8cf9a 100644 --- a/config/phase_i5d_mmp_quintile_5y.yaml +++ b/config/phase_i5d_mmp_quintile_5y.yaml @@ -42,8 +42,9 @@ universe: filters: # Selection-level daily tradability flags stay DISABLED (as in I5b/I5c): the # daily close limit flag is an EOD value not known at 14:50. The honest - # direction-aware blocking is execution-time raw stk_limit vs the raw - # execution-minute close. + # direction-aware blocking is execution-time raw stk_limit vs the RAW price + # that EXECUTES at that minute (the bar VWAP under the default + # execution_price_basis). missing_close: true suspended: false st: false diff --git a/config/phase_i5e_mmp_quintile_csi300_generalization.yaml b/config/phase_i5e_mmp_quintile_csi300_generalization.yaml index 511a3e4..b3726b1 100644 --- a/config/phase_i5e_mmp_quintile_csi300_generalization.yaml +++ b/config/phase_i5e_mmp_quintile_csi300_generalization.yaml @@ -44,8 +44,9 @@ universe: filters: # Selection-level daily tradability flags stay DISABLED (as in I5b/I5c/I5d): the # daily close limit flag is an EOD value not known at 14:50. The honest - # direction-aware blocking is execution-time raw stk_limit vs the raw - # execution-minute close. + # direction-aware blocking is execution-time raw stk_limit vs the RAW price + # that EXECUTES at that minute (the bar VWAP under the default + # execution_price_basis). missing_close: true suspended: false st: false diff --git a/config/phase_i5f_intraday_liquidity_diagnostics.yaml b/config/phase_i5f_intraday_liquidity_diagnostics.yaml index 952875f..113cfdb 100644 --- a/config/phase_i5f_intraday_liquidity_diagnostics.yaml +++ b/config/phase_i5f_intraday_liquidity_diagnostics.yaml @@ -1,7 +1,7 @@ # Phase I5f — intraday execution liquidity diagnostics (REPORT-ONLY, NOT research). # # Same intraday tail-rebalance smoke as I5a/I5b (decision 14:50, execute at the -# first valid 1min close in [14:51, 14:56:59], exec-to-exec holding returns, +# first valid 1min bar in [14:51, 14:56:59] priced on the active execution_price_basis, exec-to-exec holding returns, # PIT-safe I3 score, raw stk_limit execution-time feasibility ON) PLUS an opt-in, # REPORT-ONLY execution liquidity diagnostic. For each desired rebalance trade # (|target_weight - current_weight|) it sizes the trade against the SELECTED @@ -48,7 +48,8 @@ universe: filters: # Selection-level daily tradability flags stay DISABLED (as in I5b): the daily # close limit flag is an EOD value not known at 14:50. The honest direction-aware - # blocking is execution-time raw stk_limit vs the raw execution-minute close. + # blocking is execution-time raw stk_limit vs the RAW price that EXECUTES at that minute + # (the bar VWAP under the default execution_price_basis). missing_close: true suspended: false st: false diff --git a/qt/config.py b/qt/config.py index 2ee17e9..93cac22 100644 --- a/qt/config.py +++ b/qt/config.py @@ -340,8 +340,11 @@ class IntradayCfg(_Strict): # I5b execution-time price-limit feasibility. OFF by default, so every I5a / # daily config validates and behaves unchanged. When enabled, the intraday # tail model gates buys at the raw upper limit and sells at the raw lower - # limit, comparing the selected execution-minute RAW close to raw ``stk_limit`` - # (never qfq / daily close). ``require_price_limit_coverage`` makes a missing + # limit, comparing the RAW price that EXECUTES at the selected minute -- the + # bar VWAP under the default ``execution_price_basis`` -- to raw ``stk_limit`` + # (never qfq / daily close; see qt.intraday_tail_framework.limit_basis_phrase, + # the single authored statement of this, which this module cannot import + # without a cycle). ``require_price_limit_coverage`` makes a missing # required (anchor date, symbol) limit row a hard, pre-result failure instead # of a silent "checked" pass; ``limit_tolerance`` is the raw-price equality # band for the at-limit test. diff --git a/qt/intraday_tail_framework.py b/qt/intraday_tail_framework.py index cb263f3..4d1efcd 100644 --- a/qt/intraday_tail_framework.py +++ b/qt/intraday_tail_framework.py @@ -13,7 +13,8 @@ -> deterministic PIT-safe score = the I3 ``intraday_ret_0930_1450`` feature (only bars with available_time <= 14:50 enter it) -> engine + IntradayTailEventModel: decision 14:50, execute at the first valid - 1min close in [14:51, 14:56:59], exec-to-exec holding returns + 1min bar in [14:51, 14:56:59] priced on the active execution_price_basis + (the bar VWAP by default), exec-to-exec holding returns -> NAV / feasibility / holdings / event logs -> markdown report. It is intentionally small: the score is a single already-PIT-safe feature solely diff --git a/runtime/backtest/event_models.py b/runtime/backtest/event_models.py index 86b634b..330070e 100644 --- a/runtime/backtest/event_models.py +++ b/runtime/backtest/event_models.py @@ -132,6 +132,15 @@ class IntradayTailEventModel: symbol/date's raw ``stk_limit`` band. A buy is blocked at the upper limit, a sell at the lower limit, directionally. + That sentence is authored once, in + ``qt.intraday_tail_framework.limit_basis_phrase``, and every report composes + it. This docstring cannot import it — ``qt.intraday_tail_framework`` already + imports this class, so the reverse would cycle — so it is one of the two + restatements that survive by necessity. It is covered instead by the repo-wide + scan in ``tests/test_i5b_execution_feasibility.py``, which fails if any file + claims this gate compares a CLOSE. Change the behaviour and that scan will + point here. + Why the gate reads the VWAP and not the bar close. A limit-up execution minute has exactly two shapes, and they are the feasibility question: diff --git a/tests/test_i5b_execution_feasibility.py b/tests/test_i5b_execution_feasibility.py index 775c366..07d0246 100644 --- a/tests/test_i5b_execution_feasibility.py +++ b/tests/test_i5b_execution_feasibility.py @@ -476,55 +476,62 @@ def test_group_report_feasibility_prose_names_the_active_execution_basis(): assert "adj_factor" in text -def test_no_module_repeats_the_pre_pr75_close_based_limit_claim(): - """Guard the DEFECT CLASS, not just the two places already fixed. - - The gate input moved from the bar close to the executed price in PR #75. The - first correction pass fixed the prose in the two obvious spots and MISSED a - third copy in `_write_report`'s "Limitations" section, so a run would have - emitted two contradictory descriptions of the same check in one document. - - Review found it, and found why the other tests could not: none of them render - `_write_report` at all, so that location was structurally unreachable. - - SCOPE, stated honestly because the first version of this docstring overstated - it. This catches a LITERAL revert -- a bad merge or rebase restoring the old - sentence, which is the failure that actually happened. It does NOT catch a - reworded restatement: review wrote seven plausible ones ("its last print", - "the final tick", "1-minute" for "1min") and all seven escape this regex. No - lexical guard can assert "no other sentence makes this claim"; English has too - many ways to make it. The durable fix is structural and is the reason - `limit_basis_phrase` exists -- with one authored sentence there is no second - one to reword. This test guards that one sentence against being un-written. +def test_no_file_anywhere_claims_the_gate_compares_a_close(): + """Repo-wide: NO file may state that the I5b gate compares a close. + + The first version of this scanned only the two report modules. Review then + found SIX more live instances it could not reach even in principle -- one in + `qt/config.py` and five in the I5b/c/d/e/f config YAMLs, which are exactly what + an operator reads before enabling the feature, a higher-stakes readership than + a docstring. Scoping a guard to the files you already fixed guarantees it only + ever confirms what you already know. + + Two restatements survive by necessity and are covered by this scan rather than + by composition: `runtime/backtest/event_models.py` and `qt/config.py` both sit + UPSTREAM of `qt.intraday_tail_framework` in the import graph (it imports them), + so neither can call `limit_basis_phrase` without a cycle. + + Deliberately NOT matched: `bar_close` the basis name, "that bar's single + closing tick" the basis description, and the DAILY universe tradability filter + (`universe/filters.py`, `data/clean/tradability.py`), which is a separate and + older mechanism that genuinely does compare a raw daily close -- see + RUNBOOK.md. Those are correct and must not be swept up. + + Scope, stated honestly: this catches a claim written with the word "close" + near "execution minute". It does NOT catch a reworded synonym ("its last + print", "the final tick") -- review demonstrated seven such escapes. No lexical + guard can assert "no sentence anywhere makes this claim". Composition is the + real answer where the import graph allows it; this scan is the net under the + places where it does not. """ import re from pathlib import Path - import qt.intraday_group_report as gr - import qt.intraday_tail_framework as tf - - # Phrasings that assert the LIMIT COMPARISON reads a close. "bar_close" the - # basis name and "that bar's single closing tick" the basis description are - # both legitimate and deliberately not matched. + root = Path(__file__).resolve().parent.parent forbidden = re.compile( - r"execution-minute\s+(\*\*raw\*\*\s+)?(1min\s+)?close" - r"|1min\s+close\s+to\s+the\s+raw" - r"|raw\s+execution-minute\s+close", + r"execution[- ]minute[^.\n]{0,40}\bclose\b" + r"|\b1min close\b" + r"|raw\s+close\s+to\s+(the\s+)?raw\s+`?`?stk_limit", re.IGNORECASE, ) - for mod in (tf, gr): - src = Path(mod.__file__).read_text() - hits = [ - f"line {i}: {ln.strip()}" - for i, ln in enumerate(src.splitlines(), 1) - if forbidden.search(ln) - ] - assert not hits, ( - f"{Path(mod.__file__).name} still claims the price-limit gate compares " - f"a CLOSE. The gate reads the price that executes (the bar VWAP under " - f"the default basis). Derive the wording from execution_price_basis " - f"instead of restating it:\n " + "\n ".join(hits) - ) + targets = [root / "qt" / "config.py", *sorted((root / "config").glob("*.yaml"))] + targets += sorted((root / "qt").glob("intraday*.py")) + targets += [root / "runtime" / "backtest" / "event_models.py", + root / "runtime" / "intraday_execution.py"] + + hits = [] + for f in targets: + if not f.exists(): + continue + for i, line in enumerate(f.read_text().splitlines(), 1): + if forbidden.search(line): + hits.append(f"{f.relative_to(root)}:{i}: {line.strip()}") + assert not hits, ( + "these files claim the I5b price-limit gate compares a CLOSE. It compares " + "the price that EXECUTES (the bar VWAP under the default basis). Where the " + "import graph allows it, compose limit_basis_phrase; where it does not, " + "point at it:\n " + "\n ".join(hits) + ) def test_every_gate_description_composes_the_single_phrase(): From 1b6ff2c2aaa25277b6064be7caf400db85ace838 Mon Sep 17 00:00:00 2001 From: shaofl <2899218482@qq.com> Date: Tue, 21 Jul 2026 15:44:55 -0700 Subject: [PATCH 2/2] test(qt): scan the exec-basis modules before they can go stale Review of #82 verified the sweep independently -- 12 pre-fix hits, 0 post-fix, line numbers matching -- and noted that #79's qt/exec_basis_eval.py, qt/exec_basis_sanity.py and qt/exec_forward_returns.py describe the same execution anchor from the factor side and were not in the scan's target list. They are accurate today; it read them. Adding them anyway. The entire reason this guard had to be widened twice was that its first version covered only the files already fixed, which meant it could confirm nothing except what was already known. Waiting for these three to go stale before listing them would repeat that exactly. No behaviour change; the scan passes with them included. --- tests/test_i5b_execution_feasibility.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_i5b_execution_feasibility.py b/tests/test_i5b_execution_feasibility.py index 07d0246..16267b4 100644 --- a/tests/test_i5b_execution_feasibility.py +++ b/tests/test_i5b_execution_feasibility.py @@ -518,6 +518,11 @@ def test_no_file_anywhere_claims_the_gate_compares_a_close(): targets += sorted((root / "qt").glob("intraday*.py")) targets += [root / "runtime" / "backtest" / "event_models.py", root / "runtime" / "intraday_execution.py"] + # The exec-basis modules describe the same execution anchor from the factor + # side. They are accurate today; they are listed BEFORE they go stale, which + # is the whole lesson of how this guard had to be widened in the first place. + targets += sorted((root / "qt").glob("exec_basis*.py")) + targets += [root / "qt" / "exec_forward_returns.py"] hits = [] for f in targets: