From 025ca10fc7d028b4c9f377b97a8b463c3fe501d2 Mon Sep 17 00:00:00 2001 From: xnessom Date: Sun, 17 May 2026 22:17:08 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Jules02:=20Risk=20contr?= =?UTF-8?q?ol=20and=20drift=20monitoring=20=E2=80=94=20Market=20context=20?= =?UTF-8?q?alignment=20guard=20(#1311)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement Market Context Alignment Guard (Layer 6.2) in EnsembleModel. - Added graduated confidence penalties and hard blocks based on market regime stability. - Updated RISK_LIMITS.md with new safety thresholds. - Added comprehensive unit tests for the new alignment guard. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: xnessom <255288032+xnessom@users.noreply.github.com> --- RISK_LIMITS.md | 3 + src/models/ensemble.py | 50 ++++++++++++- tests/test_regime_alignment_safety.py | 100 ++++++++++++++++++++++++++ 3 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 tests/test_regime_alignment_safety.py diff --git a/RISK_LIMITS.md b/RISK_LIMITS.md index 5f594c94..0804007b 100644 --- a/RISK_LIMITS.md +++ b/RISK_LIMITS.md @@ -78,6 +78,9 @@ Defines immutable hard limits and automatic circuit breakers to protect capital, - **Win Rate Floor**: If win rate <45%, reduce position size to 25% - **Drift Threshold**: If model drift >0.3, retrain immediately - **Drift-Aware Confidence Penalty**: Proactively reduce signal confidence if aggregate model drift exceeds 50% of the safety threshold (drift > 0.15). Reduces confidence by up to 20%. +- **Market Context Alignment Guard**: Analyzes the stability and reliability of the current market regime. It calculates a unified `context_stability` factor by blending regime confidence, session alignment, volatility alignment, and transition stability. + - **Graduated Penalty**: If `context_stability < 0.70`, apply a linear confidence penalty (up to 15%). + - **Hard Block**: If `context_stability < 0.40`, force a `HOLD` signal to prevent trading in unstable or indeterminate market states. - **Entropy Guard**: Monitor divergence between sub-model confidences. If internal consensus is weak (std > 0.25), apply a 10% safety penalty to final confidence. - **Calibration Threshold**: Halt trading if Expected Calibration Error (ECE) > 0.25 - **Daily Retraining**: Update model daily with latest data diff --git a/src/models/ensemble.py b/src/models/ensemble.py index 4aecaf2c..4ad8b016 100644 --- a/src/models/ensemble.py +++ b/src/models/ensemble.py @@ -316,7 +316,55 @@ class EnsembleModel(BaseModel): ) metadata["drift_penalty"] = drift_penalty - # 6.2 Entropy Guard (Consistency Check) + # 6.2 Market Context Alignment Guard (Regime Stability Check) + # Evaluates the quality of the market state before approving signals. + if regime_info: + # Calculate a unified context stability factor (0.0 to 1.0) + # Factors: regime confidence, session alignment, volatility alignment, and transition stability. + context_stability = float( + np.mean( + [ + regime_info.confidence, + regime_info.session_alignment, + regime_info.volatility_alignment, + (1.0 - regime_info.transition_score), + ] + ) + ) + metadata["market_context_stability"] = context_stability + + # 6.2.1 Critical Instability Check (Hard Block) + if context_stability < 0.40: + logger.warning( + "Market context instability active | symbol=%s | stability=%.2f | forcing HOLD", + symbol, + context_stability, + ) + metadata["reason"] = "Critical market context instability" + return Signal( + direction=SignalDirection.HOLD, + confidence=0.0, + metadata=metadata, + ) + + # 6.2.2 Graduated Alignment Penalty + # If stability is below institutional baseline (70%), apply a linear penalty. + if context_stability < 0.70: + # Penalty scales from 0% at 0.7 stability up to 15% at 0.4 stability. + # Formula: (0.7 - stability) * 0.5 + alignment_penalty = (0.70 - context_stability) * 0.5 + old_conf = confidence + confidence *= 1.0 - alignment_penalty + logger.warning( + "Market alignment safeguard active | symbol=%s | stability=%.2f | confidence reduced: %.2f -> %.2f", + symbol, + context_stability, + old_conf, + confidence, + ) + metadata["market_context_penalty"] = alignment_penalty + + # 6.3 Entropy Guard (Consistency Check) # If sub-models are highly divergent in their confidence, it indicates uncertainty. winning_signals = [s for s in signals.values() if s.direction == direction] if len(winning_signals) > 1: diff --git a/tests/test_regime_alignment_safety.py b/tests/test_regime_alignment_safety.py new file mode 100644 index 00000000..c8f2182a --- /dev/null +++ b/tests/test_regime_alignment_safety.py @@ -0,0 +1,100 @@ +import unittest +from unittest.mock import MagicMock +from src.core.config import TradingConfig +from src.core.constants import SignalDirection +from src.models.base_model import Signal +from src.models.ensemble import EnsembleModel +from src.models.regime_detector import RegimeInfo, MarketRegime + +class TestRegimeAlignmentSafety(unittest.TestCase): + def setUp(self): + self.config = MagicMock(spec=TradingConfig) + self.config.model_drift_threshold = 0.3 + self.ensemble = EnsembleModel(config=self.config) + # Mock sub-agents to avoid loading weights + self.ensemble.ppo_agent = MagicMock() + self.ensemble.dreamer_agent = MagicMock() + self.ensemble.lstm_model = MagicMock() + + # Default high-confidence signals + self.signals = { + "ppo": Signal(direction=SignalDirection.BUY, confidence=0.9), + "dreamer": Signal(direction=SignalDirection.BUY, confidence=0.9), + "lstm": Signal(direction=SignalDirection.BUY, confidence=0.9) + } + + # Mock DynamicEnsemble metrics to avoid drift penalty + self.ensemble.dynamic_ensemble.calculate_metrics = MagicMock(return_value={ + "accuracy": 0.8, "drift_score": 0.0, "calibration_error": 0.0 + }) + self.ensemble.dynamic_ensemble.get_weights = MagicMock(return_value={ + "ppo": 0.333, "dreamer": 0.333, "lstm": 0.334 + }) + + def test_high_stability_alignment(self): + """Scenario A: High stability and alignment should result in no penalty.""" + regime_info = RegimeInfo( + label=MarketRegime.TRENDING, + confidence=0.9, + transition_score=0.1, + volatility_index=1.0, + session_alignment=1.0, + volatility_alignment=1.0, + transition_probabilities={"trending": 0.9}, + raw_features={} + ) + + result = self.ensemble.aggregate_signals(self.signals, regime_info=regime_info) + + self.assertEqual(result.direction, SignalDirection.BUY) + self.assertAlmostEqual(result.confidence, 0.9) + self.assertNotIn("market_context_penalty", result.metadata) + + def test_moderate_misalignment_penalty(self): + """Scenario B: Moderate instability should trigger a confidence penalty.""" + # Lower alignment and higher transition score + # context_stability = (0.6 + 0.6 + 0.6 + (1.0 - 0.4)) / 4 = 0.6 + regime_info = RegimeInfo( + label=MarketRegime.RANGING, + confidence=0.6, + transition_score=0.4, + volatility_index=1.2, + session_alignment=0.6, + volatility_alignment=0.6, + transition_probabilities={"ranging": 0.6, "trending": 0.4}, + raw_features={} + ) + + result = self.ensemble.aggregate_signals(self.signals, regime_info=regime_info) + + self.assertEqual(result.direction, SignalDirection.BUY) + self.assertLess(result.confidence, 0.9) + self.assertIn("market_context_penalty", result.metadata) + # Stability 0.6 is < 0.7 trigger + # Penalty = (0.7 - 0.6) * 0.5 = 0.05 (5% reduction) + # Expected confidence = 0.9 * 0.95 = 0.855 + self.assertAlmostEqual(result.confidence, 0.855, places=3) + + def test_critical_misalignment_hold(self): + """Scenario C: Critical instability should force a HOLD.""" + # Very low alignment and high transition probability + # context_stability = (0.3 + 0.2 + 0.2 + (1.0 - 0.7)) / 4 = 0.25 + regime_info = RegimeInfo( + label=MarketRegime.NEWS_SHOCK, + confidence=0.3, + transition_score=0.7, + volatility_index=3.5, + session_alignment=0.2, + volatility_alignment=0.2, + transition_probabilities={"news_shock": 0.3, "volatile_breakout": 0.7}, + raw_features={} + ) + + result = self.ensemble.aggregate_signals(self.signals, regime_info=regime_info) + + self.assertEqual(result.direction, SignalDirection.HOLD) + self.assertEqual(result.metadata.get("reason"), "Critical market context instability") + self.assertAlmostEqual(result.metadata.get("market_context_stability"), 0.25) + +if __name__ == "__main__": + unittest.main()