"""Configuration loader — reads config.yaml with sensible defaults."""
import os
import yaml
from typing import Any, Dict, List
CONFIG_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "config.yaml")
_DEFAULTS: Dict[str, Any] = {
"app": {"host": "0.0.0.0", "port": 5080},
"data": {
"tickers": ["AAPL", "MSFT", "GOOGL", "AMZN", "TSLA"],
"interval": "1d",
"period": "max",
"lookahead": 5,
},
"features": {
"sma_periods": [10, 20, 50, 100],
"ema_periods": [12, 26, 50],
"rsi_period": 14,
"macd_fast": 12, "macd_slow": 26, "macd_signal": 9,
"bb_period": 20, "bb_std": 2,
"volume_sma": 20,
"atr_period": 14,
"stoch_k": 14, "stoch_d": 3,
},
"model": {
"type": "xgboost",
"classification_threshold": 0.55,
"train_test_split": 0.2,
"n_estimators": 200,
"max_depth": 6,
"learning_rate": 0.1,
"random_state": 42,
},
"alerts": {
"enabled": True,
"price_change_pct": 5.0,
"rsi_overbought": 70,
"rsi_oversold": 30,
"macd_crossover": True,
"prediction_confidence": 0.65,
},
"scheduler": {"interval_hours": 4, "auto_retrain": True},
"storage": {"data_dir": "data/cache", "model_dir": "models/saved"},
}
def _deep_merge(base: dict, override: dict) -> dict:
"""Merge override into base recursively."""
result = base.copy()
for k, v in override.items():
if k in result and isinstance(result[k], dict) and isinstance(v, dict):
result[k] = _deep_merge(result[k], v)
else:
result[k] = v
return result
class Config:
"""Singleton config with section access."""
_instance = None
_data: Dict[str, Any] = {}
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
if not self._data:
self.load()
def load(self):
if os.path.exists(CONFIG_PATH):
with open(CONFIG_PATH) as f:
user_config = yaml.safe_load(f) or {}
self._data = _deep_merge(_DEFAULTS, user_config)
else:
self._data = _DEFAULTS.copy()
def get(self, key: str, default: Any = None) -> Any:
"""Dot-notation access: config.get('data.tickers')."""
keys = key.split(".")
val = self._data
for k in keys:
if isinstance(val, dict):
val = val.get(k)
else:
return default
if val is None:
return default
return val
@property
def app(self) -> dict:
return self._data["app"]
@property
def data(self) -> dict:
return self._data["data"]
@property
def features(self) -> dict:
return self._data["features"]
@property
def model(self) -> dict:
return self._data["model"]
@property
def alerts(self) -> dict:
return self._data["alerts"]
@property
def scheduler(self) -> dict:
return self._data["scheduler"]
@property
def storage(self) -> dict:
return self._data["storage"]
def reload(self):
"""Hot-reload config file."""
self._data = {}
self.load()
# Global instance
config = Config()