Handle Finnhub 403; make exchange prefix configurable

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 <noreply@anthropic.com>
This commit is contained in:
Harwinder
2026-05-04 20:26:10 -04:00
parent 43cc4bdd42
commit b124d0a7a3
+41 -4
View File
@@ -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=<exchange> and restart."
) from e
raise DataUnavailableError(
f"Finnhub API error for {fh_symbol} ({e.status_code}): {e}"
) from e