Route ORB bar data through Oracle API instead of calling Alpaca SDK directly
- libs/oracle_client/alpaca.py: Added get_multi_daily_bars() and get_multi_intraday_bars() helpers that call Oracle's /api/v1/price/data and /api/v1/alpaca/intraday endpoints respectively. Oracle handles symbol normalization (e.g. BF-B → BF.B) internally, so symbols like BF-B no longer crash the screening chunk. - apps/paper_trader/alpaca_broker.py: get_bars() and get_intraday_bars() now use the new Oracle client helpers instead of the Alpaca SDK StockBarsRequest, eliminating direct Alpaca bar API calls from broker. - apps/orb_trader/engine.py: Removed per-symbol BF-B workaround (now unnecessary since Oracle normalizes the symbol server-side); kept outer try/except for chunk-level resilience. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>main
parent
aaa960c556
commit
658a741017
@ -0,0 +1,197 @@
|
|||||||
|
"""Alpaca real-time snapshot service via Stock Oracle API.
|
||||||
|
|
||||||
|
Provides synchronous helpers for real-time price snapshots, suitable for use
|
||||||
|
in threaded engine code (which already runs in asyncio.to_thread).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AlpacaSnapshot:
|
||||||
|
"""Real-time Alpaca snapshot for a single ticker."""
|
||||||
|
|
||||||
|
ticker: str
|
||||||
|
price: float | None = None # last trade price
|
||||||
|
bid: float | None = None
|
||||||
|
ask: float | None = None
|
||||||
|
bid_size: int | None = None
|
||||||
|
ask_size: int | None = None
|
||||||
|
open: float | None = None
|
||||||
|
high: float | None = None
|
||||||
|
low: float | None = None
|
||||||
|
volume: int | None = None
|
||||||
|
vwap: float | None = None
|
||||||
|
prev_close: float | None = None
|
||||||
|
change: float | None = None
|
||||||
|
change_pct: float | None = None
|
||||||
|
timestamp: str | None = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mid(self) -> float | None:
|
||||||
|
"""Midpoint between bid and ask, or last trade if unavailable."""
|
||||||
|
if self.bid is not None and self.ask is not None:
|
||||||
|
return (self.bid + self.ask) / 2
|
||||||
|
return self.price
|
||||||
|
|
||||||
|
|
||||||
|
def _base_url() -> str:
|
||||||
|
return os.environ.get("ORACLE_URL", "http://localhost:18001").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_snapshot(data: dict[str, Any]) -> AlpacaSnapshot:
|
||||||
|
return AlpacaSnapshot(
|
||||||
|
ticker=data["ticker"],
|
||||||
|
price=data.get("price"),
|
||||||
|
bid=data.get("bid"),
|
||||||
|
ask=data.get("ask"),
|
||||||
|
bid_size=data.get("bid_size"),
|
||||||
|
ask_size=data.get("ask_size"),
|
||||||
|
open=data.get("open"),
|
||||||
|
high=data.get("high"),
|
||||||
|
low=data.get("low"),
|
||||||
|
volume=data.get("volume"),
|
||||||
|
vwap=data.get("vwap"),
|
||||||
|
prev_close=data.get("prev_close"),
|
||||||
|
change=data.get("change"),
|
||||||
|
change_pct=data.get("change_pct"),
|
||||||
|
timestamp=data.get("timestamp"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_multi_daily_bars(
|
||||||
|
tickers: list[str],
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
base_url: str | None = None,
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
"""Fetch daily OHLCV bars for multiple tickers via Oracle API.
|
||||||
|
|
||||||
|
Oracle handles symbol normalisation (e.g. BF-B → BF.B) and Alpaca batching
|
||||||
|
internally. Returns {ticker: [{date, open, high, low, close, volume}, ...]}.
|
||||||
|
Missing/errored tickers are omitted.
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
if not tickers:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result: dict[str, list[dict]] = {}
|
||||||
|
url = (base_url or _base_url()) + "/api/v1/price/data"
|
||||||
|
|
||||||
|
# Oracle handles its own batching, but we chunk here as a safety net for
|
||||||
|
# very large URL query strings.
|
||||||
|
chunk_size = 300
|
||||||
|
for i in range(0, len(tickers), chunk_size):
|
||||||
|
chunk = tickers[i : i + chunk_size]
|
||||||
|
try:
|
||||||
|
resp = httpx.get(
|
||||||
|
url,
|
||||||
|
params={"tickers": ",".join(chunk), "start_date": start_date, "end_date": end_date},
|
||||||
|
timeout=90.0,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
for ticker, bars in resp.json().get("bars", {}).items():
|
||||||
|
result[ticker] = bars
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Oracle multi_daily_bars chunk %d failed: %s", i // chunk_size, exc)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_multi_intraday_bars(
|
||||||
|
tickers: list[str],
|
||||||
|
start_date: str,
|
||||||
|
end_date: str,
|
||||||
|
interval: str = "5min",
|
||||||
|
base_url: str | None = None,
|
||||||
|
) -> dict[str, list[dict]]:
|
||||||
|
"""Fetch intraday bars for multiple tickers via Oracle API.
|
||||||
|
|
||||||
|
Returns {ticker: [{timestamp (ISO8601), open, high, low, close, volume}, ...]}.
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
if not tickers:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result: dict[str, list[dict]] = {}
|
||||||
|
url = (base_url or _base_url()) + "/api/v1/alpaca/intraday"
|
||||||
|
|
||||||
|
chunk_size = 300
|
||||||
|
for i in range(0, len(tickers), chunk_size):
|
||||||
|
chunk = tickers[i : i + chunk_size]
|
||||||
|
try:
|
||||||
|
resp = httpx.get(
|
||||||
|
url,
|
||||||
|
params={
|
||||||
|
"tickers": ",".join(chunk),
|
||||||
|
"interval": interval,
|
||||||
|
"start_date": start_date,
|
||||||
|
"end_date": end_date,
|
||||||
|
},
|
||||||
|
timeout=90.0,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
for ticker, bars in resp.json().get("bars", {}).items():
|
||||||
|
result[ticker] = bars
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Oracle multi_intraday_bars chunk %d failed: %s", i // chunk_size, exc)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def get_snapshot(ticker: str, base_url: str | None = None) -> AlpacaSnapshot | None:
|
||||||
|
"""Fetch real-time snapshot for a single ticker (synchronous).
|
||||||
|
|
||||||
|
Returns None on any error.
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
url = (base_url or _base_url()) + f"/api/v1/alpaca/snapshot/{ticker}"
|
||||||
|
try:
|
||||||
|
resp = httpx.get(url, timeout=10.0)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return _parse_snapshot(resp.json())
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Oracle snapshot(%s) failed: %s", ticker, exc)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def get_snapshots(
|
||||||
|
tickers: list[str],
|
||||||
|
base_url: str | None = None,
|
||||||
|
batch_size: int = 200,
|
||||||
|
) -> dict[str, AlpacaSnapshot]:
|
||||||
|
"""Fetch real-time snapshots for multiple tickers (synchronous).
|
||||||
|
|
||||||
|
Batches requests and returns {ticker: AlpacaSnapshot}.
|
||||||
|
Missing/errored tickers are omitted.
|
||||||
|
"""
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
if not tickers:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
result: dict[str, AlpacaSnapshot] = {}
|
||||||
|
url = (base_url or _base_url()) + "/api/v1/alpaca/snapshot"
|
||||||
|
|
||||||
|
for i in range(0, len(tickers), batch_size):
|
||||||
|
chunk = tickers[i : i + batch_size]
|
||||||
|
try:
|
||||||
|
resp = httpx.get(url, params={"tickers": ",".join(chunk)}, timeout=15.0)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
for snap_data in data.get("snapshots", []):
|
||||||
|
snap = _parse_snapshot(snap_data)
|
||||||
|
result[snap.ticker] = snap
|
||||||
|
except Exception as exc:
|
||||||
|
log.warning("Oracle snapshots(batch %d) failed: %s", i // batch_size, exc)
|
||||||
|
|
||||||
|
return result
|
||||||
Loading…
Reference in New Issue