From f9abfb98d6d49b70087cb8bd20bde374a3049664 Mon Sep 17 00:00:00 2001 From: Brian McMahon Date: Thu, 28 May 2026 11:56:47 -0700 Subject: [PATCH] feat(schema): signals.schema.json market_regime to 3-class (Phase 1C) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Caution-regime retirement Phase 1C — data contract trim. Plan doc: alpha-engine-docs/private/caution-regime-retirement-260528.md. - market_regime enum: ["bull","neutral","caution","bear"] → ["bull","neutral","bear"] (3-class Ang-Bekaert taxonomy). - Schema description names the SOTA framing: stress signals (VIX, HY OAS, SPY 30d return) flow through the continuous regime_intensity_z META_FEATURE rather than discretizing into a 4th category, and portfolio-protective hysteresis is a separate axis on the predictor drawdown leg (risk_on/caution/risk_off). - Historical signals.json artifacts with 'caution' in market_regime are GRANDFATHERED — consumers tolerant of legacy enum on read (no migration of S3 objects). Tests: - test_market_regime_enum_is_3class: pins the 3-class enum literal - test_market_regime_caution_rejected: pins jsonschema rejection - test_each_3class_regime_accepted: pins forward-compat smoke Suite: 1675 → 1678 passing. Composes with: - alpha-engine-lib #86 (Phase 1A — RegimeLiteral chokepoint v0.42) - alpha-engine-research Phase 1B (macro_agent retirement) - alpha-engine-config Phase 1D (universe.yaml regime_guardrails caution thresholds removal + ROADMAP write-back) - Phase 2 type-system separation (drawdown_tier ≠ market_regime) Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/signals.schema.json | 4 +-- .../test_data_contract_validation.py | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/contracts/signals.schema.json b/contracts/signals.schema.json index 1956a55..d321abe 100644 --- a/contracts/signals.schema.json +++ b/contracts/signals.schema.json @@ -20,8 +20,8 @@ }, "market_regime": { "type": "string", - "enum": ["bull", "neutral", "caution", "bear"], - "description": "Current market regime assessment" + "enum": ["bull", "neutral", "bear"], + "description": "Current market regime assessment. 3-class Ang-Bekaert taxonomy (v0.42.0 / 2026-05-28). Legacy 4-class 'caution' retired per caution-regime-retirement-260528.md — its driving signals (VIX, HY OAS, SPY 30d return) are weighted continuously into regime_intensity_z (predictor META_FEATURE 13) rather than discretized into a 4th category. Portfolio-protective hysteresis is a separate axis on the predictor drawdown leg (risk_on/caution/risk_off). Historical signals.json artifacts with 'caution' in market_regime are grandfathered — consumers tolerant of legacy enum on read." }, "sector_ratings": { "type": "object", diff --git a/tests/integration/test_data_contract_validation.py b/tests/integration/test_data_contract_validation.py index d41c944..c99cf61 100644 --- a/tests/integration/test_data_contract_validation.py +++ b/tests/integration/test_data_contract_validation.py @@ -69,6 +69,41 @@ def test_missing_required_field_fails(self): with pytest.raises(ValidationError): validate(instance=bad_signals, schema=schema) + def test_market_regime_enum_is_3class(self): + # 3-class Ang-Bekaert taxonomy (v0.42.0 / 2026-05-28). + # Legacy 4-class "caution" retired per + # caution-regime-retirement-260528.md. + schema = _load_schema("signals.schema.json") + regime_field = schema["properties"]["market_regime"] + assert regime_field["enum"] == ["bull", "neutral", "bear"] + + def test_market_regime_caution_rejected(self): + schema = _load_schema("signals.schema.json") + bad_signals = { + "date": "2026-04-03", + "run_time": "00:30:00", + "market_regime": "caution", + "sector_ratings": {}, + "universe": [], + "buy_candidates": [], + } + with pytest.raises(ValidationError): + validate(instance=bad_signals, schema=schema) + + def test_each_3class_regime_accepted(self): + schema = _load_schema("signals.schema.json") + for regime in ("bull", "neutral", "bear"): + signals = { + "date": "2026-04-03", + "run_time": "00:30:00", + "market_regime": regime, + "sector_ratings": {}, + "universe": [], + "buy_candidates": [], + } + # Should not raise + validate(instance=signals, schema=schema) + class TestPredictionsContract: def test_valid_predictions_pass(self):