diff --git a/cli/commands.py b/cli/commands.py index 6fcb280..248e2c8 100644 --- a/cli/commands.py +++ b/cli/commands.py @@ -21,6 +21,7 @@ from cli.tf_config import _configure_tf_metal from src.utils import load_config +from src.core.constants import DIRECTION_DEFAULTS logger = logging.getLogger(__name__) @@ -2699,8 +2700,8 @@ def _buddy_test_modular_ensemble( model_meta = json.loads(model_meta_path.read_text()) if model_meta_path.exists() else {} model_cfg = model_meta.get("config", {}) - threshold_pct = model_cfg.get("direction_threshold", 0.0015) # Default matches training - lookahead = model_cfg.get("direction_lookahead", 24) + threshold_pct = model_cfg.get("direction_threshold", DIRECTION_DEFAULTS['threshold']) + lookahead = model_cfg.get("direction_lookahead", DIRECTION_DEFAULTS['lookahead']) console.print(f"[dim]Using MODEL training settings: threshold={threshold_pct*100:.2f}%, lookahead={lookahead}[/dim]") diff --git a/cli/training.py b/cli/training.py index e77ef30..5147421 100644 --- a/cli/training.py +++ b/cli/training.py @@ -29,6 +29,7 @@ from src.utils import load_config from src.utils.seed_manager import set_global_seed, get_global_seed +from src.core.constants import DIRECTION_DEFAULTS # ── String constants (SonarQube duplicate-literal fix) ────────────── _STYLE_HEADER = "bold cyan" @@ -817,8 +818,8 @@ def _train_ensemble_models( transformer_cfg = cfg.get("transformer", {}) use_transformer = transformer_cfg.get("use_transformer", True) use_regime = transformer_cfg.get("use_regime", False) - direction_threshold = cfg.get("direction_threshold", 0.005) - direction_lookahead = cfg.get("direction_lookahead", 12) + direction_threshold = cfg.get("direction_threshold", DIRECTION_DEFAULTS['threshold']) + direction_lookahead = cfg.get("direction_lookahead", DIRECTION_DEFAULTS['lookahead']) regime_lookback = transformer_cfg.get("regime_lookback", 20) regime_lookahead = transformer_cfg.get("regime_lookahead", 12) diff --git a/src/core/constants.py b/src/core/constants.py new file mode 100644 index 0000000..c3cca74 --- /dev/null +++ b/src/core/constants.py @@ -0,0 +1,36 @@ +""" +Shared constants for ML Engine configuration. + +This module defines authoritative defaults that match config/config_improved_H1.yaml +to prevent configuration drift across the codebase. +""" + +# ============================================================================= +# DIRECTION LABELING DEFAULTS (H1 TIMEFRAME) +# ============================================================================= +# These values match config/config_improved_H1.yaml: +# direction_threshold: 0.003 # Min 0.3% move for clear signal +# direction_lookahead: 24 # 24 hours lookahead +# +# Used by: +# - cli/training.py (training orchestration) +# - src/core/modular_data_loaders.py (data loading functions) +# - cli/commands.py (backtest evaluation) + +DIRECTION_DEFAULTS = { + 'threshold': 0.003, # 0.3% minimum price move for clear signal + 'lookahead': 24, # 24 H1 bars = 24 hours lookahead +} + + +# ============================================================================= +# INFERENCE GATE DEFAULTS +# ============================================================================= +# These values match config/config_improved_H1.yaml inference section + +INFERENCE_DEFAULTS = { + 'min_tcn_probability': 0.60, # 60% minimum confidence (not coin-flip) + 'min_confidence': 50.0, # ADX-based trend strength threshold + 'min_momentum': 0.20, # Momentum percentile threshold + 'max_drawdown_pct': 0.025, # 2.5% max expected drawdown +} diff --git a/src/core/modular_data_loaders.py b/src/core/modular_data_loaders.py index eacec16..dd5a9fd 100644 --- a/src/core/modular_data_loaders.py +++ b/src/core/modular_data_loaders.py @@ -30,6 +30,8 @@ import numpy as np import pandas as pd +from src.core.constants import DIRECTION_DEFAULTS + logger = logging.getLogger(__name__) @@ -1682,8 +1684,8 @@ def load_regime_data( def load_direction_data( df: pd.DataFrame, split: Tuple[float, float, float] = (0.7, 0.2, 0.1), - lookahead: int = 6, - threshold: float = 0.001, # 0.1% minimum move (reduced from 0.5% to include more samples) + lookahead: int = DIRECTION_DEFAULTS['lookahead'], + threshold: float = DIRECTION_DEFAULTS['threshold'], locked_feature_names: Optional[List[str]] = None, gap: int = 0, # Gap between train/val and val/test splits (default 0 for backward compatibility) ) -> Dict[str, np.ndarray]: @@ -3688,8 +3690,8 @@ def validate_no_leakage( def load_all_modular_data( df: pd.DataFrame, split: Tuple[float, float, float] = (0.7, 0.2, 0.1), - direction_threshold: float = 0.005, - direction_lookahead: int = 6, + direction_threshold: float = DIRECTION_DEFAULTS['threshold'], + direction_lookahead: int = DIRECTION_DEFAULTS['lookahead'], use_regime: bool = False, regime_lookback: int = 20, regime_lookahead: int = 12, diff --git a/src/core/modular_inference.py b/src/core/modular_inference.py index 5b5156d..d8817f9 100644 --- a/src/core/modular_inference.py +++ b/src/core/modular_inference.py @@ -45,6 +45,8 @@ import numpy as np import pandas as pd +from .constants import INFERENCE_DEFAULTS + # Import normalized feature computation from data loaders from .modular_data_loaders import ( compute_normalized_features, @@ -416,8 +418,8 @@ class InferenceConfig: min_confidence: float = 50.0 # 0-100 scale (tightened from 45) # Direction-confidence gate (Transformer/primary direction model) - # 0.55 means require >=55% (or <=45%) to consider the direction actionable. - min_tcn_probability: float = 0.55 + # 0.60 means require >=60% (or <=40%) to consider the direction actionable. + min_tcn_probability: float = INFERENCE_DEFAULTS['min_tcn_probability'] # Momentum gate - median momentum is 0.3, so 0.20 catches bottom 40% min_momentum: float = 0.20 # 0-1 scale (tightened from 0.15) @@ -3884,8 +3886,8 @@ def predict( # === TRANSFORMER CONFIDENCE GATE (direction confidence) === # Require reasonable confidence in direction prediction. # Prefer config-driven threshold so this can be tuned without code changes. - # Example: min_tcn_probability=0.55 means require >=55% (or <=45%). - min_dir_prob = float(getattr(self.config, 'min_tcn_probability', 0.55)) + # Example: min_tcn_probability=0.60 means require >=60% (or <=40%). + min_dir_prob = float(getattr(self.config, 'min_tcn_probability', INFERENCE_DEFAULTS['min_tcn_probability'])) min_dir_prob = max(0.5, min(0.99, min_dir_prob)) direction_confidence_gate_passed = ( tcn_probability >= min_dir_prob or tcn_probability <= (1.0 - min_dir_prob) @@ -4082,7 +4084,7 @@ def predict( else: reasons.append(f"low_volatility_regime({volatility_regime_name or 'N/A'})") if not direction_confidence_gate_passed: - min_dir_prob = float(getattr(self.config, 'min_tcn_probability', 0.55)) + min_dir_prob = float(getattr(self.config, 'min_tcn_probability', INFERENCE_DEFAULTS['min_tcn_probability'])) min_dir_prob = max(0.5, min(0.99, min_dir_prob)) reasons.append(f"weak_direction_conf({tcn_probability:.2f}<{min_dir_prob:.2f})") if self.use_regime: diff --git a/tests/test_unified_defaults.py b/tests/test_unified_defaults.py new file mode 100644 index 0000000..fc07256 --- /dev/null +++ b/tests/test_unified_defaults.py @@ -0,0 +1,97 @@ +""" +Test suite for unified configuration defaults. + +Verifies that: +1. Constants module defines correct values +2. All code locations use the shared constants +3. Values match config/config_improved_H1.yaml +""" + +import pytest +import yaml +from pathlib import Path + + +def test_direction_defaults_exist(): + """Test that DIRECTION_DEFAULTS constant is defined correctly.""" + from src.core.constants import DIRECTION_DEFAULTS + + assert 'threshold' in DIRECTION_DEFAULTS + assert 'lookahead' in DIRECTION_DEFAULTS + assert DIRECTION_DEFAULTS['threshold'] == 0.003 + assert DIRECTION_DEFAULTS['lookahead'] == 24 + + +def test_inference_defaults_exist(): + """Test that INFERENCE_DEFAULTS constant is defined correctly.""" + from src.core.constants import INFERENCE_DEFAULTS + + assert 'min_tcn_probability' in INFERENCE_DEFAULTS + assert 'min_confidence' in INFERENCE_DEFAULTS + assert 'min_momentum' in INFERENCE_DEFAULTS + assert 'max_drawdown_pct' in INFERENCE_DEFAULTS + + assert INFERENCE_DEFAULTS['min_tcn_probability'] == 0.60 + assert INFERENCE_DEFAULTS['min_confidence'] == 50.0 + assert INFERENCE_DEFAULTS['min_momentum'] == 0.20 + assert INFERENCE_DEFAULTS['max_drawdown_pct'] == 0.025 + + +def test_constants_match_yaml_config(): + """Test that constants match config/config_improved_H1.yaml.""" + from src.core.constants import DIRECTION_DEFAULTS, INFERENCE_DEFAULTS + + config_path = Path(__file__).parent.parent / 'config' / 'config_improved_H1.yaml' + with open(config_path, 'r') as f: + config = yaml.safe_load(f) + + # Check direction defaults + assert DIRECTION_DEFAULTS['threshold'] == config['direction_threshold'] + assert DIRECTION_DEFAULTS['lookahead'] == config['direction_lookahead'] + + # Check inference defaults + inference_config = config.get('inference', {}) + assert INFERENCE_DEFAULTS['min_tcn_probability'] == inference_config.get('min_tcn_probability') + assert INFERENCE_DEFAULTS['min_confidence'] == inference_config.get('min_confidence') + assert INFERENCE_DEFAULTS['min_momentum'] == inference_config.get('min_momentum') + assert INFERENCE_DEFAULTS['max_drawdown_pct'] == inference_config.get('max_drawdown_pct') + + +def test_inference_config_uses_constant(): + """Test that InferenceConfig uses INFERENCE_DEFAULTS for min_tcn_probability.""" + from src.core.modular_inference import InferenceConfig + from src.core.constants import INFERENCE_DEFAULTS + + config = InferenceConfig() + assert config.min_tcn_probability == INFERENCE_DEFAULTS['min_tcn_probability'] + assert config.min_tcn_probability == 0.60 + + +def test_load_direction_data_signature(): + """Test that load_direction_data function signature uses constants.""" + from src.core.modular_data_loaders import load_direction_data + from src.core.constants import DIRECTION_DEFAULTS + import inspect + + sig = inspect.signature(load_direction_data) + + # Check default values in signature + assert sig.parameters['lookahead'].default == DIRECTION_DEFAULTS['lookahead'] + assert sig.parameters['threshold'].default == DIRECTION_DEFAULTS['threshold'] + + +def test_load_all_modular_data_signature(): + """Test that load_all_modular_data function signature uses constants.""" + from src.core.modular_data_loaders import load_all_modular_data + from src.core.constants import DIRECTION_DEFAULTS + import inspect + + sig = inspect.signature(load_all_modular_data) + + # Check default values in signature + assert sig.parameters['direction_threshold'].default == DIRECTION_DEFAULTS['threshold'] + assert sig.parameters['direction_lookahead'].default == DIRECTION_DEFAULTS['lookahead'] + + +if __name__ == '__main__': + pytest.main([__file__, '-v'])