Add yfinance fallback for gold/silver + coarse Telegram progress
- yfinance>=0.2.40 added as a hard dependency
- _fetch_yfinance_fallback(): pulls OHLCV from Yahoo for symbols not on
the Twelve Data free tier (XAUUSD → GC=F, XAGUSD → SI=F)
- Hooked into both batch and single-symbol fetch paths; logs as [yf]
to distinguish from Twelve Data [live]
- Fail-loud: if both providers return nothing, the pair is skipped with
the existing "data unavailable" message
- _analyze_all_tfs accepts an async progress callback; both /signal and
/scan now show coarse milestones via message edits:
"📡 Fetching market data..." → "🧠 Analyzing pairs..." → result
- prefetch_batch is now run in a thread so it doesn't block the event loop
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,11 +17,35 @@ from typing import Dict, List, Optional
|
||||
import pandas as pd
|
||||
import requests
|
||||
|
||||
try:
|
||||
import yfinance as yf
|
||||
_YF_AVAILABLE = True
|
||||
except ImportError:
|
||||
yf = None
|
||||
_YF_AVAILABLE = False
|
||||
|
||||
from config import PlatformConfig, Timeframe, SYMBOL_DISPLAY, PIP_VALUES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
_no_data_warned: set = set() # suppress duplicate "no data" warnings per symbol
|
||||
|
||||
# Symbols that Twelve Data free tier does not support — route to Yahoo Finance.
|
||||
# Gold/silver futures are the most reliable Yahoo symbols (full OHLCV history).
|
||||
_YF_FALLBACK_MAP = {
|
||||
"XAUUSD=X": "GC=F",
|
||||
"XAGUSD=X": "SI=F",
|
||||
}
|
||||
|
||||
_YF_INTERVALS = {
|
||||
Timeframe.M1: "1m",
|
||||
Timeframe.M5: "5m",
|
||||
Timeframe.M15: "15m",
|
||||
Timeframe.M30: "30m",
|
||||
Timeframe.H1: "1h",
|
||||
Timeframe.D1: "1d",
|
||||
Timeframe.W1: "1wk",
|
||||
}
|
||||
|
||||
|
||||
class DataIntegrityError(Exception):
|
||||
"""Raised when data fails validation checks."""
|
||||
@@ -227,6 +251,57 @@ class ForexDataProvider:
|
||||
|
||||
# ── Private Methods ────────────────────────────────────────────
|
||||
|
||||
def _fetch_yfinance_fallback(
|
||||
self, symbol: str, timeframe: Timeframe
|
||||
) -> Optional[pd.DataFrame]:
|
||||
"""
|
||||
Fetch OHLCV from Yahoo Finance for symbols Twelve Data doesn't cover
|
||||
on the free tier (XAUUSD, XAGUSD). Returns None if not mapped or fails.
|
||||
"""
|
||||
if not _YF_AVAILABLE:
|
||||
return None
|
||||
yf_symbol = _YF_FALLBACK_MAP.get(symbol)
|
||||
yf_interval = _YF_INTERVALS.get(timeframe)
|
||||
if yf_symbol is None or yf_interval is None:
|
||||
return None
|
||||
|
||||
# Yahoo intraday limits: 1m → last 7d, 15m/30m/1h → last 60d
|
||||
if yf_interval in ("1m", "2m"):
|
||||
period = "7d"
|
||||
elif yf_interval in ("5m", "15m", "30m", "60m", "1h"):
|
||||
period = "60d"
|
||||
else:
|
||||
period = "2y"
|
||||
|
||||
try:
|
||||
df = yf.download(
|
||||
yf_symbol, interval=yf_interval, period=period,
|
||||
progress=False, auto_adjust=False, threads=False,
|
||||
)
|
||||
if df is None or df.empty:
|
||||
return None
|
||||
|
||||
# Single-symbol download can return MultiIndex columns in newer yfinance versions
|
||||
if isinstance(df.columns, pd.MultiIndex):
|
||||
df.columns = df.columns.get_level_values(0)
|
||||
|
||||
# Normalise to UTC tz-aware index
|
||||
if df.index.tz is None:
|
||||
df.index = df.index.tz_localize("UTC")
|
||||
else:
|
||||
df.index = df.index.tz_convert("UTC")
|
||||
df.index.name = "Datetime"
|
||||
|
||||
df = df[["Open", "High", "Low", "Close", "Volume"]].copy()
|
||||
logger.info(
|
||||
f"[yf] {yf_symbol} {yf_interval}: {len(df)} bars, "
|
||||
f"last close={df['Close'].iloc[-1]:.5f}"
|
||||
)
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.warning(f"yfinance fallback failed for {symbol} {timeframe.value}: {e}")
|
||||
return None
|
||||
|
||||
def _throttle(self, credits: int = 1) -> None:
|
||||
"""
|
||||
Block until `credits` per-minute API credits are available.
|
||||
@@ -343,6 +418,14 @@ class ForexDataProvider:
|
||||
|
||||
values = sym_data.get("values")
|
||||
if not values:
|
||||
# Try yfinance fallback for unsupported symbols (XAU, XAG)
|
||||
yf_df = self._fetch_yfinance_fallback(symbol, timeframe)
|
||||
if yf_df is not None:
|
||||
yf_df = self._validate_and_clean(yf_df, symbol, timeframe)
|
||||
cache_key = f"{symbol}_{timeframe.value}"
|
||||
self._cache[cache_key] = yf_df
|
||||
self._cache_timestamps[cache_key] = now
|
||||
continue
|
||||
if td_sym not in _no_data_warned:
|
||||
logger.warning(f"No data from Twelve Data for {td_sym} — symbol may require a paid plan.")
|
||||
_no_data_warned.add(td_sym)
|
||||
@@ -378,8 +461,13 @@ class ForexDataProvider:
|
||||
"""
|
||||
Fetch OHLCV from Twelve Data REST API.
|
||||
Raises DataUnavailableError when data cannot be retrieved.
|
||||
Falls back to yfinance for symbols not on the Twelve Data free tier.
|
||||
"""
|
||||
if not self._api_key:
|
||||
# No Twelve Data key — try yfinance fallback for mapped symbols
|
||||
yf_df = self._fetch_yfinance_fallback(symbol, timeframe)
|
||||
if yf_df is not None:
|
||||
return yf_df
|
||||
raise DataUnavailableError(
|
||||
"No Twelve Data API key — set the TWELVE_DATA_API_KEY environment variable."
|
||||
)
|
||||
@@ -417,6 +505,9 @@ class ForexDataProvider:
|
||||
|
||||
values = data.get("values")
|
||||
if not values:
|
||||
yf_df = self._fetch_yfinance_fallback(symbol, timeframe)
|
||||
if yf_df is not None:
|
||||
return yf_df
|
||||
raise DataUnavailableError(
|
||||
f"No data returned for {td_symbol} — symbol may require a paid plan."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user