Data provider: key from config.json/env, provider registry, drop hardcoded key
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VPNF37MP9Kuv62B1VBrZ1b
This commit is contained in:
@@ -55,7 +55,7 @@ from config import ( # noqa: E402
|
|||||||
Timeframe,
|
Timeframe,
|
||||||
XAUUSD,
|
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 engine import IndicatorSignal, TechnicalAnalysis, TechnicalEngine # noqa: E402
|
||||||
from signal_aggregator import SignalAggregator, SignalReport, TradeSetup # 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}"
|
f"vs PlatformConfig={platform_cfg.primary_timeframe.value}"
|
||||||
)
|
)
|
||||||
|
|
||||||
provider = ForexDataProvider(platform_cfg)
|
provider = make_data_provider(platform_cfg)
|
||||||
|
|
||||||
# ── Load all required timeframes ──
|
# ── Load all required timeframes ──
|
||||||
timeframes = [bt_cfg.primary_tf] + list(bt_cfg.confirmation_tfs)
|
timeframes = [bt_cfg.primary_tf] + list(bt_cfg.confirmation_tfs)
|
||||||
|
|||||||
@@ -343,8 +343,6 @@ class PlatformConfig:
|
|||||||
|
|
||||||
data_lookback_days: int = 60
|
data_lookback_days: int = 60
|
||||||
|
|
||||||
twelve_data_api_key: str = "f563b22ffdfd4924955decbe1a8c8701"
|
|
||||||
|
|
||||||
refresh_interval: int = 300
|
refresh_interval: int = 300
|
||||||
|
|
||||||
scan_pairs: List[str] = field(
|
scan_pairs: List[str] = field(
|
||||||
|
|||||||
+21
-7
@@ -5,7 +5,6 @@ Fetches and validates OHLCV via the official `twelvedata` Python SDK.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from collections import deque
|
from collections import deque
|
||||||
@@ -14,6 +13,7 @@ from typing import Dict, Optional
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from twelvedata import TDClient
|
from twelvedata import TDClient
|
||||||
|
|
||||||
|
from app_config import AppConfigError, get_app_config, provider_api_key
|
||||||
from config import PlatformConfig, Timeframe, XAUUSD, PAIR_INFO
|
from config import PlatformConfig, Timeframe, XAUUSD, PAIR_INFO
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -70,10 +70,7 @@ class ForexDataProvider:
|
|||||||
|
|
||||||
def __init__(self, config: PlatformConfig):
|
def __init__(self, config: PlatformConfig):
|
||||||
self.config = config
|
self.config = config
|
||||||
self._api_key = (
|
self._api_key = provider_api_key() or None
|
||||||
os.environ.get("TWELVE_DATA_API_KEY")
|
|
||||||
or config.twelve_data_api_key
|
|
||||||
)
|
|
||||||
self._cache: Dict[str, pd.DataFrame] = {}
|
self._cache: Dict[str, pd.DataFrame] = {}
|
||||||
self._cache_timestamps: Dict[str, float] = {}
|
self._cache_timestamps: Dict[str, float] = {}
|
||||||
# The bot shares one provider across a thread pool; cache eviction
|
# The bot shares one provider across a thread pool; cache eviction
|
||||||
@@ -264,8 +261,8 @@ class ForexDataProvider:
|
|||||||
) -> pd.DataFrame:
|
) -> pd.DataFrame:
|
||||||
if self._td is None:
|
if self._td is None:
|
||||||
raise DataIntegrityError(
|
raise DataIntegrityError(
|
||||||
"No Twelve Data API key. Set TWELVE_DATA_API_KEY env var "
|
"No data-provider API key. Set the TWELVE_DATA_API_KEY env "
|
||||||
"or config.twelve_data_api_key."
|
"var or data_providers.twelve_data.api_key in config.json."
|
||||||
)
|
)
|
||||||
|
|
||||||
td_symbol = self._to_td_symbol(symbol)
|
td_symbol = self._to_td_symbol(symbol)
|
||||||
@@ -384,3 +381,20 @@ class ForexDataProvider:
|
|||||||
logger.info(f"{symbol}: Cleaned {cleaned}/{original_len} bars")
|
logger.info(f"{symbol}: Cleaned {cleaned}/{original_len} bars")
|
||||||
|
|
||||||
return df
|
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)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from chart_patterns import detect_chart_pattern
|
from chart_patterns import detect_chart_pattern
|
||||||
from config import PlatformConfig, SignalStrength, Timeframe
|
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 engine import TechnicalEngine, TechnicalAnalysis
|
||||||
from news_engine import NewsEngine, FundamentalContext
|
from news_engine import NewsEngine, FundamentalContext
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ class SignalAggregator:
|
|||||||
|
|
||||||
def __init__(self, config: PlatformConfig):
|
def __init__(self, config: PlatformConfig):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.data_provider = ForexDataProvider(config)
|
self.data_provider = make_data_provider(config)
|
||||||
self.tech_engine = TechnicalEngine(config.technical)
|
self.tech_engine = TechnicalEngine(config.technical)
|
||||||
self._news_engine: Optional[NewsEngine] = None # lazy
|
self._news_engine: Optional[NewsEngine] = None # lazy
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -65,6 +65,7 @@ from config import (
|
|||||||
PlatformConfig, SignalStrength, Timeframe,
|
PlatformConfig, SignalStrength, Timeframe,
|
||||||
XAUUSD, PAIR_INFO,
|
XAUUSD, PAIR_INFO,
|
||||||
)
|
)
|
||||||
|
from app_config import provider_api_key
|
||||||
from data_provider import ForexDataProvider
|
from data_provider import ForexDataProvider
|
||||||
from pair_signals import PAIR_PRODUCERS
|
from pair_signals import PAIR_PRODUCERS
|
||||||
from signal_aggregator import SignalAggregator, SignalReport
|
from signal_aggregator import SignalAggregator, SignalReport
|
||||||
@@ -609,7 +610,7 @@ _last_price_ts: Dict[str, float] = {}
|
|||||||
_scan_cooldowns: Dict[str, float] = {}
|
_scan_cooldowns: Dict[str, float] = {}
|
||||||
|
|
||||||
_cfg = PlatformConfig()
|
_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_URL = f"wss://ws.twelvedata.com/v1/quotes/price?apikey={_WS_API_KEY}"
|
||||||
_WS_COOLDOWN = _cfg.ws_cooldown_seconds
|
_WS_COOLDOWN = _cfg.ws_cooldown_seconds
|
||||||
|
|
||||||
|
|||||||
+41
@@ -2725,6 +2725,47 @@ def _():
|
|||||||
f"example config.json must be byte-identical to defaults on {tf}"
|
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
|
# SUMMARY
|
||||||
# ══════════════════════════════════════════════════════════════════════
|
# ══════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
Reference in New Issue
Block a user