From b7bc41bda45b81a674d6df796ab5dd85ef630d3b Mon Sep 17 00:00:00 2001 From: Brian McMahon Date: Tue, 19 May 2026 09:11:36 -0700 Subject: [PATCH] feat(regime): surface drawdown de-risk leg in brief + macro prompt (PR 4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan regime-drawdown-hysteresis-260518.md §6 item 4 (ROADMAP L99). The producer arc (predictor #176/#177/#179, ae #193) shipped 2026-05-19 and now fills substrate["drawdown"] + substrate["effective_regime"] weekly; this is the research consumer surface. - agents/macro_agent.py: new _format_drawdown_leg() renders the SPY tiered drawdown + book-vs-market excess as a continuous, market-grounded statement plus the composed effective_regime (most-protective over the ensemble). The HMM "weeks_in_current_state" line is reframed as a filter-stability DIAGNOSTIC, explicitly NOT a market-duration statement (origin: the 2026-05-15 brief's misleading "P(neutral)=1.00, 50 weeks in state"). - graph/research_graph.py: _build_regime_trend drops the "Weeks in State" column for a continuous "SPY Drawdown" + composed "Effective" column, and the Summary line carries the continuous statement ("SPY X% off trailing peak; book Y% off NAV HWM; Z pp deeper"). Fallbacks (both tested): - absent-key: no substrate["drawdown"] (pre-#176/#179) ⇒ the leg block is omitted entirely — byte-identical to the prior HMM-only path. - portfolio-unavailable: drawdown.excess.available=False ⇒ SPY leg still renders; excess line states it is unavailable; never raises. flag-name note: consumers/config use drawdown_regime_enabled (matches shipped predictor #178 + ROADMAP/SYSTEM_STATE), not the plan doc's stray L214 regime_drawdown_enabled spelling. Tests: +8 (drawdown columns, continuous summary, absent-key, NAV unavailable, macro-prompt leg rendering). max_tokens lint allowlist line ref bumped 476→562 (critic call relocated, unchanged 512 literal). Full suite 1372 passed. Co-Authored-By: Claude Opus 4.7 (1M context) --- agents/macro_agent.py | 90 ++++++++++++++- graph/research_graph.py | 61 ++++++++-- tests/test_email_redesign.py | 104 +++++++++++++++++- tests/test_max_tokens_lint.py | 6 +- ...t_regime_stage_c1_substrate_consumption.py | 93 ++++++++++++++++ 5 files changed, 341 insertions(+), 13 deletions(-) diff --git a/agents/macro_agent.py b/agents/macro_agent.py index 8c0b53e8..2d999a02 100644 --- a/agents/macro_agent.py +++ b/agents/macro_agent.py @@ -108,6 +108,87 @@ def _fmt(val, fmt=".1f", default="N/A") -> str: return default +def _format_drawdown_leg(substrate: dict) -> str: + """Render the deterministic drawdown de-risk leg (3rd ensemble leg) + + the composed ``effective_regime`` as a continuous, market-grounded + statement. + + Absent-key fallback: when the upstream substrate carries no + ``drawdown`` block (pre-#176/#179 producer, or producer omitted it), + returns ``""`` so the prompt is byte-identical to the prior HMM-only + path — zero behavior change for the macro agent. + + Portfolio-unavailable fallback: when ``drawdown.excess.available`` is + False (paper NAV short/gappy or not wired), the SPY leg still renders + and the excess line states it is unavailable — never raises. + """ + dd = substrate.get("drawdown") + if not isinstance(dd, dict): + return "" + + spy = dd.get("spy") or {} + excess = dd.get("excess") or {} + spy_dd = spy.get("drawdown") + spy_tier = spy.get("tier", "N/A") + spy_off = ( + f"{-float(spy_dd) * 100:.1f}% off its trailing peak" + if isinstance(spy_dd, (int, float)) + else "depth N/A" + ) + + if excess.get("available"): + nav_dd = excess.get("nav_drawdown") + depth = excess.get("excess_depth") + nav_off = ( + f"{-float(nav_dd) * 100:.1f}% off NAV high-water mark" + if isinstance(nav_dd, (int, float)) + else "NAV depth N/A" + ) + depth_str = ( + f"{float(depth) * 100:.1f}pp deeper than the market" + if isinstance(depth, (int, float)) + else "excess depth N/A" + ) + excess_line = ( + f" - book: {nav_off}; {depth_str} " + f"(tier = {excess.get('tier', 'N/A')})\n" + ) + else: + excess_line = ( + " - book-vs-market excess: UNAVAILABLE (paper NAV " + "short/gappy or not wired) — the SPY leg still acts.\n" + ) + + eff = substrate.get("effective_regime") or {} + if isinstance(eff, dict): + eff_label = eff.get("effective_regime", "N/A") + drivers = eff.get("drivers", {}) or {} + driver_str = ", ".join( + f"{k}={v}" for k, v in drivers.items() if v + ) or "none escalating" + else: + # latest.json sidecar carries effective_regime as a bare string + eff_label = eff if isinstance(eff, str) else "N/A" + driver_str = "n/a (sidecar shape)" + + return ( + "\n" + " DETERMINISTIC DRAWDOWN DE-RISK LEG (3rd ensemble leg — " + "pure-level hysteresis, zero estimation risk):\n" + " This is the continuous, market-grounded reframe of the HMM " + "filter run-length above — a real peak-to-trough statement, not " + "a label-stability artifact.\n" + f" - SPY: {spy_off} (tier = {spy_tier})\n" + f"{excess_line}" + f" - composed effective_regime = {eff_label} " + f"(most-protective over {{{driver_str}}})\n" + " The composed effective_regime is what the discrete gates " + "(entry halt, bear caps, veto) act on downstream — observe-mode " + "until promoted. You remain the FINAL authority on the regime " + "narrative; weigh this leg as a strong de-risk prior.\n" + ) + + def _format_regime_substrate(substrate: Optional[dict]) -> str: """Render the quantitative regime substrate into a compact prompt block for the LLM. Returns a fallback message when None. @@ -169,8 +250,13 @@ def _format_regime_substrate(substrate: Optional[dict]) -> str: f" - P(bear) = {_fmt(probs.get('bear'), '.2f')}\n" f" - P(neutral) = {_fmt(probs.get('neutral'), '.2f')}\n" f" - P(bull) = {_fmt(probs.get('bull'), '.2f')}\n" - f" - argmax = {hmm.get('argmax', 'N/A')}, " - f"weeks_in_current_state = {hmm.get('weeks_in_current_state', 'N/A')}\n" + f" - argmax = {hmm.get('argmax', 'N/A')}\n" + f" - filter run-length = " + f"{hmm.get('weeks_in_current_state', 'N/A')} week(s) " + f"(HMM filter-argmax stability DIAGNOSTIC — NOT a " + f"market-duration statement; see the drawdown leg below for the " + f"continuous market-grounded view)\n" + f"{_format_drawdown_leg(substrate)}" "\n" f" Composite intensity (AQR-style risk-on/risk-off, positive = risk-on):\n" f" - intensity_z = {_fmt(composite.get('intensity_z'), '+.2f')} " diff --git a/graph/research_graph.py b/graph/research_graph.py index 2a8505d4..6297064c 100644 --- a/graph/research_graph.py +++ b/graph/research_graph.py @@ -2270,8 +2270,19 @@ def _change_of(a: dict): cs = a.get("regime_change_signal") return bool(cs) - def _weeks_in_state(a: dict): - return (a.get("hmm") or {}).get("weeks_in_current_state") + def _eff_of(a: dict) -> str: + eff = a.get("effective_regime") + if isinstance(eff, dict): + return eff.get("effective_regime") or "—" + # latest.json sidecar carries it as a bare string + return eff if isinstance(eff, str) else "—" + + def _spy_dd_cell(a: dict) -> str: + spy = ((a.get("drawdown") or {}).get("spy")) or {} + dd = spy.get("drawdown") + if not isinstance(dd, (int, float)): + return "—" + return f"{-dd * 100:.1f}% ({spy.get('tier', '?')})" def _fmt_iz(iz) -> str: return f"{iz:+.2f}" if isinstance(iz, (int, float)) else "—" @@ -2280,17 +2291,25 @@ def _fmt_iz(iz) -> str: f"**Weekly regime substrate — last {len(artifacts)} run(s) " f"(oldest → newest):**", "", - "| Date | HMM Regime | Intensity-z | BOCPD Change | Weeks in State |", - "|------|------------|-------------|--------------|----------------|", + "| Date | HMM Regime | Intensity-z | BOCPD Change | " + "SPY Drawdown | Effective |", + "|------|------------|-------------|--------------|" + "--------------|-----------|", ] for a in artifacts: - wic = _weeks_in_state(a) - wic_str = str(wic) if isinstance(wic, (int, float)) else "—" lines.append( f"| {_date_of(a)} | {_argmax_of(a)} | {_fmt_iz(_iz_of(a))} | " - f"{'yes' if _change_of(a) else 'no'} | {wic_str} |" + f"{'yes' if _change_of(a) else 'no'} | {_spy_dd_cell(a)} | " + f"{_eff_of(a)} |" ) lines.append("") + lines.append( + "_HMM filter run-length (\"weeks in state\") is intentionally " + "omitted — it is a label-stability diagnostic, not a " + "market-duration statement; the continuous drawdown depth below " + "is the market-grounded view._" + ) + lines.append("") if len(artifacts) == 1: only = artifacts[0] @@ -2336,7 +2355,33 @@ def _fmt_iz(iz) -> str: else: gr_summary = "no guardrail breached" - lines.append(f"**Summary:** {iz_summary}; {gr_summary}.") + # Continuous drawdown statement (latest artifact) — the + # market-grounded reframe of the dropped "weeks in state" count. + # Absent-key fallback: omit the clause entirely when the producer + # wrote no drawdown block (pre-#176/#179) — no behavior change. + dd = latest.get("drawdown") or {} + spy = dd.get("spy") or {} + spy_dd = spy.get("drawdown") + dd_clause = "" + if isinstance(spy_dd, (int, float)): + parts = [ + f"SPY {-spy_dd * 100:.1f}% off trailing peak " + f"(tier {spy.get('tier', '?')})" + ] + ex = dd.get("excess") or {} + if ex.get("available"): + nav_dd = ex.get("nav_drawdown") + depth = ex.get("excess_depth") + if isinstance(nav_dd, (int, float)): + parts.append(f"book {-nav_dd * 100:.1f}% off NAV HWM") + if isinstance(depth, (int, float)): + parts.append(f"{depth * 100:.1f}pp deeper than market") + else: + parts.append("book NAV unavailable — SPY leg only") + eff = _eff_of(latest) + dd_clause = f" Drawdown leg: {'; '.join(parts)}; effective={eff}." + + lines.append(f"**Summary:** {iz_summary}; {gr_summary}.{dd_clause}") return lines diff --git a/tests/test_email_redesign.py b/tests/test_email_redesign.py index 2c7cb262..21c59772 100644 --- a/tests/test_email_redesign.py +++ b/tests/test_email_redesign.py @@ -56,8 +56,10 @@ def _artifact( change: bool = False, weeks_in_state: int = 1, guardrails: dict | None = None, + drawdown: dict | None = None, + effective_regime: dict | str | None = None, ) -> dict: - return { + art = { "run_id": run_id, "trading_day": trading_day, "calendar_date": trading_day, @@ -73,6 +75,12 @@ def _artifact( "active_severity_floor": None, }, } + # Additive drawdown leg (present only post-#176/#179 producer). + if drawdown is not None: + art["drawdown"] = drawdown + if effective_regime is not None: + art["effective_regime"] = effective_regime + return art def _base_state(**overrides) -> dict: @@ -357,3 +365,97 @@ def test_guardrail_breach_surfaced_in_summary(self): lines = _build_regime_trend(am, n_weeks=8) joined = "\n".join(lines) assert "guardrail breached: vix_bear_breached" in joined + + # ── Drawdown leg (3rd ensemble leg, #176/#179) ────────────────────── + + def _dd_block(self, *, spy_dd, spy_tier, excess=None): + block = { + "spy": { + "tier": spy_tier, + "drawdown": spy_dd, + "peak": 600.0, + "regime_contribution": ( + {"risk_on": None, "caution": "caution", + "risk_off": "bear"}.get(spy_tier) + ), + }, + "excess": excess or { + "available": False, "tier": "risk_on", + "nav_drawdown": None, "excess_depth": None, + "regime_contribution": None, + }, + } + return block + + def test_drawdown_columns_and_continuous_summary(self): + """The dropped 'weeks in state' count is reframed to a + continuous SPY-drawdown column + composed effective regime, and + the summary carries the market-grounded continuous statement.""" + am = _FakeArchiveManager([ + _artifact(run_id="2605010000", trading_day="2026-05-01", + argmax="neutral", iz=0.1, + drawdown=self._dd_block(spy_dd=-0.012, spy_tier="risk_on"), + effective_regime={"effective_regime": "neutral", + "drivers": {"hmm": "neutral"}}), + _artifact(run_id="2605080000", trading_day="2026-05-08", + argmax="bear", iz=1.2, + drawdown=self._dd_block( + spy_dd=-0.082, spy_tier="caution", + excess={"available": True, "tier": "alpha_bleed", + "nav_drawdown": -0.115, + "excess_depth": 0.033, + "regime_contribution": "bear"}), + effective_regime={"effective_regime": "bear", + "drivers": {"hmm": "bear", + "drawdown_excess": "bear"}}), + ]) + lines = _build_regime_trend(am, n_weeks=8) + joined = "\n".join(lines) + # New columns present; legacy column header gone. + assert "SPY Drawdown" in joined and "Effective" in joined + assert "Weeks in State" not in joined + # Continuous depth + tier in the row, composed regime surfaced. + assert "8.2% (caution)" in joined + assert "| bear |" in joined + # Run-length explicitly de-emphasised as a diagnostic. + assert "label-stability diagnostic" in joined + # Continuous market-grounded summary clause. + assert "Drawdown leg:" in joined + assert "SPY 8.2% off trailing peak" in joined + assert "book 11.5% off NAV HWM" in joined + assert "3.3pp deeper than market" in joined + assert "effective=bear" in joined + + def test_drawdown_absent_key_no_behavior_change(self): + """Pre-#176/#179 artifacts (no drawdown block) render '—' in the + new cells and the summary carries no Drawdown-leg clause.""" + am = _FakeArchiveManager([ + _artifact(run_id="2605010000", trading_day="2026-05-01", + argmax="neutral", iz=0.1), + _artifact(run_id="2605080000", trading_day="2026-05-08", + argmax="neutral", iz=0.3), + ]) + lines = _build_regime_trend(am, n_weeks=8) + joined = "\n".join(lines) + assert "**Summary:**" in joined + assert "Drawdown leg:" not in joined # absent-key fallback + assert "| — | — |" in joined # SPY DD + Effective both em-dash + + def test_drawdown_portfolio_unavailable_falls_back_to_spy_only(self): + am = _FakeArchiveManager([ + _artifact(run_id="2605010000", trading_day="2026-05-01", + argmax="neutral", iz=0.1, + drawdown=self._dd_block(spy_dd=-0.02, spy_tier="risk_on"), + effective_regime={"effective_regime": "neutral", + "drivers": {}}), + _artifact(run_id="2605080000", trading_day="2026-05-08", + argmax="caution", iz=0.9, + drawdown=self._dd_block(spy_dd=-0.061, spy_tier="caution"), + effective_regime={"effective_regime": "caution", + "drivers": {"drawdown_spy": "caution"}}), + ]) + lines = _build_regime_trend(am, n_weeks=8) + joined = "\n".join(lines) + assert "SPY 6.1% off trailing peak" in joined + assert "book NAV unavailable — SPY leg only" in joined + assert "effective=caution" in joined diff --git a/tests/test_max_tokens_lint.py b/tests/test_max_tokens_lint.py index 7ab38b58..b01875cb 100644 --- a/tests/test_max_tokens_lint.py +++ b/tests/test_max_tokens_lint.py @@ -43,9 +43,11 @@ # critic ever generates verbose output. # Line moved 462 → 476 by the all-agents-strict rework (added the # langchain_utils import + an explicit max_retries= - # SECTOR_TEAM_LLM_MAX_RETRIES kwarg on the critic ChatAnthropic). + # SECTOR_TEAM_LLM_MAX_RETRIES kwarg on the critic ChatAnthropic), + # then 476 → 562 by the drawdown-leg surface (added + # _format_drawdown_leg + the continuous-statement reframe). # Same intentional 512-literal critic call, just relocated. - "agents/macro_agent.py": {476}, + "agents/macro_agent.py": {562}, } diff --git a/tests/test_regime_stage_c1_substrate_consumption.py b/tests/test_regime_stage_c1_substrate_consumption.py index 76484af4..544ca1b8 100644 --- a/tests/test_regime_stage_c1_substrate_consumption.py +++ b/tests/test_regime_stage_c1_substrate_consumption.py @@ -202,6 +202,99 @@ def test_format_regime_substrate_renders_full_block() -> None: assert "FINAL authority" in block +# --------------------------------------------------------------------------- +# Drawdown de-risk leg (3rd ensemble leg, predictor #176/#179) +# --------------------------------------------------------------------------- + + +def _substrate_with_drawdown(*, excess_available: bool) -> dict: + excess = ( + {"available": True, "tier": "alpha_bleed", + "nav_drawdown": -0.118, "excess_depth": 0.041, + "regime_contribution": "bear"} + if excess_available else + {"available": False, "tier": "risk_on", "nav_drawdown": None, + "excess_depth": None, "regime_contribution": None} + ) + return { + "run_id": "2605190230", "calendar_date": "2026-05-19", + "trading_day": "2026-05-19", "schema_version": 1, + "hmm": {"argmax": "neutral", "weeks_in_current_state": 50, + "probs": {"bear": 0.0, "neutral": 1.0, "bull": 0.0}}, + "composite": {"intensity_z": 0.3, "implied_severity": "neutral", + "per_feature_z": {"vix_level": 0.4}}, + "bocpd": {"change_signal": False}, + "guardrails": {"active_severity_floor": None}, + "drawdown": { + "spy": {"tier": "caution", "drawdown": -0.072, "peak": 600.0, + "regime_contribution": "caution"}, + "excess": excess, + }, + "effective_regime": { + "effective_regime": "bear" if excess_available else "caution", + "drivers": {"hmm": "neutral", "drawdown_spy": "caution", + "drawdown_excess": ( + "bear" if excess_available else None)}, + }, + } + + +def test_format_drawdown_leg_renders_continuous_statement() -> None: + """The drawdown leg reframes the misleading HMM run-length into a + continuous, market-grounded statement + composed effective regime.""" + from agents.macro_agent import _format_regime_substrate + block = _format_regime_substrate( + _substrate_with_drawdown(excess_available=True)) + # Run-length explicitly demoted to a diagnostic, not market duration. + assert "filter run-length" in block + assert "NOT a market-duration statement" in block + # Continuous SPY + book statement. + assert "7.2% off its trailing peak" in block + assert "tier = caution" in block + assert "11.8% off NAV high-water mark" in block + assert "4.1pp deeper than the market" in block + # Composed effective regime + drivers surfaced. + assert "composed effective_regime = bear" in block + assert "DETERMINISTIC DRAWDOWN DE-RISK LEG" in block + + +def test_format_drawdown_leg_portfolio_unavailable_fallback() -> None: + """NAV unavailable ⇒ SPY leg still renders; excess line states it is + unavailable; never raises.""" + from agents.macro_agent import _format_regime_substrate + block = _format_regime_substrate( + _substrate_with_drawdown(excess_available=False)) + assert "7.2% off its trailing peak" in block + assert "book-vs-market excess: UNAVAILABLE" in block + assert "the SPY leg still acts" in block + assert "composed effective_regime = caution" in block + + +def test_format_drawdown_leg_absent_key_is_byte_identical() -> None: + """Pre-#176/#179 substrate (no drawdown key) ⇒ the drawdown block is + omitted entirely — zero behavior change vs the HMM-only path.""" + from agents.macro_agent import ( + _format_drawdown_leg, + _format_regime_substrate, + ) + no_dd = { + "run_id": "2605190230", "trading_day": "2026-05-19", + "schema_version": 1, + "hmm": {"argmax": "neutral", "weeks_in_current_state": 3, + "probs": {"bear": 0.1, "neutral": 0.8, "bull": 0.1}}, + "composite": {"intensity_z": 0.2, "implied_severity": "neutral", + "per_feature_z": {}}, + "bocpd": {"change_signal": False}, + "guardrails": {"active_severity_floor": None}, + } + assert _format_drawdown_leg(no_dd) == "" + block = _format_regime_substrate(no_dd) + assert "DETERMINISTIC DRAWDOWN DE-RISK LEG" not in block + # The run-length is still reframed as a diagnostic even without the + # drawdown leg (the misleading framing is fixed regardless). + assert "filter run-length" in block + + # --------------------------------------------------------------------------- # Macro agent threads substrate through reflection wrapper # ---------------------------------------------------------------------------