"""Alpaca Markets OHLCV provider using the alpaca-py SDK.""" from __future__ import annotations import asyncio import logging from datetime import date from alpaca.data.historical import StockHistoricalDataClient from alpaca.data.requests import StockBarsRequest from alpaca.data.timeframe import TimeFrame from app.exceptions import ProviderError, RateLimitError from app.providers.protocol import OHLCVData logger = logging.getLogger(__name__) class AlpacaOHLCVProvider: """Fetches daily OHLCV bars from Alpaca Markets Data API.""" def __init__(self, api_key: str, api_secret: str) -> None: if not api_key or not api_secret: raise ProviderError("Alpaca API key and secret are required") self._client = StockHistoricalDataClient(api_key, api_secret) 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*.""" try: request = StockBarsRequest( symbol_or_symbols=ticker, timeframe=TimeFrame.Day, start=start_date, end=end_date, ) # alpaca-py's client is synchronous — run in a thread 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, []) for bar in bar_set: results.append( OHLCVData( ticker=ticker, date=bar.timestamp.date(), open=float(bar.open), high=float(bar.high), low=float(bar.low), close=float(bar.close), volume=int(bar.volume), ) ) return results except Exception as exc: msg = str(exc).lower() if "rate" in msg and "limit" in msg: raise RateLimitError(f"Alpaca rate limit hit for {ticker}") from exc logger.error("Alpaca provider error for %s: %s", ticker, exc) raise ProviderError(f"Alpaca provider error for {ticker}: {exc}") from exc