diff --git a/backtest/gpu_backtest.py b/backtest/gpu_backtest.py index ccf180c..bee59aa 100644 --- a/backtest/gpu_backtest.py +++ b/backtest/gpu_backtest.py @@ -55,7 +55,7 @@ from config import ( # noqa: E402 Timeframe, XAUUSD, ) -from data_provider import ForexDataProvider # noqa: E402 +from data_provider import ForexDataProvider, make_data_provider # noqa: E402 from engine import IndicatorSignal, TechnicalAnalysis, TechnicalEngine # noqa: E402 from signal_aggregator import SignalAggregator, SignalReport, TradeSetup # noqa: E402 @@ -1679,7 +1679,7 @@ def run_backtest( f"vs PlatformConfig={platform_cfg.primary_timeframe.value}" ) - provider = ForexDataProvider(platform_cfg) + provider = make_data_provider(platform_cfg) # ── Load all required timeframes ── timeframes = [bt_cfg.primary_tf] + list(bt_cfg.confirmation_tfs) diff --git a/config.py b/config.py index 6b2ce22..336ee95 100755 --- a/config.py +++ b/config.py @@ -343,8 +343,6 @@ class PlatformConfig: data_lookback_days: int = 60 - twelve_data_api_key: str = "f563b22ffdfd4924955decbe1a8c8701" - refresh_interval: int = 300 scan_pairs: List[str] = field( diff --git a/data_provider.py b/data_provider.py index 69e5256..018c1b0 100755 --- a/data_provider.py +++ b/data_provider.py @@ -5,7 +5,6 @@ Fetches and validates OHLCV via the official `twelvedata` Python SDK. """ import logging -import os import threading import time from collections import deque @@ -14,6 +13,7 @@ from typing import Dict, Optional import pandas as pd from twelvedata import TDClient +from app_config import AppConfigError, get_app_config, provider_api_key from config import PlatformConfig, Timeframe, XAUUSD, PAIR_INFO logger = logging.getLogger(__name__) @@ -70,10 +70,7 @@ class ForexDataProvider: def __init__(self, config: PlatformConfig): self.config = config - self._api_key = ( - os.environ.get("TWELVE_DATA_API_KEY") - or config.twelve_data_api_key - ) + self._api_key = provider_api_key() or None self._cache: Dict[str, pd.DataFrame] = {} self._cache_timestamps: Dict[str, float] = {} # The bot shares one provider across a thread pool; cache eviction @@ -264,8 +261,8 @@ class ForexDataProvider: ) -> pd.DataFrame: if self._td is None: raise DataIntegrityError( - "No Twelve Data API key. Set TWELVE_DATA_API_KEY env var " - "or config.twelve_data_api_key." + "No data-provider API key. Set the TWELVE_DATA_API_KEY env " + "var or data_providers.twelve_data.api_key in config.json." ) td_symbol = self._to_td_symbol(symbol) @@ -384,3 +381,20 @@ class ForexDataProvider: logger.info(f"{symbol}: Cleaned {cleaned}/{original_len} bars") return df + + +# ── Provider registry ───────────────────────────────────────────────── +# config.json data_providers.active selects from here. Adding a provider +# later = one adapter class + one entry + one config block. +PROVIDER_REGISTRY: Dict[str, type] = {"twelve_data": ForexDataProvider} + + +def make_data_provider(config: PlatformConfig) -> ForexDataProvider: + """Build the ACTIVE data provider (config.json: data_providers.active).""" + name = get_app_config().active_provider + cls = PROVIDER_REGISTRY.get(name) + if cls is None: + raise AppConfigError( + f'Unknown data provider "{name}"; ' + f"registered: {sorted(PROVIDER_REGISTRY)}") + return cls(config) diff --git a/signal_aggregator.py b/signal_aggregator.py index 0d75ff3..d24dc7d 100755 --- a/signal_aggregator.py +++ b/signal_aggregator.py @@ -15,7 +15,7 @@ import pandas as pd from chart_patterns import detect_chart_pattern from config import PlatformConfig, SignalStrength, Timeframe -from data_provider import ForexDataProvider +from data_provider import ForexDataProvider, make_data_provider from engine import TechnicalEngine, TechnicalAnalysis from news_engine import NewsEngine, FundamentalContext @@ -104,7 +104,7 @@ class SignalAggregator: def __init__(self, config: PlatformConfig): self.config = config - self.data_provider = ForexDataProvider(config) + self.data_provider = make_data_provider(config) self.tech_engine = TechnicalEngine(config.technical) self._news_engine: Optional[NewsEngine] = None # lazy diff --git a/telegram_bot.py b/telegram_bot.py index 5c06083..182a570 100755 --- a/telegram_bot.py +++ b/telegram_bot.py @@ -65,6 +65,7 @@ from config import ( PlatformConfig, SignalStrength, Timeframe, XAUUSD, PAIR_INFO, ) +from app_config import provider_api_key from data_provider import ForexDataProvider from pair_signals import PAIR_PRODUCERS from signal_aggregator import SignalAggregator, SignalReport @@ -609,7 +610,7 @@ _last_price_ts: Dict[str, float] = {} _scan_cooldowns: Dict[str, float] = {} _cfg = PlatformConfig() -_WS_API_KEY = os.environ.get("TWELVE_DATA_API_KEY") or _cfg.twelve_data_api_key +_WS_API_KEY = provider_api_key() _WS_URL = f"wss://ws.twelvedata.com/v1/quotes/price?apikey={_WS_API_KEY}" _WS_COOLDOWN = _cfg.ws_cooldown_seconds diff --git a/test_all.py b/test_all.py index 4742056..c2b5e4c 100644 --- a/test_all.py +++ b/test_all.py @@ -2725,6 +2725,47 @@ def _(): f"example config.json must be byte-identical to defaults on {tf}" +@test("data_provider: api key resolves env → config.json (no code default)") +def _(): + from data_provider import ForexDataProvider + app = AppConfig(provider_settings={"twelve_data": {"api_key": "json-key"}}) + set_app_config(app) + try: + with patch.dict(os.environ, {}, clear=True): + dp = ForexDataProvider(PlatformConfig()) + assert dp._api_key == "json-key" + with patch.dict(os.environ, {"TWELVE_DATA_API_KEY": "env-key"}): + dp = ForexDataProvider(PlatformConfig()) + assert dp._api_key == "env-key" + set_app_config(AppConfig()) # no key anywhere + with patch.dict(os.environ, {}, clear=True): + dp = ForexDataProvider(PlatformConfig()) + assert dp._api_key is None and dp._td is None + finally: + set_app_config(_TEST_APP) + + +@test("data_provider: make_data_provider dispatches on active provider") +def _(): + from data_provider import ForexDataProvider, make_data_provider + assert isinstance(make_data_provider(PlatformConfig()), ForexDataProvider) + set_app_config(AppConfig(active_provider="polygon", + provider_settings={"polygon": {"api_key": "k"}})) + try: + make_data_provider(PlatformConfig()) + assert False, "unregistered provider must raise" + except AppConfigError as e: + assert "polygon" in str(e) + finally: + set_app_config(_TEST_APP) + + +@test("config.py: hardcoded twelve_data_api_key field is gone") +def _(): + assert not hasattr(PlatformConfig(), "twelve_data_api_key"), \ + "credentials must not live in committed source" + + # ══════════════════════════════════════════════════════════════════════ # SUMMARY # ══════════════════════════════════════════════════════════════════════