diff --git a/IMPROVEMENTS_SUMMARY.md b/IMPROVEMENTS_SUMMARY.md index e25d4d2b..b04a2a77 100644 --- a/IMPROVEMENTS_SUMMARY.md +++ b/IMPROVEMENTS_SUMMARY.md @@ -255,3 +255,4 @@ Your MT5 trading bot repository is now positioned as a **world-class, enterprise This blueprint provides everything needed to build, deploy, and operate a professional algorithmic trading system for MetaTrader 5 XAUUSD trading at institutional standards. **Ready to build the next generation of trading automation? Start with Week 1 priorities and execute systematically.** 🚀 +Governing schema enforcement and institutional documentation for signal attribution and market analysis. diff --git a/requirements-ci.txt b/requirements-ci.txt index d56744f2..75f8f86f 100644 --- a/requirements-ci.txt +++ b/requirements-ci.txt @@ -20,7 +20,6 @@ redis==5.2.1 # -- Pydantic / settings ------------------------------------------- pydantic==2.10.6 pydantic-settings==2.7.1 -starlette>=0.47.2 # -- Testing ------------------------------------------------------- optuna==4.8.0 diff --git a/requirements.txt b/requirements.txt index 25bc6d57..94f7f627 100644 --- a/requirements.txt +++ b/requirements.txt @@ -40,7 +40,6 @@ alembic==1.14.1 redis==5.2.1 # ── Monitoring & Logging ──────────────────────────────────── -starlette>=0.47.2 prometheus-client==0.21.1 structlog==25.1.0 rich==13.9.4 diff --git a/src/core/decision_support.py b/src/core/decision_support.py index d0ccdeed..1b997538 100644 --- a/src/core/decision_support.py +++ b/src/core/decision_support.py @@ -1,12 +1,25 @@ """ MT5 AI/ML Trading Bot - Enterprise Edition src/core/decision_support.py -Structured operator-facing decision support system. -Provides institutional-grade decision packets before execution. -All data packets generated by this system are immutable (frozen) to ensure -that the information presented to the operator remains consistent and -cannot be altered by subsequent processing steps. +Structured operator-facing decision support system. +Provides institutional-grade decision packets before trade execution, +aggregating signal attribution, market regime, macro risk, and strategy performance. + +Usage: + dss = DecisionSupportSystem() + packet = dss.assemble_packet( + symbol="XAUUSD", + explanation=explanation, + regime_info=regime, + macro_risk=macro, + performance_metrics=metrics + ) + dss.format_for_operator(packet, console=rich_console) + +All data packets generated by this system are immutable (frozen) and enforce +strict Pydantic range constraints to ensure that the information presented +to the operator is both accurate and consistent. Author : triqbit License: MIT @@ -39,26 +52,39 @@ class PerformanceContext(BaseModel): Recent performance metrics for the current strategy/account. Enables the operator to understand the strategy's current state. - This model is immutable (frozen) and forbids extra fields. + This model is immutable (frozen) and forbids extra fields to ensure + performance metrics remain consistent and auditable. """ model_config = ConfigDict(extra="forbid", frozen=True) sharpe_ratio: float = Field( - 0.0, description="Risk-adjusted return (Sharpe Ratio). Target > 2.0 per standards." + 0.0, + ge=-10.0, + le=10.0, + description="Risk-adjusted return (Sharpe Ratio). Target > 2.0 per standards.", ) profit_factor: float = Field( - 0.0, description="Gross Profit / Gross Loss. Target > 2.0 per standards." + 0.0, + ge=0.0, + description="Gross Profit / Gross Loss. Target > 2.0 per standards.", ) recovery_factor: float = Field( - 0.0, description="Net Profit / Max Drawdown. Target > 2.0 per standards." + 0.0, + ge=0.0, + description="Net Profit / Max Drawdown. Target > 2.0 per standards.", ) win_rate: float = Field( 0.0, ge=0.0, le=1.0, description="Percentage of winning trades (0.0 to 1.0)." ) - win_loss_ratio: float = Field(0.0, description="Average Win / Average Loss.") + win_loss_ratio: float = Field( + 0.0, ge=0.0, description="Average Win / Average Loss." + ) max_drawdown: float = Field( - 0.0, description="Maximum observed equity drawdown from peak to trough." + 0.0, + ge=0.0, + le=1.0, + description="Maximum observed equity drawdown from peak to trough.", ) total_trades: int = Field(0, ge=0, description="Count of trades analyzed in this window.") diff --git a/src/core/explainability.py b/src/core/explainability.py index 9d6fda04..326d2fdf 100644 --- a/src/core/explainability.py +++ b/src/core/explainability.py @@ -1,8 +1,24 @@ """ MT5 AI/ML Trading Bot - Enterprise Edition src/core/explainability.py + Trade signal explainability and attribution system. -Provides structured breakdowns of why a signal was generated or rejected. +Provides institutional-grade structured breakdowns of why a signal was generated, +including ensemble voting, feature impacts, and execution filter results. + +Usage: + explainer = SignalExplainer() + explanation = explainer.explain( + symbol="XAUUSD", + direction=1, + confidence=0.85, + ... + ) + # Use explainer.format_for_terminal(explanation) for visualization + +All attribution models in this module are immutable (frozen) and enforce strict +validation to ensure technical trust and a reliable audit trail. + Author : triqbit License: MIT """ @@ -13,7 +29,7 @@ import logging from datetime import UTC, datetime from typing import TYPE_CHECKING, Any -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from src.core.constants import ModelAction, SignalDirection @@ -28,11 +44,19 @@ class FeatureContribution(BaseModel): """ Structured contribution from a specific feature cluster. Provides both quantitative (score) and qualitative (impact, summary) attribution. + + This model is immutable (frozen) and forbids extra fields to ensure + consistent and auditable feature impact reporting. """ + model_config = ConfigDict(extra="forbid", frozen=True) + cluster_name: str = Field(..., description="Name of the feature cluster (e.g., Trend, Volatility)") contribution_score: float = Field( - ..., description="Normalized contribution score (-1.0 to 1.0)" + ..., + ge=-1.0, + le=1.0, + description="Normalized contribution score (-1.0 to 1.0).", ) impact_level: str = Field(..., description="Qualitative impact (Low, Medium, High)") summary: str = Field(..., description="Human-readable description of the contribution") @@ -42,12 +66,21 @@ class ModelAttribution(BaseModel): """ Detailed breakdown of an individual model's contribution to the ensemble decision. Tracks alignment, confidence, and relative dominance within the group. + + This model is immutable (frozen) and forbids extra fields to maintain + traceability of the ensemble's internal voting mechanics. """ + model_config = ConfigDict(extra="forbid", frozen=True) + model_name: str = Field(..., description="Name of the model (e.g., PPO, LSTM)") vote: SignalDirection = Field(..., description="The direction voted by this model") - confidence: float = Field(..., description="Model's internal confidence score") - weight: float = Field(..., description="Weight of this model in the ensemble") + confidence: float = Field( + ..., ge=0.0, le=1.0, description="Model's internal confidence score (0.0 to 1.0)." + ) + weight: float = Field( + ..., ge=0.0, le=1.0, description="Weight of this model in the ensemble (0.0 to 1.0)." + ) is_dominant: bool = Field(False, description="Whether this model drove the final decision") @@ -55,8 +88,13 @@ class RiskAssessment(BaseModel): """ Summary of institutional risk management constraints applied to the signal. Captures rejection reasons, R:R quality, and Kelly-based sizing suggestions. + + This model is immutable (frozen) and forbids extra fields to ensure + risk assessments are not modified after generation. """ + model_config = ConfigDict(extra="forbid", frozen=True) + passed: bool = Field(..., description="Whether the signal passed all risk filters") rejection_reasons: list[str] = Field( default_factory=list, description="Reasons for rejection if any" @@ -73,12 +111,19 @@ class RegimeContext(BaseModel): """ Institutional market regime context at the time of signal generation. Decomposes the macro state into regime labels, volatility levels, and strategy favorability. + + This model is immutable (frozen) and forbids extra fields to preserve + the market state snapshot at the time of execution. """ + model_config = ConfigDict(extra="forbid", frozen=True) + regime_name: str = Field( "Unknown", description="Detected market regime (e.g., Trending, Ranging)" ) - confidence: float = Field(0.0, description="Regime detection confidence") + confidence: float = Field( + 0.0, ge=0.0, le=1.0, description="Regime detection confidence (0.0 to 1.0)." + ) volatility_state: str = Field( "Normal", description="Current volatility level (Low, Normal, High, Extreme)" ) @@ -92,8 +137,12 @@ class FilterResult(BaseModel): """ Institutional audit record for an individual execution filter. Captures the pass/fail status along with observed values and their respective thresholds. + + This model is immutable (frozen) and forbids extra fields for technical trust. """ + model_config = ConfigDict(extra="forbid", frozen=True) + filter_name: str = Field(..., description="Name of the filter (e.g., Spread, Momentum)") passed: bool = Field(..., description="Whether the filter passed") value: Any = Field(None, description="Actual value observed") @@ -105,8 +154,13 @@ class ExecutionSummary(BaseModel): """ Unified summary of the institutional execution filter cascade. Aggregates results from multiple technical and operational gates before signal release. + + This model is immutable (frozen) and forbids extra fields to ensure + execution filter results remain consistent across the pipeline. """ + model_config = ConfigDict(extra="forbid", frozen=True) + passed: bool = Field(..., description="Whether all execution filters passed") filters: list[FilterResult] = Field(default_factory=list, description="Detailed filter results") summary: str = Field(..., description="Human-readable execution summary") @@ -117,8 +171,13 @@ class SignalExplanation(BaseModel): Institutional-grade root explanation object for a trade signal. Aggregates execution, model, feature, risk, and regime data into a unified structure suitable for real-time dashboards, post-trade analysis, and regulatory auditing. + + This model is immutable (frozen) and forbids extra fields to ensure that once an + explanation is generated, it cannot be altered by downstream components. """ + model_config = ConfigDict(extra="forbid", frozen=True, use_enum_values=False) + signal_id: int | None = Field(None, description="Database ID of the signal") timestamp: datetime = Field( default_factory=lambda: datetime.now(UTC), @@ -126,7 +185,12 @@ class SignalExplanation(BaseModel): ) symbol: str = Field(..., description="Trading symbol (e.g., XAUUSD)") direction: SignalDirection = Field(..., description="Final ensemble signal direction") - total_confidence: float = Field(..., description="Aggregated ensemble confidence score") + total_confidence: float = Field( + ..., + ge=0.0, + le=1.0, + description="Aggregated ensemble confidence score (0.0 to 1.0).", + ) # Components execution_summary: ExecutionSummary = Field(..., description="Execution-level filter breakdown") @@ -145,8 +209,6 @@ class SignalExplanation(BaseModel): ..., description="Key-value pairs for automated post-trade analysis" ) - model_config = {"use_enum_values": False} - class SignalExplainer: """ @@ -297,19 +359,21 @@ class SignalExplainer: dominant_models.append(name) attributions.append( - ModelAttribution( - model_name=name, - vote=vote_dir, - confidence=model_conf, - weight=weight, - is_dominant=False, # Set in second pass - ) + { + "model_name": name, + "vote": vote_dir, + "confidence": model_conf, + "weight": weight, + "is_dominant": False, # Set in second pass + } ) - # Finalize dominant models - for attr in attributions: - if attr.model_name in dominant_models: - attr.is_dominant = True + # Finalize dominant models and convert to objects + final_attributions = [] + for attr_dict in attributions: + if attr_dict["model_name"] in dominant_models: + attr_dict["is_dominant"] = True + final_attributions.append(ModelAttribution(**attr_dict)) # 3. Risk Assessment risk_assessment = RiskAssessment( @@ -427,7 +491,7 @@ class SignalExplainer: direction=SignalDirection(direction), total_confidence=confidence, execution_summary=execution_summary, - model_attributions=attributions, + model_attributions=final_attributions, feature_contributions=contributions, risk_assessment=risk_assessment, regime_context=regime_context, diff --git a/src/models/regime_detector.py b/src/models/regime_detector.py index b9be8962..7488b362 100644 --- a/src/models/regime_detector.py +++ b/src/models/regime_detector.py @@ -1,7 +1,22 @@ """ MT5 AI/ML Trading Bot - Enterprise Edition src/models/regime_detector.py -Market regime detection for XAUUSD. + +Institutional-grade market regime detection for XAUUSD. +Uses statistical price features and Gaussian Mixture Models (GMM) to classify +market states into categories such as Trending, Ranging, and News Shock. + +Usage: + detector = RegimeDetector() + # For real-time detection + regime_info = detector.detect(ohlcv_df) + + # For historical labeling + df_with_regimes = detector.label_history(large_df) + +Regime data structures are immutable (frozen) and enforce strict confidence +and transition score validation to ensure technical trust in market analysis. + Author : triqbit License: MIT """ @@ -16,7 +31,7 @@ import joblib import numpy as np import pandas as pd import structlog -from pydantic import BaseModel, Field +from pydantic import BaseModel, ConfigDict, Field from scipy import stats from sklearn.mixture import GaussianMixture @@ -51,11 +66,20 @@ class MarketRegime(str, Enum): class RegimeInfo(BaseModel): - """Structured regime detection output with transparency for signal attribution.""" + """ + Structured regime detection output with transparency for signal attribution. + + This model is immutable (frozen) and forbids extra fields to ensure + market regime data is consistent and auditable. + """ + + model_config = ConfigDict(extra="forbid", frozen=True) label: MarketRegime = Field(..., description="Detected regime label") confidence: float = Field(..., ge=0.0, le=1.0, description="Detection confidence") - transition_score: float = Field(..., description="Likelihood of a regime transition") + transition_score: float = Field( + ..., ge=0.0, le=1.0, description="Likelihood of a regime transition" + ) volatility_index: float = Field(..., description="Normalized volatility metric") raw_features: dict[str, float] = Field( default_factory=dict, description="Underlying statistical features used for detection" diff --git a/tests/test_schema_enforcement.py b/tests/test_schema_enforcement.py new file mode 100644 index 00000000..66b5d0c5 --- /dev/null +++ b/tests/test_schema_enforcement.py @@ -0,0 +1,82 @@ +import pytest +from pydantic import ValidationError +from src.core.explainability import ( + FeatureContribution, + ModelAttribution, + RiskAssessment, + RegimeContext, + FilterResult, + ExecutionSummary, + SignalExplanation, +) +from src.core.decision_support import PerformanceContext +from src.models.regime_detector import RegimeInfo, MarketRegime +from src.core.constants import SignalDirection + +def test_explainability_extra_forbid(): + with pytest.raises(ValidationError): + FeatureContribution( + cluster_name="Trend", + contribution_score=0.5, + impact_level="High", + summary="Test", + extra_field="invalid" + ) + +def test_explainability_frozen(): + fc = FeatureContribution( + cluster_name="Trend", + contribution_score=0.5, + impact_level="High", + summary="Test" + ) + with pytest.raises(ValidationError): + fc.contribution_score = 0.8 + +def test_explainability_range_constraints(): + with pytest.raises(ValidationError): + FeatureContribution( + cluster_name="Trend", + contribution_score=1.5, # Should be <= 1.0 + impact_level="High", + summary="Test" + ) + with pytest.raises(ValidationError): + ModelAttribution( + model_name="PPO", + vote=SignalDirection.BUY, + confidence=1.1, # Should be <= 1.0 + weight=0.5 + ) + +def test_performance_context_constraints(): + with pytest.raises(ValidationError): + PerformanceContext(sharpe_ratio=11.0) # Should be <= 10.0 + with pytest.raises(ValidationError): + PerformanceContext(max_drawdown=1.5) # Should be <= 1.0 + with pytest.raises(ValidationError): + PerformanceContext(profit_factor=-1.0) # Should be >= 0.0 + +def test_regime_info_constraints(): + with pytest.raises(ValidationError): + RegimeInfo( + label=MarketRegime.TRENDING, + confidence=1.2, # Should be <= 1.0 + transition_score=0.5, + volatility_index=1.0 + ) + with pytest.raises(ValidationError): + RegimeInfo( + label=MarketRegime.TRENDING, + confidence=0.8, + transition_score=-0.1, # Should be >= 0.0 + volatility_index=1.0 + ) + with pytest.raises(ValidationError): + RegimeInfo( + label=MarketRegime.TRENDING, + confidence=0.8, + transition_score=0.5, + volatility_index=1.0, + extra="forbidden" + )