Spaces:
Runtime error
Runtime error
| """Alpaca adapter β real Alpaca API for stocks.""" | |
| from __future__ import annotations | |
| import logging | |
| from datetime import datetime, timedelta, timezone | |
| import pandas as pd | |
| from trading_cli.execution.adapters.base import ( | |
| AccountInfo, | |
| MarketClock, | |
| OrderResult, | |
| Position, | |
| TradingAdapter, | |
| ) | |
| from trading_cli.execution.adapters.registry import register_adapter | |
| logger = logging.getLogger(__name__) | |
| class AlpacaAdapter(TradingAdapter): | |
| """Alpaca Markets adapter for US equities (paper & live trading).""" | |
| def __init__(self, config: dict) -> None: | |
| self._config = config | |
| self._api_key = config.get("alpaca_api_key", "") | |
| self._api_secret = config.get("alpaca_api_secret", "") | |
| self._paper = config.get("alpaca_paper", True) | |
| self._demo = not (self._api_key and self._api_secret) | |
| if self._demo: | |
| logger.info("AlpacaAdapter: no API keys found, running in demo mode") | |
| return | |
| try: | |
| from alpaca.trading.client import TradingClient | |
| from alpaca.data.historical import StockHistoricalDataClient | |
| from alpaca.data.historical.news import NewsClient | |
| self._trading_client = TradingClient( | |
| api_key=self._api_key, | |
| secret_key=self._api_secret, | |
| paper=self._paper, | |
| ) | |
| self._historical_client = StockHistoricalDataClient( | |
| api_key=self._api_key, | |
| secret_key=self._api_secret, | |
| ) | |
| self._news_client = NewsClient( | |
| api_key=self._api_key, | |
| secret_key=self._api_secret, | |
| ) | |
| logger.info("AlpacaAdapter connected (paper=%s)", self._paper) | |
| except ImportError as exc: | |
| raise RuntimeError("alpaca-py not installed. Run: uv add alpaca-py") from exc | |
| except Exception as exc: | |
| logger.error("Failed to connect to Alpaca: %s", exc) | |
| self._demo = True | |
| def adapter_id(self) -> str: | |
| return "alpaca" | |
| def supports_paper_trading(self) -> bool: | |
| return True | |
| def is_demo_mode(self) -> bool: | |
| return self._demo | |
| # ββ Account & Positions βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_account(self) -> AccountInfo: | |
| if self._demo: | |
| return AccountInfo( | |
| equity=100000.0, | |
| cash=100000.0, | |
| buying_power=400000.0, | |
| portfolio_value=100000.0, | |
| ) | |
| acct = self._trading_client.get_account() | |
| return AccountInfo( | |
| equity=float(acct.equity), | |
| cash=float(acct.cash), | |
| buying_power=float(acct.buying_power), | |
| portfolio_value=float(acct.portfolio_value), | |
| ) | |
| def get_positions(self) -> list[Position]: | |
| if self._demo: | |
| return [] | |
| raw = self._trading_client.get_all_positions() | |
| out = [] | |
| for p in raw: | |
| out.append( | |
| Position( | |
| symbol=p.symbol, | |
| qty=float(p.qty), | |
| avg_entry_price=float(p.avg_entry_price), | |
| current_price=float(p.current_price), | |
| unrealized_pl=float(p.unrealized_pl), | |
| unrealized_plpc=float(p.unrealized_plpc), | |
| market_value=float(p.market_value), | |
| side=str(p.side), | |
| ) | |
| ) | |
| return out | |
| # ββ Orders βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def submit_market_order(self, symbol: str, qty: int, side: str) -> OrderResult: | |
| if self._demo: | |
| return OrderResult( | |
| order_id=f"DEMO-{datetime.now().timestamp()}", | |
| symbol=symbol, | |
| action=side, | |
| qty=qty, | |
| status="filled", | |
| filled_price=100.0, # Mock price | |
| ) | |
| from alpaca.trading.requests import MarketOrderRequest | |
| from alpaca.trading.enums import OrderSide, TimeInForce | |
| order_side = OrderSide.BUY if side.upper() == "BUY" else OrderSide.SELL | |
| req = MarketOrderRequest( | |
| symbol=symbol, | |
| qty=qty, | |
| side=order_side, | |
| time_in_force=TimeInForce.DAY, | |
| ) | |
| try: | |
| order = self._trading_client.submit_order(order_data=req) | |
| filled_price = float(order.filled_avg_price) if order.filled_avg_price else None | |
| return OrderResult( | |
| order_id=str(order.id), | |
| symbol=symbol, | |
| action=side, | |
| qty=qty, | |
| status=str(order.status), | |
| filled_price=filled_price, | |
| ) | |
| except Exception as exc: | |
| logger.error("Order submission failed for %s %s %d: %s", side, symbol, qty, exc) | |
| raise | |
| def close_position(self, symbol: str) -> OrderResult | None: | |
| if self._demo: | |
| return None | |
| try: | |
| response = self._trading_client.close_position(symbol) | |
| return OrderResult( | |
| order_id=str(response.id), | |
| symbol=symbol, | |
| action="SELL", | |
| qty=int(float(response.qty or 0)), | |
| status=str(response.status), | |
| ) | |
| except Exception as exc: | |
| logger.error("Close position failed for %s: %s", symbol, exc) | |
| return None | |
| # ββ Market Data βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fetch_ohlcv(self, symbol: str, days: int = 90) -> pd.DataFrame: | |
| if self._demo: | |
| # Fallback to yfinance in demo mode | |
| from trading_cli.data.market import fetch_ohlcv_yfinance | |
| return fetch_ohlcv_yfinance(symbol, days) | |
| try: | |
| from alpaca.data.requests import StockBarsRequest | |
| from alpaca.data.timeframe import TimeFrame | |
| end = datetime.now(tz=timezone.utc) | |
| start = end - timedelta(days=days + 10) # extra buffer for weekends | |
| request = StockBarsRequest( | |
| symbol_or_symbols=symbol, | |
| timeframe=TimeFrame.Day, | |
| start=start, | |
| end=end, | |
| feed="iex", | |
| ) | |
| bars = self._historical_client.get_stock_bars(request) | |
| df = bars.df | |
| if isinstance(df.index, pd.MultiIndex): | |
| df = df.xs(symbol, level=0) if symbol in df.index.get_level_values(0) else df | |
| df.index = pd.to_datetime(df.index, utc=True) | |
| df = df.rename(columns={"open": "Open", "high": "High", "low": "Low", | |
| "close": "Close", "volume": "Volume"}) | |
| return df.tail(days) | |
| except Exception as exc: | |
| logger.warning("Alpaca OHLCV fetch failed for %s: %s β falling back to yfinance", symbol, exc) | |
| from trading_cli.data.market import fetch_ohlcv_yfinance | |
| return fetch_ohlcv_yfinance(symbol, days) | |
| def get_latest_quote(self, symbol: str) -> float | None: | |
| if self._demo: | |
| return None | |
| try: | |
| from alpaca.data.requests import StockLatestTradeRequest | |
| req = StockLatestTradeRequest(symbol_or_symbols=symbol, feed="iex") | |
| trades = self._historical_client.get_stock_latest_trade(req) | |
| return float(trades[symbol].price) | |
| except Exception as exc: | |
| logger.warning("Alpaca latest quote failed for %s: %s", symbol, exc) | |
| return None | |
| def get_latest_quotes_batch(self, symbols: list[str]) -> dict[str, float]: | |
| if self._demo: | |
| return {} | |
| try: | |
| from alpaca.data.requests import StockLatestTradeRequest | |
| req = StockLatestTradeRequest(symbol_or_symbols=symbols, feed="iex") | |
| trades = self._historical_client.get_stock_latest_trade(req) | |
| return {sym: float(trade.price) for sym, trade in trades.items()} | |
| except Exception as exc: | |
| logger.warning("Batch Alpaca quote failed: %s", exc) | |
| return {} | |
| # ββ Market Info βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_market_clock(self) -> MarketClock: | |
| if self._demo: | |
| now = datetime.now(tz=timezone.utc) | |
| hour_et = (now.hour - 5) % 24 | |
| is_open = now.weekday() < 5 and 9 <= hour_et < 16 | |
| return MarketClock( | |
| is_open=is_open, | |
| next_open="09:30 ET", | |
| next_close="16:00 ET", | |
| ) | |
| try: | |
| clock = self._trading_client.get_clock() | |
| return MarketClock( | |
| is_open=clock.is_open, | |
| next_open=str(clock.next_open), | |
| next_close=str(clock.next_close), | |
| ) | |
| except Exception as exc: | |
| logger.warning("get_market_clock failed: %s", exc) | |
| return MarketClock(is_open=False, next_open="Unknown", next_close="Unknown") | |
| # ββ News ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fetch_news(self, symbol: str, max_articles: int = 50, | |
| days_ago: int = 0) -> list[tuple[str, float]]: | |
| if self._demo or not hasattr(self, '_news_client') or self._news_client is None: | |
| return [] | |
| try: | |
| from alpaca.data.requests import NewsRequest | |
| now = datetime.now(tz=timezone.utc) | |
| target_date = now - timedelta(days=days_ago) | |
| day_start = target_date.replace(hour=0, minute=0, second=0, microsecond=0) | |
| day_end = target_date.replace(hour=23, minute=59, second=59) | |
| request = NewsRequest( | |
| symbols=symbol, | |
| start=day_start, | |
| end=day_end, | |
| limit=min(max_articles, 100), | |
| ) | |
| response = self._news_client.get_news(request) | |
| items = getattr(response, "news", response) if response else [] | |
| headlines: list[tuple[str, float]] = [] | |
| for item in items: | |
| title = getattr(item, "headline", "") or getattr(item, "title", "") | |
| if not title: | |
| continue | |
| created = getattr(item, "created_at", None) or getattr(item, "updated_at", None) | |
| if created: | |
| import pandas as pd | |
| ts = pd.Timestamp(created).timestamp() if isinstance(created, str) else float(created) | |
| else: | |
| ts = now.timestamp() | |
| headlines.append((title, float(ts))) | |
| return headlines | |
| except Exception as exc: | |
| logger.warning("Alpaca news fetch failed for %s: %s", symbol, exc) | |
| return [] | |
| # ββ Asset Search ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def get_all_assets(self) -> list[dict[str, str]]: | |
| """Fetch all available assets with their symbols and company names. | |
| Returns: | |
| List of dicts with 'symbol' and 'name' keys. | |
| """ | |
| if self._demo: | |
| # Return a basic hardcoded list for demo mode | |
| return [ | |
| {"symbol": "AAPL", "name": "Apple Inc."}, | |
| {"symbol": "TSLA", "name": "Tesla Inc."}, | |
| {"symbol": "NVDA", "name": "NVIDIA Corporation"}, | |
| {"symbol": "MSFT", "name": "Microsoft Corporation"}, | |
| {"symbol": "AMZN", "name": "Amazon.com Inc."}, | |
| {"symbol": "GOOGL", "name": "Alphabet Inc. Class A"}, | |
| {"symbol": "META", "name": "Meta Platforms Inc."}, | |
| {"symbol": "SPY", "name": "SPDR S&P 500 ETF Trust"}, | |
| ] | |
| try: | |
| from alpaca.trading.requests import GetAssetsRequest | |
| from alpaca.trading.enums import AssetStatus, AssetClass | |
| # Get all active US equity assets | |
| request = GetAssetsRequest( | |
| status=AssetStatus.ACTIVE, | |
| asset_class=AssetClass.US_EQUITY, | |
| ) | |
| assets = self._trading_client.get_all_assets(request) | |
| return [ | |
| {"symbol": asset.symbol, "name": asset.name} | |
| for asset in assets | |
| if asset.tradable | |
| ] | |
| except Exception as exc: | |
| logger.warning("Failed to fetch assets: %s", exc) | |
| return [] | |