from __future__ import annotations import hashlib import hmac import time from urllib.parse import urlencode import httpx from app.broker.base import Broker, Fill from app.market.binance import BASE_URL, to_exchange_symbol from app.models import OrderSide, utcnow TESTNET_URL = "https://testnet.binance.vision" class BinanceBroker(Broker): """Live Binance spot market-order execution (requires API credentials).""" 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("Binance broker requires EXCHANGE_API_KEY and EXCHANGE_API_SECRET") self.api_key = api_key self.api_secret = api_secret.encode() self._client = httpx.AsyncClient(base_url=base_url, timeout=timeout) def _sign(self, params: dict) -> dict: params = {**params, "timestamp": int(time.time() * 1000), "recvWindow": 5000} query = urlencode(params) signature = hmac.new(self.api_secret, query.encode(), hashlib.sha256).hexdigest() return {**params, "signature": signature} async def _market_order(self, symbol: str, side: OrderSide, qty: float) -> Fill: params = self._sign( { "symbol": to_exchange_symbol(symbol), "side": side.value.upper(), "type": "MARKET", "quantity": f"{qty:.8f}".rstrip("0").rstrip("."), } ) response = await self._client.post( "/api/v3/order", params=params, headers={"X-MBX-APIKEY": self.api_key}, ) response.raise_for_status() data = response.json() executed = float(data.get("executedQty", qty)) quote = float(data.get("cummulativeQuoteQty", 0.0)) price = quote / executed if executed else 0.0 fee = sum(float(fill.get("commission", 0.0)) for fill in data.get("fills", [])) return Fill( symbol=symbol, side=side, qty=executed, 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()