diff --git a/docs/research/regime-monitor-v4.md b/docs/research/regime-monitor-v4.md index 6eb0dec..4649cf6 100644 --- a/docs/research/regime-monitor-v4.md +++ b/docs/research/regime-monitor-v4.md @@ -10,9 +10,14 @@ gate entries, exits, position size, ranking, or alerts about individual setups. **v4 supersedes v3** (2026-08-08). Unlike v3, whose calibration was ad-hoc and never landed, every number below is reproducible: - .\.venv\Scripts\python.exe scripts -un_regime_monitor_calibration.py ^ - --methodology v2_reconstruction,v3,v4 --cache-dir .calib-cache +``` +.venv/Scripts/python.exe scripts/run_regime_monitor_calibration.py --methodology v2_reconstruction,v2_reconstruction_oas400,v3,v4,v4-vix-only,v4-p1-only --cache-dir .calib-cache +``` + +`v3` and `v4` are mandatory — the row-wise `state_v4 <= state_v3` invariant is +a hard gate and needs both — and the replayed **start** date is asserted +against the published window. The session *count* alone proves nothing, since +the harness slices the tail of the price series to whatever was asked for. The harness replays the 408 sessions ending 2026-07-24 from the live inputs (Alpaca for all 33 symbols, FRED for VIX and HY OAS) with no database, and @@ -244,11 +249,31 @@ Every distribution above inherits that bias. It is the same bias v2 carried, so the v2/v3 comparison is like-for-like, but the absolute band shares are optimistic. +**Which OAS window the published v2 figures used.** v2 requested 13 years of HY +OAS and sliced `HY_OAS_REFERENCE_YEARS = 10.0` per session; ICE serves only ~3 +years (778 observations from 2023-08-08), so the effective window was that. But +production v2 also fetched only 400 *calendar* days at one point — the bug fixed +2026-08-07 — and whether the published numbers predate that was not recoverable +from the text. Settled by replay rather than assumed: the +`v2_reconstruction_oas400` variant truncates the OAS **source series** to 400 +days (patching the per-session window cannot simulate data that was simply +absent) and yields avg 26.54, p80 42.52, max **100.00**, against published +22.6 / 35.1 / 91.2. Full coverage reproduces all three. So the published figures +correspond to the untruncated fetch. + +**The top VIX anchors are exercised, not just asserted.** The window contains a +52.33 close (2025-04-08), so the 40 → 80 → 55 → 100 segment is fed by real data +rather than justified from long-run history alone. + ## Point-in-time record -The first run under a new `METHODOLOGY` rebuilds the latest 400 trading sessions -with sufficient sensor warm-up; routine runs thereafter insert/update only the -latest trading date. The history API and main chart show only snapshots matching +The first run under a new `METHODOLOGY` rebuilds every session inside +`REBUILD_LOOKBACK_DAYS` — 672 calendar days, roughly 464 trading sessions; +routine runs thereafter insert/update only the latest trading date. The bound is +in calendar days rather than a session count because the binding constraint is +the OAS fetch: each replayed row needs W3's lookback inside +`HY_OAS_WINDOW_DAYS`, so replaying further back would recreate the credit gap a +reseed exists to close. The history API and main chart show only snapshots matching the current methodology, so a bump reseeds the series rather than splicing two formulas into one line. diff --git a/reports/regime-monitor-v4-calibration-20260808-201931.json b/reports/regime-monitor-v4-calibration-20260808-201931.json deleted file mode 100644 index 7b9a35c..0000000 --- a/reports/regime-monitor-v4-calibration-20260808-201931.json +++ /dev/null @@ -1,1414 +0,0 @@ -{ - "generated_at": "2026-08-08T20:19:31", - "git_rev": "c3ae5ad", - "params": { - "end": "2026-07-24", - "sessions": 408, - "history_days": 1200, - "oas_window_days": 700, - "oas_fetch_days": 4748, - "methodology": "v2_reconstruction,v3,v4,v4-vix-b,v4-p1-capped", - "expected_first_session": null, - "cache_dir": ".calib-cache", - "out": null, - "quiet": true - }, - "provenance": { - "symbols": { - "AAPL": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "MSFT": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "NVDA": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "AMZN": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "META": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "GOOGL": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "AVGO": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "AMD": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "ORCL": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "CRM": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "NOW": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "PLTR": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "ANET": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "DELL": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "SMCI": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "MU": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "QCOM": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "INTC": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "AMAT": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "LRCX": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "KLAC": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "SNPS": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "CDNS": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "ADI": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "TXN": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "IBM": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "CSCO": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "PANW": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "CRWD": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "VRT": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "SMH": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "QQQ": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "SPY": { - "bars": 825, - "first": "2023-04-11", - "last": "2026-07-24" - } - }, - "vix": { - "points": 850, - "first": "2023-04-11", - "last": "2026-07-24" - }, - "oas": { - "points": 778, - "first": "2023-08-08", - "last": "2026-07-24" - }, - "scored_range": [ - "2024-12-05", - "2026-07-24" - ] - }, - "hard_gates": [ - { - "gate": "symbols_fetched", - "expected": 33, - "measured": 33, - "passed": true, - "detail": [] - }, - { - "gate": "per_symbol_warmup_252_bars", - "expected": "all", - "measured": 33, - "passed": true, - "detail": [] - }, - { - "gate": "per_symbol_reaches_last_session", - "expected": "2026-07-24", - "measured": 33, - "passed": true, - "detail": [] - }, - { - "gate": "breadth_counts_full_basket", - "expected": 30, - "measured": "408/408 sessions", - "passed": true, - "detail": [] - }, - { - "gate": "sessions_scored", - "expected": 408, - "measured": 408, - "passed": true - }, - { - "gate": "last_scored_date", - "expected": "2026-07-24", - "measured": "2026-07-24", - "passed": true - }, - { - "gate": "w1_available_every_session", - "expected": 408, - "measured": 408, - "passed": true - }, - { - "gate": "state_coverage_100_every_row", - "expected": 0, - "measured": 0, - "passed": true, - "detail": [] - }, - { - "gate": "no_stale_inputs", - "expected": 0, - "measured": 0, - "passed": true, - "detail": [] - }, - { - "gate": "state_v4_le_v3_every_row", - "expected": 0, - "measured": 0, - "passed": true, - "detail": [] - } - ], - "blocked_by": [], - "provisional": false, - "variants": { - "v2_reconstruction": { - "label": "v2_reconstruction", - "sessions": 408, - "state": { - "avg": 22.68, - "median": 16.15, - "p80": 35.1, - "p90": 65.63, - "max": 91.2, - "scored": 408 - }, - "warning_avg": 11.65, - "saturation_census": { - "p3_pegged": 39, - "p2_pegged": 0, - "v1_pegged": 14, - "p1_pegged": 46 - }, - "price_argmax_sole": { - "P1": 9, - "P2": 18, - "P3": 308 - }, - "price_argmax_tie_inclusive": { - "P1": 82, - "P2": 53, - "P3": 381 - }, - "price_argmax_ties": 73, - "w1_nonzero_sessions": 108, - "band_shares_current": { - "stable": 57.1, - "watch": 30.1, - "elevated": 1.7, - "breaking": 11.0 - }, - "soft_gates": [ - { - "gate": "v2_state_avg", - "expected": 22.6, - "measured": 22.68, - "tolerance": 0.3, - "passed": true - }, - { - "gate": "v2_state_p80", - "expected": 35.1, - "measured": 35.1, - "tolerance": 0.5, - "passed": true - }, - { - "gate": "v2_state_max", - "expected": 91.2, - "measured": 91.2, - "tolerance": 0.5, - "passed": true - }, - { - "gate": "v2_p3_pegged", - "expected": 39, - "measured": 39, - "tolerance": 0, - "passed": true - }, - { - "gate": "w1_live_sessions", - "expected": 108, - "measured": 108, - "tolerance": 0, - "passed": true - } - ] - }, - "v3": { - "label": "v3", - "sessions": 408, - "state": { - "avg": 18.13, - "median": 9.1, - "p80": 31.36, - "p90": 65.0, - "max": 87.4, - "scored": 408 - }, - "warning_avg": 14.88, - "saturation_census": { - "p3_pegged": 0, - "p2_pegged": 0, - "v1_pegged": 14, - "p1_pegged": 46 - }, - "price_argmax_sole": { - "P1": 71, - "P2": 33, - "P3": 268 - }, - "price_argmax_tie_inclusive": { - "P1": 107, - "P2": 69, - "P3": 304 - }, - "price_argmax_ties": 36, - "w1_nonzero_sessions": 197, - "band_shares_current": { - "stable": 73.0, - "watch": 15.4, - "elevated": 1.2, - "breaking": 10.3 - }, - "soft_gates": [ - { - "gate": "v3_state_max", - "expected": 87.4, - "measured": 87.4, - "tolerance": 0.5, - "passed": true - } - ] - }, - "v4": { - "label": "v4", - "sessions": 408, - "state": { - "avg": 14.78, - "median": 8.35, - "p80": 21.7, - "p90": 43.63, - "max": 83.5, - "scored": 408 - }, - "warning_avg": 14.88, - "saturation_census": { - "p3_pegged": 0, - "p2_pegged": 0, - "v1_pegged": 0, - "p1_pegged": 0 - }, - "price_argmax_sole": { - "P1": 17, - "P2": 48, - "P3": 307 - }, - "price_argmax_tie_inclusive": { - "P1": 53, - "P2": 84, - "P3": 343 - }, - "price_argmax_ties": 36, - "w1_nonzero_sessions": 197, - "band_shares_current": { - "stable": 78.9, - "watch": 13.0, - "elevated": 4.7, - "breaking": 3.4 - }, - "soft_gates": [], - "band_grid": [ - { - "bands": [ - 15.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 68.9, - "watch": 19.6, - "elevated": 6.4, - "breaking": 5.1 - } - }, - { - "bands": [ - 15.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 68.9, - "watch": 19.6, - "elevated": 8.1, - "breaking": 3.4 - } - }, - { - "bands": [ - 15.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 68.9, - "watch": 19.6, - "elevated": 10.3, - "breaking": 1.2 - } - }, - { - "bands": [ - 15.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 68.9, - "watch": 23.0, - "elevated": 2.9, - "breaking": 5.1 - } - }, - { - "bands": [ - 15.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 68.9, - "watch": 23.0, - "elevated": 4.7, - "breaking": 3.4 - } - }, - { - "bands": [ - 15.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 68.9, - "watch": 23.0, - "elevated": 6.9, - "breaking": 1.2 - } - }, - { - "bands": [ - 20.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 78.9, - "watch": 9.6, - "elevated": 6.4, - "breaking": 5.1 - } - }, - { - "bands": [ - 20.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 78.9, - "watch": 9.6, - "elevated": 8.1, - "breaking": 3.4 - } - }, - { - "bands": [ - 20.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 78.9, - "watch": 9.6, - "elevated": 10.3, - "breaking": 1.2 - } - }, - { - "bands": [ - 20.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 78.9, - "watch": 13.0, - "elevated": 2.9, - "breaking": 5.1 - } - }, - { - "bands": [ - 20.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 78.9, - "watch": 13.0, - "elevated": 4.7, - "breaking": 3.4 - } - }, - { - "bands": [ - 20.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 78.9, - "watch": 13.0, - "elevated": 6.9, - "breaking": 1.2 - } - }, - { - "bands": [ - 25.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 82.1, - "watch": 6.4, - "elevated": 6.4, - "breaking": 5.1 - } - }, - { - "bands": [ - 25.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 82.1, - "watch": 6.4, - "elevated": 8.1, - "breaking": 3.4 - } - }, - { - "bands": [ - 25.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 82.1, - "watch": 6.4, - "elevated": 10.3, - "breaking": 1.2 - } - }, - { - "bands": [ - 25.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 82.1, - "watch": 9.8, - "elevated": 2.9, - "breaking": 5.1 - } - }, - { - "bands": [ - 25.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 82.1, - "watch": 9.8, - "elevated": 4.7, - "breaking": 3.4 - } - }, - { - "bands": [ - 25.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 82.1, - "watch": 9.8, - "elevated": 6.9, - "breaking": 1.2 - } - } - ] - }, - "v4-vix-b": { - "label": "v4-vix-b", - "sessions": 408, - "state": { - "avg": 15.24, - "median": 8.55, - "p80": 22.62, - "p90": 44.33, - "max": 83.6, - "scored": 408 - }, - "warning_avg": 14.88, - "saturation_census": { - "p3_pegged": 0, - "p2_pegged": 0, - "v1_pegged": 0, - "p1_pegged": 0 - }, - "price_argmax_sole": { - "P1": 17, - "P2": 48, - "P3": 307 - }, - "price_argmax_tie_inclusive": { - "P1": 53, - "P2": 84, - "P3": 343 - }, - "price_argmax_ties": 36, - "w1_nonzero_sessions": 197, - "band_shares_current": { - "stable": 77.9, - "watch": 13.7, - "elevated": 4.7, - "breaking": 3.7 - }, - "soft_gates": [], - "band_grid": [ - { - "bands": [ - 15.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 68.1, - "watch": 20.1, - "elevated": 6.4, - "breaking": 5.4 - } - }, - { - "bands": [ - 15.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 68.1, - "watch": 20.1, - "elevated": 8.1, - "breaking": 3.7 - } - }, - { - "bands": [ - 15.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 68.1, - "watch": 20.1, - "elevated": 10.0, - "breaking": 1.7 - } - }, - { - "bands": [ - 15.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 68.1, - "watch": 23.5, - "elevated": 2.9, - "breaking": 5.4 - } - }, - { - "bands": [ - 15.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 68.1, - "watch": 23.5, - "elevated": 4.7, - "breaking": 3.7 - } - }, - { - "bands": [ - 15.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 68.1, - "watch": 23.5, - "elevated": 6.6, - "breaking": 1.7 - } - }, - { - "bands": [ - 20.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 77.9, - "watch": 10.3, - "elevated": 6.4, - "breaking": 5.4 - } - }, - { - "bands": [ - 20.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 77.9, - "watch": 10.3, - "elevated": 8.1, - "breaking": 3.7 - } - }, - { - "bands": [ - 20.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 77.9, - "watch": 10.3, - "elevated": 10.0, - "breaking": 1.7 - } - }, - { - "bands": [ - 20.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 77.9, - "watch": 13.7, - "elevated": 2.9, - "breaking": 5.4 - } - }, - { - "bands": [ - 20.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 77.9, - "watch": 13.7, - "elevated": 4.7, - "breaking": 3.7 - } - }, - { - "bands": [ - 20.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 77.9, - "watch": 13.7, - "elevated": 6.6, - "breaking": 1.7 - } - }, - { - "bands": [ - 25.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 81.6, - "watch": 6.6, - "elevated": 6.4, - "breaking": 5.4 - } - }, - { - "bands": [ - 25.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 81.6, - "watch": 6.6, - "elevated": 8.1, - "breaking": 3.7 - } - }, - { - "bands": [ - 25.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 81.6, - "watch": 6.6, - "elevated": 10.0, - "breaking": 1.7 - } - }, - { - "bands": [ - 25.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 81.6, - "watch": 10.0, - "elevated": 2.9, - "breaking": 5.4 - } - }, - { - "bands": [ - 25.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 81.6, - "watch": 10.0, - "elevated": 4.7, - "breaking": 3.7 - } - }, - { - "bands": [ - 25.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 81.6, - "watch": 10.0, - "elevated": 6.6, - "breaking": 1.7 - } - } - ] - }, - "v4-p1-capped": { - "label": "v4-p1-capped", - "sessions": 408, - "state": { - "avg": 14.73, - "median": 8.35, - "p80": 21.7, - "p90": 43.63, - "max": 80.1, - "scored": 408 - }, - "warning_avg": 14.88, - "saturation_census": { - "p3_pegged": 0, - "p2_pegged": 0, - "v1_pegged": 0, - "p1_pegged": 0 - }, - "price_argmax_sole": { - "P1": 0, - "P2": 49, - "P3": 323 - }, - "price_argmax_tie_inclusive": { - "P1": 36, - "P2": 85, - "P3": 359 - }, - "price_argmax_ties": 36, - "w1_nonzero_sessions": 197, - "band_shares_current": { - "stable": 78.9, - "watch": 13.0, - "elevated": 4.7, - "breaking": 3.4 - }, - "soft_gates": [], - "band_grid": [ - { - "bands": [ - 15.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 68.9, - "watch": 19.6, - "elevated": 6.4, - "breaking": 5.1 - } - }, - { - "bands": [ - 15.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 68.9, - "watch": 19.6, - "elevated": 8.1, - "breaking": 3.4 - } - }, - { - "bands": [ - 15.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 68.9, - "watch": 19.6, - "elevated": 10.3, - "breaking": 1.2 - } - }, - { - "bands": [ - 15.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 68.9, - "watch": 23.0, - "elevated": 2.9, - "breaking": 5.1 - } - }, - { - "bands": [ - 15.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 68.9, - "watch": 23.0, - "elevated": 4.7, - "breaking": 3.4 - } - }, - { - "bands": [ - 15.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 68.9, - "watch": 23.0, - "elevated": 6.9, - "breaking": 1.2 - } - }, - { - "bands": [ - 20.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 78.9, - "watch": 9.6, - "elevated": 6.4, - "breaking": 5.1 - } - }, - { - "bands": [ - 20.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 78.9, - "watch": 9.6, - "elevated": 8.1, - "breaking": 3.4 - } - }, - { - "bands": [ - 20.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 78.9, - "watch": 9.6, - "elevated": 10.3, - "breaking": 1.2 - } - }, - { - "bands": [ - 20.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 78.9, - "watch": 13.0, - "elevated": 2.9, - "breaking": 5.1 - } - }, - { - "bands": [ - 20.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 78.9, - "watch": 13.0, - "elevated": 4.7, - "breaking": 3.4 - } - }, - { - "bands": [ - 20.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 78.9, - "watch": 13.0, - "elevated": 6.9, - "breaking": 1.2 - } - }, - { - "bands": [ - 25.0, - 40.0, - 60.0 - ], - "shares": { - "stable": 82.1, - "watch": 6.4, - "elevated": 6.4, - "breaking": 5.1 - } - }, - { - "bands": [ - 25.0, - 40.0, - 65.0 - ], - "shares": { - "stable": 82.1, - "watch": 6.4, - "elevated": 8.1, - "breaking": 3.4 - } - }, - { - "bands": [ - 25.0, - 40.0, - 70.0 - ], - "shares": { - "stable": 82.1, - "watch": 6.4, - "elevated": 10.3, - "breaking": 1.2 - } - }, - { - "bands": [ - 25.0, - 50.0, - 60.0 - ], - "shares": { - "stable": 82.1, - "watch": 9.8, - "elevated": 2.9, - "breaking": 5.1 - } - }, - { - "bands": [ - 25.0, - 50.0, - 65.0 - ], - "shares": { - "stable": 82.1, - "watch": 9.8, - "elevated": 4.7, - "breaking": 3.4 - } - }, - { - "bands": [ - 25.0, - 50.0, - 70.0 - ], - "shares": { - "stable": 82.1, - "watch": 9.8, - "elevated": 6.9, - "breaking": 1.2 - } - } - ] - } - }, - "scenarios": { - "note": "pillar arithmetic on explicit sensor scores; State weights unchanged", - "vix_a": [ - { - "scenario": "S1 ordinary tape", - "P1": 0.0, - "P2": 0.0, - "P3": 7.5, - "price": 7.5, - "breadth": 0.0, - "C1": 0.0, - "V1": 4.0, - "state": 3.6 - }, - { - "scenario": "S2 10% correction, calm credit", - "P1": 30.0, - "P2": 0.0, - "P3": 31.25, - "price": 31.25, - "breadth": 62.5, - "C1": 0.0, - "V1": 34.4, - "state": 33.28 - }, - { - "scenario": "S3a 2022-style, calm credit, no death cross", - "P1": 87.5, - "P2": 0.0, - "P3": 90.83, - "price": 90.83, - "breadth": 100.0, - "C1": 0.0, - "V1": 60.0, - "state": 70.33 - }, - { - "scenario": "S3b 2022-style, calm credit, death cross", - "P1": 87.5, - "P2": 100.0, - "P3": 90.83, - "price": 100.0, - "breadth": 100.0, - "C1": 0.0, - "V1": 60.0, - "state": 74.0 - }, - { - "scenario": "S4 credit event on top", - "P1": 100.0, - "P2": 100.0, - "P3": 100.0, - "price": 100.0, - "breadth": 100.0, - "C1": 75.0, - "V1": 86.67, - "state": 93.0 - }, - { - "scenario": "S5 March 2020, everything pegged", - "P1": 100.0, - "P2": 100.0, - "P3": 100.0, - "price": 100.0, - "breadth": 100.0, - "C1": 100.0, - "V1": 100.0, - "state": 100.0 - } - ] - }, - "diagnostics": { - "vix_top10": [ - [ - "2025-04-08", - 52.33 - ], - [ - "2025-04-07", - 46.98 - ], - [ - "2025-04-04", - 45.31 - ], - [ - "2025-04-10", - 40.72 - ], - [ - "2024-08-05", - 38.57 - ], - [ - "2025-04-11", - 37.56 - ], - [ - "2025-04-21", - 33.82 - ], - [ - "2025-04-09", - 33.62 - ], - [ - "2025-04-16", - 32.64 - ], - [ - "2026-03-27", - 31.05 - ] - ], - "candidate_anchors": { - "P5_VIX_ANCHORS_A": [ - [ - 15.0, - 0.0 - ], - [ - 20.0, - 20.0 - ], - [ - 25.0, - 38.0 - ], - [ - 30.0, - 55.0 - ], - [ - 40.0, - 80.0 - ], - [ - 55.0, - 100.0 - ] - ], - "P5_VIX_ANCHORS_B": [ - [ - 15.0, - 0.0 - ], - [ - 20.0, - 25.0 - ], - [ - 25.0, - 45.0 - ], - [ - 30.0, - 65.0 - ], - [ - 40.0, - 85.0 - ], - [ - 55.0, - 100.0 - ] - ], - "P1_TREND_BREAK_ANCHORS": [ - [ - 0.0, - 20.0 - ], - [ - 3.0, - 35.0 - ], - [ - 8.0, - 55.0 - ], - [ - 15.0, - 75.0 - ], - [ - 25.0, - 100.0 - ] - ] - } - }, - "v4_recommendation": { - "state_bands_candidate": [ - 20.0, - 50.0, - 65.0 - ], - "provisional": false, - "note": "confirm against band_grid + scenarios before shipping" - } -} \ No newline at end of file diff --git a/reports/regime-monitor-v4-calibration-20260808-201931.md b/reports/regime-monitor-v4-calibration-20260808-201931.md deleted file mode 100644 index 5e3abf1..0000000 --- a/reports/regime-monitor-v4-calibration-20260808-201931.md +++ /dev/null @@ -1,75 +0,0 @@ -# Regime Monitor v4 calibration - -Generated 2026-08-08T20:19:31 at `c3ae5ad`, 2024-12-05 → 2026-07-24. - -## Hard gates - -| gate | expected | measured | | -|---|---|---|---| -| symbols_fetched | 33 | 33 | ok | -| per_symbol_warmup_252_bars | all | 33 | ok | -| per_symbol_reaches_last_session | 2026-07-24 | 33 | ok | -| breadth_counts_full_basket | 30 | 408/408 sessions | ok | -| sessions_scored | 408 | 408 | ok | -| last_scored_date | 2026-07-24 | 2026-07-24 | ok | -| w1_available_every_session | 408 | 408 | ok | -| state_coverage_100_every_row | 0 | 0 | ok | -| no_stale_inputs | 0 | 0 | ok | -| state_v4_le_v3_every_row | 0 | 0 | ok | - -## Distributions - -| variant | avg | median | p80 | p90 | max | -|---|---|---|---|---|---| -| v2_reconstruction | 22.68 | 16.15 | 35.1 | 65.63 | 91.2 | -| v3 | 18.13 | 9.1 | 31.36 | 65.0 | 87.4 | -| v4 | 14.78 | 8.35 | 21.7 | 43.63 | 83.5 | -| v4-vix-b | 15.24 | 8.55 | 22.62 | 44.33 | 83.6 | -| v4-p1-capped | 14.73 | 8.35 | 21.7 | 43.63 | 80.1 | - -## Saturation census (sessions pegged at 100) - -| variant | P1 | P2 | P3 | V1 | -|---|---|---|---|---| -| v2_reconstruction | 46 | 0 | 39 | 14 | -| v3 | 46 | 0 | 0 | 14 | -| v4 | 0 | 0 | 0 | 0 | -| v4-vix-b | 0 | 0 | 0 | 0 | -| v4-p1-capped | 0 | 0 | 0 | 0 | - -## Reproduction gates — v2_reconstruction - -| figure | published | measured | | -|---|---|---|---| -| v2_state_avg | 22.6 | 22.68 | ok | -| v2_state_p80 | 35.1 | 35.1 | ok | -| v2_state_max | 91.2 | 91.2 | ok | -| v2_p3_pegged | 39 | 39 | ok | -| w1_live_sessions | 108 | 108 | ok | - -## Reproduction gates — v3 - -| figure | published | measured | | -|---|---|---|---| -| v3_state_max | 87.4 | 87.4 | ok | - -## v4 band-share grid (watch 20 / elevated 50) - -| breaking | stable | watch | elevated | breaking | -|---|---|---|---|---| -| 60 | 78.9 | 13.0 | 2.9 | 5.1 | -| 65 | 78.9 | 13.0 | 4.7 | 3.4 | -| 70 | 78.9 | 13.0 | 6.9 | 1.2 | - -## Scenarios (pillar arithmetic, explicit sensor scores) - -| scenario | price | breadth | C1 | V1 | State | -|---|---|---|---|---|---| -| S1 ordinary tape | 7.5 | 0.0 | 0.0 | 4.0 | **3.6** | -| S2 10% correction, calm credit | 31.25 | 62.5 | 0.0 | 34.4 | **33.28** | -| S3a 2022-style, calm credit, no death cross | 90.83 | 100.0 | 0.0 | 60.0 | **70.33** | -| S3b 2022-style, calm credit, death cross | 100.0 | 100.0 | 0.0 | 60.0 | **74.0** | -| S4 credit event on top | 100.0 | 100.0 | 75.0 | 86.67 | **93.0** | -| S5 March 2020, everything pegged | 100.0 | 100.0 | 100.0 | 100.0 | **100.0** | - -Recommendation: `{'state_bands_candidate': [20.0, 50.0, 65.0], 'provisional': False, 'note': 'confirm against band_grid + scenarios before shipping'}` diff --git a/scripts/run_regime_monitor_calibration.py b/scripts/run_regime_monitor_calibration.py index fdc5f4d..d6a2499 100644 --- a/scripts/run_regime_monitor_calibration.py +++ b/scripts/run_regime_monitor_calibration.py @@ -55,6 +55,10 @@ from app.services import regime_monitor_service as rms # noqa: E402 # on purpose -- anything tighter would be false precision. PUBLISHED = { "window_end": "2026-07-24", + # The session count alone is tautological -- the harness slices the tail of + # leader_series, so it can only ever equal what was asked for. The start date + # is what actually validates the calendar. + "window_first": "2024-12-05", "sessions": 408, "w1_live_sessions": 108, "v2_state_avg": 22.6, @@ -223,8 +227,19 @@ VARIANTS: dict[str, dict[str, Callable]] = { "p1_trend_break": _capped(_candidate_p1(), 50.0), "p5_volatility": _candidate_p5(P5_VIX_ANCHORS_A), }, - "v4-vix-only": {"p5_volatility": _candidate_p5(P5_VIX_ANCHORS_A)}, - "v4-p1-only": {"p1_trend_break": _candidate_p1()}, + "v4-vix-only": {"p1_trend_break": _v3_p1_trend_break}, + "v4-p1-only": {"p5_volatility": _v3_p5_volatility}, + # (4) v2 as production actually fetched it: a 400-calendar-day OAS source, + # which left the oldest rows with no credit at all. Truncating the SERIES is + # the only faithful simulation -- patching the per-session window is not, + # because the data was simply absent. + "v2_reconstruction_oas400": { + "p1_trend_break": _v3_p1_trend_break, + "p5_volatility": _v3_p5_volatility, + "p3_drawdown": _v2_p3_drawdown, + "f2_credit_spreads": _v2_f2_credit_spreads, + "HY_OAS_WINDOW_DAYS": 3653, + }, # v2 State sensors + the v2 divergence gate that feeds W1. The v2 *Warning # composition* (F1/F3 fundamentals, 20 of 100 points) is NOT reconstructed, # so only State statistics and the W1 census are comparable to the published @@ -245,7 +260,14 @@ VARIANTS: dict[str, dict[str, Callable]] = { } # Variants needing the retired divergence formula rather than the live one. -V2_DIVERGENCE_VARIANTS = {"v2_reconstruction"} +V2_DIVERGENCE_VARIANTS = {"v2_reconstruction", "v2_reconstruction_oas400"} + +# Variants whose OAS *source series* is truncated before replay, in calendar days. +OAS_SOURCE_TRUNCATION = {"v2_reconstruction_oas400": 400} + +# A v4 recommendation is meaningless without both of these: the row-wise +# state_v4 <= state_v3 invariant needs them, and it is a hard gate. +REQUIRED_VARIANTS = ("v3", "v4") @contextlib.contextmanager @@ -332,6 +354,10 @@ def _replay( divergence_series = divergence_by_variant[ "v2" if variant in V2_DIVERGENCE_VARIANTS else "live" ] + truncate_days = OAS_SOURCE_TRUNCATION.get(variant) + if truncate_days is not None and oas: + cutoff = max(d for d, _ in oas) - timedelta(days=truncate_days) + oas = [(d, v) for d, v in oas if d >= cutoff] rows: list[dict] = [] with patched(VARIANTS[variant]): for as_of in sessions: @@ -383,6 +409,10 @@ def _stats(rows: list[dict], label: str) -> dict: return sum(1 for r in rows if (_sensor_score(r, pillar, sensor) or 0) >= 100.0) argmax_sole, argmax_tied, ties = {"P1": 0, "P2": 0, "P3": 0}, {"P1": 0, "P2": 0, "P3": 0}, 0 + # The P1_SCORE_CAP rule is "sole argmax on >80% of sessions with State >= 40", + # so the all-session count does not evaluate it. Track the conditional + # population separately rather than deciding off the wrong denominator. + stressed_sole, stressed_total = {"P1": 0, "P2": 0, "P3": 0}, 0 for row in rows: legs = {s: _sensor_score(row, "price", s) for s in ("P1", "P2", "P3")} live = {k: v for k, v in legs.items() if v is not None} @@ -396,6 +426,10 @@ def _stats(rows: list[dict], label: str) -> dict: argmax_tied[w] += 1 if len(winners) == 1: argmax_sole[winners[0]] += 1 + if (row["state"]["score"] or 0) >= 40.0: + stressed_total += 1 + if len(winners) == 1: + stressed_sole[winners[0]] += 1 return { "label": label, @@ -418,6 +452,13 @@ def _stats(rows: list[dict], label: str) -> dict: "price_argmax_sole": argmax_sole, "price_argmax_tie_inclusive": argmax_tied, "price_argmax_ties": ties, + "price_argmax_when_state_ge_40": { + "sessions": stressed_total, + "sole": stressed_sole, + "p1_sole_share_pct": round(stressed_sole["P1"] / stressed_total * 100.0, 1) + if stressed_total else None, + "cap_rule": "P1_SCORE_CAP warranted if p1_sole_share_pct > 80", + }, "w1_nonzero_sessions": sum( 1 for r in rows if (_sensor_score(r, "breadth_divergence", "W1") or 0) > 0 ), @@ -460,7 +501,7 @@ def _completeness_gates( ] -def _pipeline_gates(rows: list[dict], sessions: list[date], expected_first: str | None) -> list[dict]: +def _pipeline_gates(rows: list[dict], sessions: list[date], expected_first: str) -> list[dict]: coverage_bad = [ r["date"] for r in rows if (r["state"]["coverage"] or 0) < 100.0 ] @@ -482,13 +523,13 @@ def _pipeline_gates(rows: list[dict], sessions: list[date], expected_first: str {"gate": "no_stale_inputs", "expected": 0, "measured": len(stale), "passed": not stale, "detail": stale[:20]}, ] - # sessions_scored alone is tautological when the harness slices the tail of - # leader_series -- the calendar is only really checked by the start date. - if expected_first: - gates.append({ - "gate": "first_scored_date", "expected": expected_first, - "measured": rows[0]["date"], "passed": rows[0]["date"] == expected_first, - }) + # Unconditional: sessions_scored is tautological when the harness slices the + # tail of leader_series, so the start date is the only real calendar check. + # An optional gate is not a gate. + gates.append({ + "gate": "first_scored_date", "expected": expected_first, + "measured": rows[0]["date"], "passed": rows[0]["date"] == expected_first, + }) return gates @@ -574,8 +615,13 @@ def _scenarios(vix_anchors, p1_anchors) -> list[dict]: def _markdown(report: dict) -> str: """Scannable sibling to the JSON. Never written over a curated docs/ file.""" lines = ["# Regime Monitor v4 calibration", ""] - lines.append(f"Generated {report['generated_at']} at `{report['git_rev']}`, " + src = report["source"] + dirty = " **(dirty working tree)**" if src.get("git_dirty") else "" + lines.append(f"Generated {report['generated_at']} at `{src['git_rev']}`{dirty}, " f"{report['provenance']['scored_range'][0]} → {report['provenance']['scored_range'][1]}.") + lines += ["", "Source hashes (sha256, first 16):", ""] + for rel, digest in src["source_sha256"].items(): + lines.append(f"- `{rel}` — `{digest}`") lines += ["", "## Hard gates", "", "| gate | expected | measured | |", "|---|---|---|---|"] for g in report["hard_gates"]: lines.append(f"| {g['gate']} | {g['expected']} | {g['measured']} | {'ok' if g['passed'] else '**FAIL**'} |") @@ -637,7 +683,9 @@ def _parse_args() -> argparse.Namespace: help="fetch range; matches v2's request (ICE truncates to ~3y)") p.add_argument("--methodology", default="v3,v4") p.add_argument("--expected-first-session", default=None, - help="assert the first replayed date; sessions count alone is tautological") + help="assert the first replayed date. Defaults to the published " + "window's start; REQUIRED when --end/--sessions are overridden, " + "since the count alone is tautological.") p.add_argument("--cache-dir", default=None) p.add_argument("--out", default=None) p.add_argument("--quiet", action="store_true") @@ -652,6 +700,33 @@ def _git_rev() -> str: return "unknown" +def _source_state() -> dict: + """Identify the code that produced this run, not just the commit HEAD names. + + A run from a dirty tree is not reproducible by checking out git_rev -- which + is exactly how the first v4 artifact was generated, with HEAD still on the + harness commit while the v4 sensors lived only in the working tree. The + hashes make that visible instead of implied. + """ + import hashlib + + tracked = [ + "app/services/regime_monitor_service.py", + "app/services/breadth_service.py", + "scripts/run_regime_monitor_calibration.py", + ] + digests = {} + for rel in tracked: + path = ROOT / rel + digests[rel] = hashlib.sha256(path.read_bytes()).hexdigest()[:16] if path.exists() else None + try: + dirty = bool(subprocess.run(["git", "status", "--porcelain"], cwd=ROOT, + capture_output=True, text=True, check=True).stdout.strip()) + except Exception: + dirty = None + return {"git_rev": _git_rev(), "git_dirty": dirty, "source_sha256": digests} + + async def _main() -> int: args = _parse_args() end = date.fromisoformat(args.end) @@ -659,10 +734,23 @@ async def _main() -> int: config = deepcopy(rms.DEFAULT_CONFIG) symbols = _all_symbols(config) variants = [v.strip() for v in args.methodology.split(",") if v.strip()] + expected_first = args.expected_first_session + if not expected_first: + if args.end != PUBLISHED["window_end"] or args.sessions != PUBLISHED["sessions"]: + print("--expected-first-session is required when --end or --sessions " + "differ from the published window.", file=sys.stderr) + return 2 + expected_first = PUBLISHED["window_first"] unknown = [v for v in variants if v not in VARIANTS] if unknown: print(f"unknown variant(s): {unknown}; known: {sorted(VARIANTS)}", file=sys.stderr) return 2 + missing = [v for v in REQUIRED_VARIANTS if v not in variants] + if missing: + print(f"--methodology must include {list(REQUIRED_VARIANTS)}; missing {missing}. " + "The state_v4 <= state_v3 invariant is a hard gate and needs both.", + file=sys.stderr) + return 2 if not args.quiet: print(f"fetching {len(symbols)} symbols from Alpaca...", flush=True) @@ -707,7 +795,7 @@ async def _main() -> int: canonical = rows_by_variant.get("v3") or next(iter(rows_by_variant.values())) hard_gates = _completeness_gates(prices, symbols, sessions, breadth_counts, len(_basket(config))) - hard_gates += _pipeline_gates(canonical, sessions, args.expected_first_session) + hard_gates += _pipeline_gates(canonical, sessions, expected_first) if "v3" in rows_by_variant and "v4" in rows_by_variant: hard_gates.append(_invariant_gate(rows_by_variant["v3"], rows_by_variant["v4"])) @@ -718,7 +806,7 @@ async def _main() -> int: report = { "generated_at": datetime.now().isoformat(timespec="seconds"), - "git_rev": _git_rev(), + "source": _source_state(), "params": vars(args), "provenance": { "symbols": {s: {"bars": len(prices.get(s, [])), diff --git a/tests/unit/test_regime_monitor.py b/tests/unit/test_regime_monitor.py index 3d53d40..7dc3f8f 100644 --- a/tests/unit/test_regime_monitor.py +++ b/tests/unit/test_regime_monitor.py @@ -52,6 +52,10 @@ def test_band_for_is_per_axis(): assert band_for(20, STATE_BANDS) == "watch" assert band_for(50, STATE_BANDS) == "elevated" assert band_for(80, STATE_BANDS) == "breaking" + # v4 moved the top band 80 -> 65; pin the new boundary from both sides so a + # silent revert cannot pass. band_for is inclusive at the threshold. + assert band_for(64.9, STATE_BANDS) == "elevated" + assert band_for(65, STATE_BANDS) == "breaking" # Warning's realized range is far narrower, so it gets its own thresholds. assert band_for(45, STATE_BANDS) == "watch" assert band_for(45, WARNING_BANDS) == "elevated" @@ -401,6 +405,7 @@ async def test_v2_observation_survives_the_methodology_bump(monkeypatch, stored_ "source": "gemini", "fetched_at": "2026-07-24T14:25:47+00:00", "effective_date": "2026-07-27", + "locked": True, }) monkeypatch.setattr(rms.settings_store, "get_value", fake_value) @@ -412,6 +417,9 @@ async def test_v2_observation_survives_the_methodology_bump(monkeypatch, stored_ assert result["effective_date"] == "2026-07-27" assert result["f1_score"] == 37.5 # recomputed on the current scale, not the stored 0.0 assert result["fetched_at"] == "2026-07-24T14:25:47+00:00" # or a refresh loop starts + # locked is the operator saying "do not overwrite this". Losing it is half the + # failure mode: update_regime_monitor only auto-refreshes when locked is false. + assert result["locked"] is True @pytest.mark.asyncio