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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user