Switch data source to Finnhub API; remove synthetic data fallback

Adds FinnhubDataProvider as a drop-in replacement for ForexDataProvider.
Rate limit or API errors raise DataUnavailableError immediately — the
Telegram bot shows a clean per-TF error card rather than fake prices.

- data_provider_finnhub.py: Finnhub client, OANDA symbol mapping,
  4h resampled from 60-min candles, shared module-level rate limiter
  (60 calls/min free tier), full OHLCV validation copied from original
- signal_aggregator.py: import FinnhubDataProvider alias ForexDataProvider
- config.py: add finnhub_api_key field (FINNHUB_API_KEY env var overrides)
- telegram_bot.py: remove synthetic-data warning and _SUPPORTED_PAIRS;
  unsupported pairs now show the engine's "data unavailable" card
- requirements.txt: add finnhub-python>=1.0

Set FINNHUB_API_KEY env var before running. main branch is untouched.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Harwinder
2026-05-04 20:12:41 -04:00
parent e6ef6e8c9d
commit 43cc4bdd42
5 changed files with 397 additions and 14 deletions
+6 -13
View File
@@ -30,7 +30,7 @@ from telegram.constants import ParseMode
from telegram.ext import Application, CommandHandler, ContextTypes
from config import (
MAJOR_PAIRS, CROSS_PAIRS, SYMBOL_DISPLAY,
MAJOR_PAIRS, SYMBOL_DISPLAY,
PlatformConfig, SignalStrength, Timeframe,
)
from signal_aggregator import SignalAggregator, SignalReport
@@ -54,8 +54,7 @@ _TF_CONFIRMATIONS = {
"D1": [Timeframe.H4, Timeframe.W1],
}
_SUPPORTED_PAIRS = set(MAJOR_PAIRS + CROSS_PAIRS)
_GRADE_RANK = {"A+": 0, "A": 1, "B": 2, "C": 3, "D": 4, "F": 5}
_GRADE_RANK = {"A+": 0, "A": 1, "B": 2, "C": 3, "D": 4, "F": 5}
_EST = ZoneInfo("America/New_York")
# ── Formatting helpers ──────────────────────────────────────────────
@@ -114,7 +113,6 @@ def _build_combined_signal(
results_by_tf: Dict[str, List[SignalReport]],
display: str,
now_str: str,
synthetic: bool = False,
) -> str:
"""
Single message showing all timeframes stacked.
@@ -125,11 +123,7 @@ def _build_combined_signal(
lines.append(f"🌐 <b>{_h(display)} — Signal Analysis</b>")
lines.append(f"🕐 {now_str}")
if synthetic:
lines.append(
"⚠️ <b>Note:</b> Not on Twelve Data free tier — "
"prices are <b>simulated</b> data."
)
# Finnhub branch: no synthetic data — real prices or error shown per-TF.
for tf in SCAN_TIMEFRAMES:
reports = results_by_tf.get(tf, [])
@@ -360,9 +354,8 @@ class ForexTelegramBot:
raw = args[0].upper().replace("/", "").replace("_", "")
if not raw.endswith("=X"):
raw += "=X"
display = SYMBOL_DISPLAY.get(raw, raw.replace("=X", ""))
tfs_str = " · ".join(SCAN_TIMEFRAMES)
synthetic = raw not in _SUPPORTED_PAIRS
display = SYMBOL_DISPLAY.get(raw, raw.replace("=X", ""))
tfs_str = " · ".join(SCAN_TIMEFRAMES)
msg = await update.message.reply_text(
f"🔄 Analyzing {_h(display)} across {_h(tfs_str)}…"
@@ -376,7 +369,7 @@ class ForexTelegramBot:
await msg.edit_text(f"❌ No data returned for {_h(display)}")
return
text = _build_combined_signal(results_by_tf, display, now_str, synthetic)
text = _build_combined_signal(results_by_tf, display, now_str)
await msg.delete()
await self._send_html(update, text)