🛡️ Jules02: Risk control and drift monitoring — Market context alignment guard (#1311)

- 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>
This commit is contained in:
xnessom
2026-05-18 05:17:08 +00:00
committed by GitHub
co-authored by google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> xnessom
parent 8f88e9b187
commit 025ca10fc7
3 changed files with 152 additions and 1 deletions
+3
View File
@@ -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
+49 -1
View File
@@ -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:
+100
View File
@@ -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()