mirror of
https://github.com/triqbit/mt5-ai-xauusd-trader.git
synced 2026-11-02 06:56:15 +00:00
🛡️ 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:
co-authored by
google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
xnessom
parent
8f88e9b187
commit
025ca10fc7
@@ -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
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user