From 79ca19f45f1eeadf2f6610238221b64b1dc71948 Mon Sep 17 00:00:00 2001 From: Dennis Thiessen Date: Sat, 7 Mar 2026 17:27:57 +0100 Subject: [PATCH] add support for alternative ticker universe formatting --- app/providers/alpaca.py | 17 ++++++++++++++--- app/providers/fundamentals_chain.py | 22 ++++++++++++++++------ 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/app/providers/alpaca.py b/app/providers/alpaca.py index c7f04df..86fed45 100644 --- a/app/providers/alpaca.py +++ b/app/providers/alpaca.py @@ -25,13 +25,24 @@ class AlpacaOHLCVProvider: raise ProviderError("Alpaca API key and secret are required") self._client = StockHistoricalDataClient(api_key, api_secret) + @staticmethod + def _to_alpaca_symbol(symbol: str) -> str: + """Convert internal symbol format (BRK-B) to Alpaca format (BRK.B).""" + return symbol.replace("-", ".") + + @staticmethod + def _from_alpaca_symbol(symbol: str) -> str: + """Convert Alpaca symbol format (BRK.B) back to internal format (BRK-B).""" + return symbol.replace(".", "-") + async def fetch_ohlcv( self, ticker: str, start_date: date, end_date: date ) -> list[OHLCVData]: """Fetch daily OHLCV bars for *ticker* between *start_date* and *end_date*.""" + alpaca_symbol = self._to_alpaca_symbol(ticker) try: request = StockBarsRequest( - symbol_or_symbols=ticker, + symbol_or_symbols=alpaca_symbol, timeframe=TimeFrame.Day, start=start_date, end=end_date, @@ -42,11 +53,11 @@ class AlpacaOHLCVProvider: bars = await asyncio.to_thread(self._client.get_stock_bars, request) results: list[OHLCVData] = [] - bar_set = bars.get(ticker, []) if hasattr(bars, "get") else getattr(bars, "data", {}).get(ticker, []) + bar_set = bars.get(alpaca_symbol, []) if hasattr(bars, "get") else getattr(bars, "data", {}).get(alpaca_symbol, []) for bar in bar_set: results.append( OHLCVData( - ticker=ticker, + ticker=ticker, # use original internal symbol date=bar.timestamp.date(), open=float(bar.open), high=float(bar.high), diff --git a/app/providers/fundamentals_chain.py b/app/providers/fundamentals_chain.py index 8be9382..bb512be 100644 --- a/app/providers/fundamentals_chain.py +++ b/app/providers/fundamentals_chain.py @@ -38,6 +38,14 @@ def _safe_float(value: object) -> float | None: return None +def _to_api_symbol(symbol: str) -> str: + """Convert internal symbol format (BRK-B) to API format (BRK.B). + + Finnhub and Alpha Vantage use dot-separated share class notation. + """ + return symbol.replace("-", ".") + + class FinnhubFundamentalProvider: """Fundamentals provider backed by Finnhub free endpoints.""" @@ -49,19 +57,20 @@ class FinnhubFundamentalProvider: async def fetch_fundamentals(self, ticker: str) -> FundamentalData: unavailable: dict[str, str] = {} + api_symbol = _to_api_symbol(ticker) async with httpx.AsyncClient(timeout=30.0, verify=_CA_BUNDLE_PATH) as client: profile_resp = await client.get( f"{self._base_url}/stock/profile2", - params={"symbol": ticker, "token": self._api_key}, + params={"symbol": api_symbol, "token": self._api_key}, ) metric_resp = await client.get( f"{self._base_url}/stock/metric", - params={"symbol": ticker, "metric": "all", "token": self._api_key}, + params={"symbol": api_symbol, "metric": "all", "token": self._api_key}, ) earnings_resp = await client.get( f"{self._base_url}/stock/earnings", - params={"symbol": ticker, "limit": 1, "token": self._api_key}, + params={"symbol": api_symbol, "limit": 1, "token": self._api_key}, ) for resp, endpoint in ( @@ -121,19 +130,20 @@ class AlphaVantageFundamentalProvider: async def fetch_fundamentals(self, ticker: str) -> FundamentalData: unavailable: dict[str, str] = {} + api_symbol = _to_api_symbol(ticker) async with httpx.AsyncClient(timeout=30.0, verify=_CA_BUNDLE_PATH) as client: overview_resp = await client.get( self._base_url, - params={"function": "OVERVIEW", "symbol": ticker, "apikey": self._api_key}, + params={"function": "OVERVIEW", "symbol": api_symbol, "apikey": self._api_key}, ) earnings_resp = await client.get( self._base_url, - params={"function": "EARNINGS", "symbol": ticker, "apikey": self._api_key}, + params={"function": "EARNINGS", "symbol": api_symbol, "apikey": self._api_key}, ) income_resp = await client.get( self._base_url, - params={"function": "INCOME_STATEMENT", "symbol": ticker, "apikey": self._api_key}, + params={"function": "INCOME_STATEMENT", "symbol": api_symbol, "apikey": self._api_key}, ) for resp, endpoint in (