from __future__ import annotations import base64 import json import time import uuid import httpx from app.broker.base import Broker, Fill from app.market.coinbase import BASE_URL, to_product_id from app.models import OrderSide, utcnow class CoinbaseBroker(Broker): """Live Coinbase Advanced Trade execution (requires API credentials). Credentials are a Coinbase CDP API key + an EC private key (PEM). The key name is ``EXCHANGE_API_KEY`` and the PEM is ``EXCHANGE_API_SECRET`` (with literal ``\\n`` escapes allowed). """ def __init__( self, api_key: str, api_secret: str, base_url: str = BASE_URL, timeout: float = 10.0, ) -> None: if not api_key or not api_secret: raise ValueError( "Coinbase broker requires EXCHANGE_API_KEY and EXCHANGE_API_SECRET (PEM)" ) self.api_key = api_key self.api_secret = api_secret self._client = httpx.AsyncClient(base_url=base_url, timeout=timeout) def _token(self, method: str, path: str) -> str: from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import ec nonce = str(uuid.uuid4()) timestamp = str(int(time.time())) message = f"{timestamp}{method}{path}{nonce}" private_key = serialization.load_pem_private_key( self.api_secret.replace("\\n", "\n").encode(), password=None ) signature = private_key.sign(message.encode(), ec.ECDSA(hashes.SHA256())) return f"Bearer {self.api_key}:{base64.b64encode(signature).decode()}:{nonce}:{timestamp}" async def _market_order(self, symbol: str, side: OrderSide, qty: float) -> Fill: path = "/api/v3/brokerage/orders" body = { "client_order_id": str(uuid.uuid4()), "product_id": to_product_id(symbol), "side": side.value.upper(), "order_configuration": {"market_market_ioc": {"base_size": f"{qty:.8f}"}}, } response = await self._client.post( path, content=json.dumps(body), headers={ "Authorization": self._token("POST", path), "Content-Type": "application/json", }, ) response.raise_for_status() data = response.json() order = data.get("order", data) success = order.get("success", True) if not success: raise RuntimeError(f"coinbase order rejected: {order.get('failure_reason', order)}") filled = float(order.get("filled_size") or qty) price = float(order.get("average_filled_price") or 0.0) fee = float(order.get("total_fees") or 0.0) return Fill(symbol=symbol, side=side, qty=filled, price=price, fee=fee, ts=utcnow()) async def buy(self, symbol: str, qty: float, ref_price: float) -> Fill: return await self._market_order(symbol, OrderSide.BUY, qty) async def sell(self, symbol: str, qty: float, ref_price: float) -> Fill: return await self._market_order(symbol, OrderSide.SELL, qty) async def close(self) -> None: await self._client.aclose()