from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any import yaml @dataclass(frozen=True) class StrategyWatch: id: str enabled: bool timeframe: str telegram_thread_id: int | None @dataclass(frozen=True) class Watchlist: symbols: list[str] strategies: dict[str, StrategyWatch] def load_watchlist(path: str | Path) -> Watchlist: raw_path = Path(path) if not raw_path.is_file(): raise FileNotFoundError(f"Watchlist not found: {raw_path}") data: Any = yaml.safe_load(raw_path.read_text(encoding="utf-8")) or {} symbols_raw = data.get("symbols") or [] symbols = [str(s).strip().upper() for s in symbols_raw if str(s).strip()] if not symbols: raise ValueError("Watchlist has no symbols") strategies: dict[str, StrategyWatch] = {} for key, cfg in (data.get("strategies") or {}).items(): if not isinstance(cfg, dict): continue strategies[str(key)] = StrategyWatch( id=str(key), enabled=bool(cfg.get("enabled", True)), timeframe=str(cfg.get("timeframe", "")).strip(), telegram_thread_id=_optional_int(cfg.get("telegram_thread_id")), ) if "ltf" not in strategies: strategies["ltf"] = StrategyWatch("ltf", True, "15m", None) if "fvg" not in strategies: strategies["fvg"] = StrategyWatch("fvg", True, "6h", None) return Watchlist(symbols=symbols, strategies=strategies) def _optional_int(value: object) -> int | None: if value is None or value == "": return None return int(value)