diff --git a/qt/intraday_group_report.py b/qt/intraday_group_report.py index b641fb4..4d721f5 100644 --- a/qt/intraday_group_report.py +++ b/qt/intraday_group_report.py @@ -19,6 +19,7 @@ plot_quintile_nav, plot_spread_curve, ) +from qt.intraday_tail_framework import limit_basis_phrase def _write_figures( @@ -184,10 +185,10 @@ def _feasibility_lines(result) -> list[str]: cov = result.limit_coverage lines.extend([ "- **enabled** (`intraday.price_limit_check=true`): a buy is blocked at the " - "raw upper limit and a sell at the raw lower limit, comparing the price " - f"that actually EXECUTES — the selected execution minute on the " - f"`{cfg.intraday.execution_price_basis}` basis — to the raw `stk_limit` " - "band (RAW-vs-RAW; never qfq / daily close / a daily-close-derived flag).", + "raw upper limit and a sell at the raw lower limit, comparing " + f"{limit_basis_phrase(cfg.intraday.execution_price_basis)} — to the raw " + "`stk_limit` band (RAW-vs-RAW; never qfq / daily close / a " + "daily-close-derived flag).", "- a limit-up minute is LOCKED (every print at the limit, VWAP equals it up " "to rounding, buy blocked) or OPENED (prints landed below it, which is " "evidence a fill was achievable, buy allowed). The executed price separates " diff --git a/qt/intraday_tail_framework.py b/qt/intraday_tail_framework.py index 3c26c0a..cb263f3 100644 --- a/qt/intraday_tail_framework.py +++ b/qt/intraday_tail_framework.py @@ -259,9 +259,9 @@ def _load_price_limits( Returns ``(limits_df, stk_limit_gap_fetches)``. The limits flow through the SAME P4 read-through cache as the daily endpoints (no new endpoint); a fully-covered window costs zero gap fetches. The cache stores RAW - ``up_limit`` / ``down_limit`` only — the model compares them to the RAW price - that EXECUTES at the selected minute (the bar VWAP under the default basis), - never qfq / daily close. + ``up_limit`` / ``down_limit`` only; for what they are compared against, see + :func:`limit_basis_phrase` — deliberately not restated here, because a second + statement of the same fact is a second thing to get wrong. """ if cfg.intraday is None or not cfg.intraday.price_limit_check: return None, 0 @@ -723,22 +723,37 @@ def _append_liquidity_section(lines: list[str], result: I5aResult) -> None: lines.append("") -def limit_basis_lines(execution_price_basis: str) -> list[str]: - """Report prose stating what the I5b price-limit gate ACTUALLY compares. +def limit_basis_phrase(execution_price_basis: str) -> str: + """THE ONE sentence fragment naming what the price-limit gate compares. + + Every place that describes the gate — the feasibility section, the report's + Limitations bullet, the group report, and any future one — composes this + rather than authoring its own sentence. - Extracted so it is testable. The gate reads the price that EXECUTES — which - since PR #75 is the bar VWAP by default, not the bar close — and an earlier - revision of this text kept claiming "the raw 1min close" after the gate input - had changed. A report that misstates the check it performed is worse than no - report, so the wording is derived from the active basis and pinned by test. + That indirection is the actual fix, and it replaced a weaker one. When PR #75 + moved the gate from the bar close to the executed price, three independently + written sentences described it; the first correction pass fixed two, and a + regex guard was added to catch the rest. Review then demonstrated that seven + plausible rewordings ("its last print", "the final tick", "1-minute" instead + of "1min") all escape that regex — which is the general fate of a lexical + guard aimed at a semantic claim. A regex cannot assert "no other sentence + says this"; having no other sentence can. The guard is kept, scoped to what + it genuinely does: catching a literal revert. """ + return ( + f"the price that actually EXECUTES — the selected execution minute on the " + f"`{execution_price_basis}` basis" + ) + + +def limit_basis_lines(execution_price_basis: str) -> list[str]: + """Report prose stating what the I5b price-limit gate ACTUALLY compares.""" return [ - f"- **comparison basis**: the price that actually EXECUTES — the selected " - f"execution minute on the `{execution_price_basis}` basis — vs the " - f"symbol/date **raw** `stk_limit` band. NOT qfq, NOT the daily close, NOT " - f"a daily-close-derived limit flag. (The intraday cache stores unadjusted " - f"bars and `stk_limit` is raw, and a bar VWAP is raw amount over raw " - f"volume, so the comparison is RAW-vs-RAW either way.)", + f"- **comparison basis**: {limit_basis_phrase(execution_price_basis)} — vs " + f"the symbol/date **raw** `stk_limit` band. NOT qfq, NOT the daily close, " + f"NOT a daily-close-derived limit flag. (The intraday cache stores " + f"unadjusted bars and `stk_limit` is raw, and a bar VWAP is raw amount " + f"over raw volume, so the comparison is RAW-vs-RAW either way.)", "- **why the executed price and not the bar close**: a limit-up minute has " "two shapes. LOCKED (封死涨停) — every print is at the limit, so the VWAP " "equals it up to rounding and the buy must be blocked. OPENED (盘中打开) — " @@ -962,10 +977,9 @@ def _write_report(result: I5aResult, *, elapsed: float) -> None: lines.append( f"- **Execution-time feasibility (I5b)**: a missing/NaN execution bar " f"blocks BOTH directions; on top of that, raw `stk_limit` blocks buys at " - f"the upper limit and sells at the lower limit, comparing the price that " - f"actually EXECUTES — the selected execution minute on the " - f"`{ec.execution_price_basis}` basis — to raw limits (see the section " - f"above). Remaining gap: only the price-limit and bar-existence " + f"the upper limit and sells at the lower limit, comparing " + f"{limit_basis_phrase(ec.execution_price_basis)} — to raw limits (see the " + f"section above). Remaining gap: only the price-limit and bar-existence " f"constraints are modeled; partial-fill / liquidity / volume caps at the " f"execution minute are not." ) diff --git a/tests/test_i5b_execution_feasibility.py b/tests/test_i5b_execution_feasibility.py index 7a9d3b7..775c366 100644 --- a/tests/test_i5b_execution_feasibility.py +++ b/tests/test_i5b_execution_feasibility.py @@ -485,9 +485,17 @@ def test_no_module_repeats_the_pre_pr75_close_based_limit_claim(): 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. The - honest fix for a claim that gets restated in prose in N places is to assert no - (N+1)th copy exists, rather than to enumerate the ones we happen to know. + `_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. """ import re from pathlib import Path @@ -517,3 +525,98 @@ def test_no_module_repeats_the_pre_pr75_close_based_limit_claim(): f"the default basis). Derive the wording from execution_price_basis " f"instead of restating it:\n " + "\n ".join(hits) ) + + +def test_every_gate_description_composes_the_single_phrase(): + """All three descriptions of the gate compose one phrase; none re-authors it. + + This is the guard the regex above cannot be: a lexical scan enumerates + wordings, whereas having exactly one authored sentence leaves nothing to + reword. If a future edit writes its own sentence instead of composing + `limit_basis_phrase`, that sentence stops containing the phrase and this + fails. + """ + from types import SimpleNamespace + + from qt.intraday_group_report import _feasibility_lines + from qt.intraday_tail_framework import limit_basis_lines, limit_basis_phrase + + cfg = load_config(str(_I5B_CONFIG)) + basis = cfg.intraday.execution_price_basis + phrase = limit_basis_phrase(basis) + assert f"`{basis}`" in phrase # the phrase is basis-derived, not a constant + + # 1. the tail report's feasibility section + assert phrase in " ".join(limit_basis_lines(basis)) + + # 2. the group report's feasibility section + result = SimpleNamespace( + config=cfg, + price_limit_check=True, + limit_coverage={"required": 1, "present": 1, "missing": 0}, + stk_limit_gap_fetches=0, + groups=( + SimpleNamespace( + group=1, up_limit_blocked_buys=0, down_limit_blocked_sells=0, + missing_limit_rows=0, opened_limit_up_minutes=0, + opened_limit_down_minutes=0, missing_adj_factor_pairs=0, + ), + ), + ) + assert phrase in " ".join(_feasibility_lines(result)) + + # 3. the tail report's Limitations bullet -- the one the first pass missed. + # Read from source rather than rendering, since building a full I5aResult + # here would test the dataclass, not the sentence. + import inspect + + import qt.intraday_tail_framework as tf + + src = inspect.getsource(tf._write_report) + assert "limit_basis_phrase(ec.execution_price_basis)" in src, ( + "the Limitations bullet must COMPOSE limit_basis_phrase, not restate the " + "comparison in its own words" + ) + + +def test_composed_gate_sentences_are_grammatical_in_every_context(): + """The phrase is an appositive: every host sentence must close its em-dash. + + Composing one fragment into several sentences trades three ways to state a + fact wrong for one way to punctuate it wrong. The first version of this + refactor did exactly that -- it dropped the CLOSING em-dash in two of the + three hosts, leaving "...on the `bar_vwap` basis to the raw stk_limit band" + with an unclosed dash. Rendered output was NOT byte-identical to the previous + release, contrary to what the change claimed, and a rendering probe caught it. + + So: wherever the phrase is embedded mid-sentence, the em-dash that opens the + appositive inside it must be matched by one after it. + """ + from types import SimpleNamespace + + from qt.intraday_group_report import _feasibility_lines + from qt.intraday_tail_framework import limit_basis_phrase + + cfg = load_config(str(_I5B_CONFIG)) + phrase = limit_basis_phrase(cfg.intraday.execution_price_basis) + assert "—" in phrase, "the phrase itself opens an appositive with an em-dash" + + result = SimpleNamespace( + config=cfg, price_limit_check=True, + limit_coverage={"required": 1, "present": 1, "missing": 0}, + stk_limit_gap_fetches=0, + groups=(SimpleNamespace( + group=1, up_limit_blocked_buys=0, down_limit_blocked_sells=0, + missing_limit_rows=0, opened_limit_up_minutes=0, + opened_limit_down_minutes=0, missing_adj_factor_pairs=0),), + ) + for line in _feasibility_lines(result): + if phrase in line: + tail = line.split(phrase, 1)[1] + assert tail.lstrip().startswith("—"), ( + f"the phrase is embedded mid-sentence but its appositive is never " + f"closed; the text reads '...{phrase[-30:]}{tail[:40]}'" + ) + break + else: + raise AssertionError("the group report no longer composes the phrase at all")