From b124d0a7a3b5b696b98da87f129d538d3cbe6b37 Mon Sep 17 00:00:00 2001 From: Harwinder Date: Mon, 4 May 2026 20:26:10 -0400 Subject: [PATCH] Handle Finnhub 403; make exchange prefix configurable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit OANDA forex candles require a paid plan — free-tier keys get 403. - Add FINNHUB_FOREX_EXCHANGE env var (default OANDA) so the exchange prefix can be changed without touching code - Handle 403 specifically: clear error message tells the user which exchange failed and how to discover what their key can access - Add discover_exchanges() method wrapping forex_exchanges() API call so the right prefix can be found before configuring the bot Co-Authored-By: Claude Sonnet 4.6 --- data_provider_finnhub.py | 45 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/data_provider_finnhub.py b/data_provider_finnhub.py index f573e76..7b37ab5 100644 --- a/data_provider_finnhub.py +++ b/data_provider_finnhub.py @@ -57,6 +57,10 @@ class FinnhubDataProvider: H4 bars are synthesised client-side by resampling 60-minute candles. """ + # Forex exchange/broker prefix — override with FINNHUB_FOREX_EXCHANGE env var. + # OANDA requires a paid plan. Run discover_exchanges() to see what your key can access. + DEFAULT_EXCHANGE = "OANDA" + # Finnhub resolution strings (H4 uses 60-min candles, then resampled) FH_RESOLUTIONS = { Timeframe.M1: "1", @@ -80,14 +84,41 @@ class FinnhubDataProvider: "No Finnhub API key — set the FINNHUB_API_KEY environment variable." ) self._client = finnhub.Client(api_key=api_key) + self._exchange = ( + os.environ.get("FINNHUB_FOREX_EXCHANGE") or self.DEFAULT_EXCHANGE + ) + logger.info(f"Finnhub forex exchange: {self._exchange}") self._cache: Dict[str, pd.DataFrame] = {} self._cache_timestamps: Dict[str, float] = {} - @staticmethod - def _to_finnhub_symbol(symbol: str) -> str: - """Convert EURUSD=X → OANDA:EUR_USD.""" + def _to_finnhub_symbol(self, symbol: str) -> str: + """Convert EURUSD=X → {exchange}:EUR_USD.""" clean = symbol.replace("=X", "") - return f"OANDA:{clean[:3]}_{clean[3:]}" + return f"{self._exchange}:{clean[:3]}_{clean[3:]}" + + def discover_exchanges(self) -> List[str]: + """ + Return the list of forex exchanges available on your Finnhub plan. + Call this once to find which exchange prefix to use. + + Usage: + set FINNHUB_API_KEY=your_key + python -c " + from config import PlatformConfig + from data_provider_finnhub import FinnhubDataProvider + p = FinnhubDataProvider(PlatformConfig()) + print(p.discover_exchanges()) + " + """ + self._throttle() + try: + exchanges = self._client.forex_exchanges() + logger.info(f"Available Finnhub forex exchanges: {exchanges}") + return exchanges + except finnhub.FinnhubAPIException as e: + raise DataUnavailableError( + f"Could not list Finnhub forex exchanges: {e}" + ) from e # ── Public interface (matches ForexDataProvider) ─────────────────── @@ -256,6 +287,12 @@ class FinnhubDataProvider: "Finnhub API rate limit reached — too many requests. " "Free tier allows 60 calls/minute. Try again shortly." ) from e + if e.status_code == 403: + raise DataUnavailableError( + f"Finnhub 403: exchange '{self._exchange}' is not accessible on your plan. " + "Run discover_exchanges() to see what your API key can access, " + "then set FINNHUB_FOREX_EXCHANGE= and restart." + ) from e raise DataUnavailableError( f"Finnhub API error for {fh_symbol} ({e.status_code}): {e}" ) from e