From 44cadf7a9f1f197345ec22ccda8872a7c53c5cec Mon Sep 17 00:00:00 2001 From: Harwinder Sekhon Date: Tue, 21 Jul 2026 13:10:58 -0400 Subject: [PATCH] Data provider: key from config.json/env, provider registry, drop hardcoded key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves the Twelve Data API key through app_config.provider_api_key() (env → config.json → no default) instead of a hardcoded field on PlatformConfig. Adds data_provider.PROVIDER_REGISTRY + make_data_provider() so signal_aggregator and the backtester build the active provider through one seam; telegram_bot.py gets the same one-line WS-key fix now (per Task 4's already-planned edit for that line) so the suite doesn't crash on import between this commit and Task 4's full wiring pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01VPNF37MP9Kuv62B1VBrZ1b --- backtest/gpu_backtest.py | 4 ++-- config.py | 2 -- data_provider.py | 28 ++++++++++++++++++++------- signal_aggregator.py | 4 ++-- telegram_bot.py | 3 ++- test_all.py | 41 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 68 insertions(+), 14 deletions(-) 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 # ══════════════════════════════════════════════════════════════════════