From 38207d342c27c32de092d35f546d8fb6b7c0b9bd Mon Sep 17 00:00:00 2001 From: I Luk Kim Date: Mon, 13 Apr 2026 11:31:32 -0700 Subject: [PATCH] =?UTF-8?q?fix:=20Alpaca=20intraday=20503=20=E2=80=94=20au?= =?UTF-8?q?to-select=20feed=3Diex=20for=20free=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Free plan blocks SIP intraday data (403). IEX feed is free and real-time. - _default_feed(): returns 'iex' for intraday timeframes, None for daily+ - get_bars() / get_multi_bars(): apply default feed automatically - Callers can override with feed='sip' if on paid plan Co-Authored-By: Claude Sonnet 4.6 --- app/services/alpaca_client.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) 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)