fix: Alpaca intraday 503 — auto-select feed=iex for free plan

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 <noreply@anthropic.com>
main
I Luk Kim 4 months ago
parent 5ef4d1790d
commit 38207d342c

@ -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: def normalize_ticker(symbol: str) -> str:
"""Normalize ticker symbol for Alpaca API. """Normalize ticker symbol for Alpaca API.
@ -152,6 +165,7 @@ class AlpacaClient:
start: Optional[str] = None, start: Optional[str] = None,
end: Optional[str] = None, end: Optional[str] = None,
limit: int = 10000, limit: int = 10000,
feed: Optional[str] = None,
) -> List[Dict]: ) -> List[Dict]:
""" """
Fetch bars for a single symbol with automatic pagination. 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") start: RFC-3339 date/datetime (e.g. "2024-01-01")
end: RFC-3339 date/datetime end: RFC-3339 date/datetime
limit: Max bars per page (Alpaca max 10000) 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: Returns:
List of bar dicts with keys: t, o, h, l, c, v, n, vw List of bar dicts with keys: t, o, h, l, c, v, n, vw
@ -172,6 +188,10 @@ class AlpacaClient:
params["start"] = start params["start"] = start
if end: if end:
params["end"] = 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] = [] all_bars: List[Dict] = []
path = f"/v2/stocks/{normalize_ticker(symbol).upper()}/bars" path = f"/v2/stocks/{normalize_ticker(symbol).upper()}/bars"
@ -196,6 +216,7 @@ class AlpacaClient:
end: Optional[str] = None, end: Optional[str] = None,
limit: int = 10000, limit: int = 10000,
batch_size: int = 200, batch_size: int = 200,
feed: Optional[str] = None,
) -> Dict[str, List[Dict]]: ) -> Dict[str, List[Dict]]:
""" """
Fetch bars for multiple symbols with auto-pagination and transparent batching. 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] normalized = [normalize_ticker(s).upper() for s in symbols]
result: Dict[str, List[Dict]] = {s: [] for s in normalized} 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 # Process in batches to stay within URL length limits
for batch_start in range(0, len(normalized), batch_size): for batch_start in range(0, len(normalized), batch_size):
batch = normalized[batch_start: batch_start + batch_size] batch = normalized[batch_start: batch_start + batch_size]
@ -234,6 +257,8 @@ class AlpacaClient:
params["start"] = start params["start"] = start
if end: if end:
params["end"] = end params["end"] = end
if effective_feed:
params["feed"] = effective_feed
while True: while True:
data = await self._request("GET", path, params=params) data = await self._request("GET", path, params=params)

Loading…
Cancel
Save