"""Broker API adapter - paper/live trading, orders, positions, balances.""" import hashlib import logging import uuid from datetime import datetime from typing import Any, Dict, Optional import httpx from .base import AdapterResult, BaseAdapter logger = logging.getLogger("broker_adapter") class BrokerAdapter(BaseAdapter): """Broker API adapter supporting paper and live modes.""" def __init__(self, api_key: str = "", api_secret: str = "", base_url: str = "", mode: str = "paper"): self.api_key = api_key self.api_secret = api_secret self.base_url = base_url self.mode = mode # paper | live def source_type(self) -> str: return "broker" def _headers(self) -> Dict[str, str]: return { "Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json", } async def fetch(self, ticker: str, config: Dict[str, Any]) -> AdapterResult: """Fetch positions and recent orders for a ticker.""" async with httpx.AsyncClient(timeout=30) as client: try: resp = await client.get( f"{self.base_url}/v2/positions/{ticker}", headers=self._headers(), ) raw = resp.content data = resp.json() if resp.status_code == 200 else {} content_hash = hashlib.sha256(raw).hexdigest() return AdapterResult( source_type="broker", ticker=ticker, items=[data] if data else [], raw_payload=raw, content_hash=content_hash, fetched_at=datetime.utcnow(), ) except Exception as e: logger.error(f"Broker fetch failed for {ticker}: {e}") return AdapterResult( source_type="broker", ticker=ticker, items=[], raw_payload=b"", content_hash="", fetched_at=datetime.utcnow(), error=str(e), ) async def submit_order( self, ticker: str, side: str, qty: float, order_type: str = "market", limit_price: Optional[float] = None, idempotency_key: Optional[str] = None, ) -> Dict[str, Any]: """Submit an order to the broker. Returns broker response.""" if self.mode == "live": logger.warning("LIVE order submission") idem_key = idempotency_key or str(uuid.uuid4()) payload = { "symbol": ticker, "qty": str(qty), "side": side, "type": order_type, "time_in_force": "day", } if limit_price and order_type == "limit": payload["limit_price"] = str(limit_price) async with httpx.AsyncClient(timeout=30) as client: try: resp = await client.post( f"{self.base_url}/v2/orders", headers={**self._headers(), "Idempotency-Key": idem_key}, json=payload, ) resp.raise_for_status() return resp.json() except httpx.HTTPStatusError as e: logger.error(f"Order rejected: {e.response.text}") return {"error": e.response.text, "status": e.response.status_code} except Exception as e: logger.error(f"Order submission failed: {e}") return {"error": str(e)} async def get_account(self) -> Dict[str, Any]: async with httpx.AsyncClient(timeout=30) as client: resp = await client.get(f"{self.base_url}/v2/account", headers=self._headers()) return resp.json()