77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
"""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 alpaca.data.enums import Adjustment
|
|
|
|
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)
|
|
|
|
@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=alpaca_symbol,
|
|
timeframe=TimeFrame.Day,
|
|
start=start_date,
|
|
end=end_date,
|
|
adjustment=Adjustment.SPLIT,
|
|
)
|
|
|
|
# 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(alpaca_symbol, []) if hasattr(bars, "get") else getattr(bars, "data", {}).get(alpaca_symbol, [])
|
|
for bar in bar_set:
|
|
results.append(
|
|
OHLCVData(
|
|
ticker=ticker, # use original internal symbol
|
|
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
|