|
|
|
@ -16,17 +16,31 @@ logger = logging.getLogger(__name__)
|
|
|
|
# Interval mapping: internal format → Alpaca API format
|
|
|
|
# Interval mapping: internal format → Alpaca API format
|
|
|
|
INTERVAL_MAP = {
|
|
|
|
INTERVAL_MAP = {
|
|
|
|
"1m": "1Min",
|
|
|
|
"1m": "1Min",
|
|
|
|
|
|
|
|
"1min": "1Min",
|
|
|
|
"2m": "2Min",
|
|
|
|
"2m": "2Min",
|
|
|
|
"5m": "5Min",
|
|
|
|
"5m": "5Min",
|
|
|
|
|
|
|
|
"5min": "5Min",
|
|
|
|
"15m": "15Min",
|
|
|
|
"15m": "15Min",
|
|
|
|
|
|
|
|
"15min": "15Min",
|
|
|
|
"30m": "30Min",
|
|
|
|
"30m": "30Min",
|
|
|
|
|
|
|
|
"30min": "30Min",
|
|
|
|
"1h": "1Hour",
|
|
|
|
"1h": "1Hour",
|
|
|
|
|
|
|
|
"60min": "1Hour",
|
|
|
|
"1d": "1Day",
|
|
|
|
"1d": "1Day",
|
|
|
|
"1w": "1Week",
|
|
|
|
"1w": "1Week",
|
|
|
|
"1mo": "1Month",
|
|
|
|
"1mo": "1Month",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_ticker(symbol: str) -> str:
|
|
|
|
|
|
|
|
"""Normalize ticker symbol for Alpaca API.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
US stock tickers use dots for share classes (BF.B, BRK.B) while
|
|
|
|
|
|
|
|
Yahoo Finance and other sources use hyphens (BF-B, BRK-B).
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
return symbol.replace("-", ".")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AlpacaClient:
|
|
|
|
class AlpacaClient:
|
|
|
|
"""Alpaca Market Data API client (v2)"""
|
|
|
|
"""Alpaca Market Data API client (v2)"""
|
|
|
|
|
|
|
|
|
|
|
|
@ -160,7 +174,7 @@ class AlpacaClient:
|
|
|
|
params["end"] = end
|
|
|
|
params["end"] = end
|
|
|
|
|
|
|
|
|
|
|
|
all_bars: List[Dict] = []
|
|
|
|
all_bars: List[Dict] = []
|
|
|
|
path = f"/v2/stocks/{symbol.upper()}/bars"
|
|
|
|
path = f"/v2/stocks/{normalize_ticker(symbol).upper()}/bars"
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
while True:
|
|
|
|
data = await self._request("GET", path, params=params)
|
|
|
|
data = await self._request("GET", path, params=params)
|
|
|
|
@ -181,16 +195,38 @@ class AlpacaClient:
|
|
|
|
start: Optional[str] = None,
|
|
|
|
start: Optional[str] = None,
|
|
|
|
end: Optional[str] = None,
|
|
|
|
end: Optional[str] = None,
|
|
|
|
limit: int = 10000,
|
|
|
|
limit: int = 10000,
|
|
|
|
|
|
|
|
batch_size: int = 200,
|
|
|
|
) -> Dict[str, List[Dict]]:
|
|
|
|
) -> Dict[str, List[Dict]]:
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
Fetch bars for multiple symbols in one request with auto-pagination.
|
|
|
|
Fetch bars for multiple symbols with auto-pagination and transparent batching.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Normalizes tickers (BF-B → BF.B) before calling Alpaca and returns results
|
|
|
|
|
|
|
|
keyed by the *Alpaca* symbol (normalized). Callers that need the original
|
|
|
|
|
|
|
|
symbol names should build their own reverse-map before calling.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
|
|
symbols: List of ticker symbols (hyphens are auto-normalized to dots)
|
|
|
|
|
|
|
|
timeframe: Internal interval string (e.g. "1d", "1h", "5min")
|
|
|
|
|
|
|
|
start: RFC-3339 date/datetime string
|
|
|
|
|
|
|
|
end: RFC-3339 date/datetime string
|
|
|
|
|
|
|
|
limit: Max bars per page (Alpaca max 10000)
|
|
|
|
|
|
|
|
batch_size: Max symbols per Alpaca request (default 200, ~1KB URL)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
Returns:
|
|
|
|
Dict mapping symbol → list of bar dicts
|
|
|
|
Dict mapping normalized Alpaca symbol → list of bar dicts
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
alpaca_tf = INTERVAL_MAP.get(timeframe, timeframe)
|
|
|
|
alpaca_tf = INTERVAL_MAP.get(timeframe, timeframe)
|
|
|
|
|
|
|
|
path = "/v2/stocks/bars"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Normalize all symbols for Alpaca (BF-B → BF.B)
|
|
|
|
|
|
|
|
normalized = [normalize_ticker(s).upper() for s in symbols]
|
|
|
|
|
|
|
|
result: Dict[str, List[Dict]] = {s: [] for s in normalized}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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]
|
|
|
|
params: Dict = {
|
|
|
|
params: Dict = {
|
|
|
|
"symbols": ",".join(s.upper() for s in symbols),
|
|
|
|
"symbols": ",".join(batch),
|
|
|
|
"timeframe": alpaca_tf,
|
|
|
|
"timeframe": alpaca_tf,
|
|
|
|
"limit": min(limit, 10000),
|
|
|
|
"limit": min(limit, 10000),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -199,9 +235,6 @@ class AlpacaClient:
|
|
|
|
if end:
|
|
|
|
if end:
|
|
|
|
params["end"] = end
|
|
|
|
params["end"] = end
|
|
|
|
|
|
|
|
|
|
|
|
result: Dict[str, List[Dict]] = {s.upper(): [] for s in symbols}
|
|
|
|
|
|
|
|
path = "/v2/stocks/bars"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while True:
|
|
|
|
while True:
|
|
|
|
data = await self._request("GET", path, params=params)
|
|
|
|
data = await self._request("GET", path, params=params)
|
|
|
|
bars_map = data.get("bars") or {}
|
|
|
|
bars_map = data.get("bars") or {}
|
|
|
|
@ -223,7 +256,7 @@ class AlpacaClient:
|
|
|
|
Returns Alpaca's snapshot object with keys:
|
|
|
|
Returns Alpaca's snapshot object with keys:
|
|
|
|
latestTrade, latestQuote, minuteBar, dailyBar, prevDailyBar
|
|
|
|
latestTrade, latestQuote, minuteBar, dailyBar, prevDailyBar
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
data = await self._request("GET", f"/v2/stocks/{symbol.upper()}/snapshot")
|
|
|
|
data = await self._request("GET", f"/v2/stocks/{normalize_ticker(symbol).upper()}/snapshot")
|
|
|
|
return data
|
|
|
|
return data
|
|
|
|
|
|
|
|
|
|
|
|
async def get_snapshots(self, symbols: List[str]) -> Dict[str, Dict]:
|
|
|
|
async def get_snapshots(self, symbols: List[str]) -> Dict[str, Dict]:
|
|
|
|
@ -232,7 +265,7 @@ class AlpacaClient:
|
|
|
|
|
|
|
|
|
|
|
|
Returns dict mapping symbol → snapshot object.
|
|
|
|
Returns dict mapping symbol → snapshot object.
|
|
|
|
"""
|
|
|
|
"""
|
|
|
|
params = {"symbols": ",".join(s.upper() for s in symbols)}
|
|
|
|
params = {"symbols": ",".join(normalize_ticker(s).upper() for s in symbols)}
|
|
|
|
data = await self._request("GET", "/v2/stocks/snapshots", params=params)
|
|
|
|
data = await self._request("GET", "/v2/stocks/snapshots", params=params)
|
|
|
|
return data # {SYMBOL: {...snapshot...}, ...}
|
|
|
|
return data # {SYMBOL: {...snapshot...}, ...}
|
|
|
|
|
|
|
|
|
|
|
|
|