Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand Down Expand Up @@ -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]")

Expand Down
5 changes: 3 additions & 2 deletions cli/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)

Expand Down
36 changes: 36 additions & 0 deletions src/core/constants.py
Original file line number Diff line number Diff line change
@@ -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
}
10 changes: 6 additions & 4 deletions src/core/modular_data_loaders.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
import numpy as np
import pandas as pd

from src.core.constants import DIRECTION_DEFAULTS

logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions src/core/modular_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
97 changes: 97 additions & 0 deletions tests/test_unified_defaults.py
Original file line number Diff line number Diff line change
@@ -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'])
Loading