diff --git a/app/services/alpaca_client.py b/app/services/alpaca_client.py index 14ee2e9..c2e3224 100644 --- a/app/services/alpaca_client.py +++ b/app/services/alpaca_client.py @@ -32,6 +32,19 @@ INTERVAL_MAP = { } +_DAILY_TIMEFRAMES = {"1d", "1Day", "1w", "1Week", "1mo", "1Month"} + + +def _default_feed(timeframe: str) -> Optional[str]: + """Return 'iex' for intraday timeframes (free plan), None for daily+. + + Daily/weekly/monthly SIP data is accessible on the free plan. + Intraday SIP data requires a paid subscription — use IEX instead. + """ + alpaca_tf = INTERVAL_MAP.get(timeframe, timeframe) + return None if alpaca_tf in _DAILY_TIMEFRAMES else "iex" + + def normalize_ticker(symbol: str) -> str: """Normalize ticker symbol for Alpaca API. @@ -152,6 +165,7 @@ class AlpacaClient: start: Optional[str] = None, end: Optional[str] = None, limit: int = 10000, + feed: Optional[str] = None, ) -> List[Dict]: """ Fetch bars for a single symbol with automatic pagination. @@ -162,6 +176,8 @@ class AlpacaClient: start: RFC-3339 date/datetime (e.g. "2024-01-01") end: RFC-3339 date/datetime limit: Max bars per page (Alpaca max 10000) + feed: Data feed ("iex" = free real-time, "sip" = paid consolidated). + Defaults to "iex" for intraday, no feed param for daily+. Returns: List of bar dicts with keys: t, o, h, l, c, v, n, vw @@ -172,6 +188,10 @@ class AlpacaClient: params["start"] = start if end: params["end"] = end + # Default to iex feed for intraday intervals (free plan compatible) + effective_feed = feed or (_default_feed(timeframe)) + if effective_feed: + params["feed"] = effective_feed all_bars: List[Dict] = [] path = f"/v2/stocks/{normalize_ticker(symbol).upper()}/bars" @@ -196,6 +216,7 @@ class AlpacaClient: end: Optional[str] = None, limit: int = 10000, batch_size: int = 200, + feed: Optional[str] = None, ) -> Dict[str, List[Dict]]: """ Fetch bars for multiple symbols with auto-pagination and transparent batching. @@ -222,6 +243,8 @@ class AlpacaClient: normalized = [normalize_ticker(s).upper() for s in symbols] result: Dict[str, List[Dict]] = {s: [] for s in normalized} + effective_feed = feed or _default_feed(timeframe) + # Process in batches to stay within URL length limits for batch_start in range(0, len(normalized), batch_size): batch = normalized[batch_start: batch_start + batch_size] @@ -234,6 +257,8 @@ class AlpacaClient: params["start"] = start if end: params["end"] = end + if effective_feed: + params["feed"] = effective_feed while True: data = await self._request("GET", path, params=params)