"""Binance live-trading adapter. Only transport details live here. Business safety checks and audit logging stay in the live_trading DB/service layer. """ from __future__ import annotations import os import hashlib import hmac from dataclasses import dataclass from urllib.parse import urlencode import requests import ccxt class LiveTradingConfigError(RuntimeError): pass @dataclass class BinanceLiveClient: exchange: object market_type: str = "um_futures" def load_markets(self): return self.exchange.load_markets() def fetch_balance(self): return self.exchange.fetch_balance() def fetch_ticker(self, symbol: str): return self.exchange.fetch_ticker(symbol) def fetch_positions(self, symbols: list[str] | None = None): if hasattr(self.exchange, "fetch_positions"): return self.exchange.fetch_positions(symbols) return [] def fetch_open_orders(self, symbol: str | None = None): return self.exchange.fetch_open_orders(symbol) def fetch_orders(self, symbol: str | None = None, limit: int = 30): if hasattr(self.exchange, "fetch_orders"): return self.exchange.fetch_orders(symbol, None, limit) return [] def set_leverage(self, symbol: str, leverage: float): if hasattr(self.exchange, "set_leverage"): return self.exchange.set_leverage(int(leverage), symbol) return {"skipped": True, "reason": "exchange_does_not_support_set_leverage"} def amount_to_precision(self, symbol: str, amount: float) -> float: try: return float(self.exchange.amount_to_precision(symbol, amount)) except Exception: return float(amount) def price_to_precision(self, symbol: str, price: float) -> str: try: return str(self.exchange.price_to_precision(symbol, price)) except Exception: return str(price) def min_notional(self, symbol: str) -> float: try: market = self.exchange.market(symbol) limits = market.get("limits") if isinstance(market, dict) else {} cost = limits.get("cost") if isinstance(limits.get("cost"), dict) else {} value = cost.get("min") return float(value or 0) except Exception: return 0.0 def create_market_order(self, symbol: str, side: str, amount: float, params: dict | None = None): return self.exchange.create_order(symbol, "market", side, amount, None, params or {}) def create_limit_order(self, symbol: str, side: str, amount: float, price: float, params: dict | None = None): return self.exchange.create_order(symbol, "limit", side, amount, price, params or {}) def cancel_order(self, order_id: str, symbol: str): return self.exchange.cancel_order(order_id, symbol) def _market_id(self, symbol: str) -> str: try: return str(self.exchange.market(symbol).get("id") or symbol.replace("/", "")) except Exception: return symbol.replace("/", "") def _signed_fapi_request(self, method: str, path: str, params: dict) -> dict: timestamp = int(self.exchange.milliseconds()) if hasattr(self.exchange, "milliseconds") else 0 payload = {**(params or {}), "timestamp": timestamp} query = urlencode(payload, doseq=True) secret = str(getattr(self.exchange, "secret", "") or "") signature = hmac.new(secret.encode("utf-8"), query.encode("utf-8"), hashlib.sha256).hexdigest() signed_query = f"{query}&signature={signature}" urls = getattr(self.exchange, "urls", {}) if isinstance(getattr(self.exchange, "urls", {}), dict) else {} api_urls = urls.get("api") if isinstance(urls.get("api"), dict) else {} base_url = str(api_urls.get("fapiPrivate") or "https://fapi.binance.com/fapi/v1").rstrip("/") url = f"{base_url}{path}" headers = {"X-MBX-APIKEY": str(getattr(self.exchange, "apiKey", "") or "")} response = requests.request(method.upper(), url, params=signed_query, headers=headers, timeout=15) try: data = response.json() except Exception: data = {"raw": response.text} if response.status_code >= 400 or (isinstance(data, dict) and int(data.get("code", 0) or 0) < 0): raise ccxt.ExchangeError(f"binanceusdm {data}") return data def _create_algo_order(self, symbol: str, side: str, order_type: str, amount: float, trigger_price: float, params: dict | None = None): merged = { "algoType": "CONDITIONAL", "symbol": self._market_id(symbol), "side": side.upper(), "type": order_type, "quantity": self.exchange.amount_to_precision(symbol, amount), "triggerPrice": self.price_to_precision(symbol, trigger_price), "workingType": "CONTRACT_PRICE", "reduceOnly": "true", **(params or {}), } merged.pop("stopPrice", None) return self._signed_fapi_request("POST", "/algoOrder", merged) def create_stop_loss_order(self, symbol: str, side: str, amount: float, stop_price: float, params: dict | None = None): return self._create_algo_order(symbol, side, "STOP_MARKET", amount, stop_price, params) def create_take_profit_order(self, symbol: str, side: str, amount: float, stop_price: float, params: dict | None = None): return self._create_algo_order(symbol, side, "TAKE_PROFIT_MARKET", amount, stop_price, params) def cancel_algo_order(self, *, algo_id: str | int | None = None, client_algo_id: str | None = None): params: dict = {} if algo_id: params["algoId"] = algo_id if client_algo_id: params["clientAlgoId"] = client_algo_id if not params: raise ValueError("algo_id or client_algo_id is required") return self._signed_fapi_request("DELETE", "/algoOrder", params) def build_binance_client(account: dict, *, require_testnet: bool = True) -> BinanceLiveClient: market_type = str(account.get("market_type") or "um_futures") testnet = bool(account.get("testnet", True)) risk_config = account.get("risk_config") if isinstance(account.get("risk_config"), dict) else {} sandbox_mode = str(risk_config.get("sandbox_mode") or os.getenv("ALPHAX_LIVE_TRADING_SANDBOX_MODE", "demo")).strip().lower() if require_testnet and not testnet: raise LiveTradingConfigError("mainnet execution is not allowed by this smoke tester") api_key_env = str(account.get("api_key_env") or "ALPHAX_BINANCE_API_KEY") api_secret_env = str(account.get("api_secret_env") or "ALPHAX_BINANCE_API_SECRET") api_key = os.getenv(api_key_env, "").strip() api_secret = os.getenv(api_secret_env, "").strip() if not api_key or not api_secret: raise LiveTradingConfigError(f"missing Binance credentials env: {api_key_env}/{api_secret_env}") klass = ccxt.binanceusdm if market_type == "um_futures" else ccxt.binance exchange = klass({ "apiKey": api_key, "secret": api_secret, "enableRateLimit": True, "options": { "defaultType": "future" if market_type == "um_futures" else "spot", "fetchCurrencies": False, "warnOnFetchOpenOrdersWithoutSymbol": False, }, }) if testnet and sandbox_mode == "demo" and isinstance(getattr(exchange, "urls", None), dict) and exchange.urls.get("demo"): exchange.urls["api"] = exchange.urls["demo"] elif hasattr(exchange, "set_sandbox_mode"): exchange.set_sandbox_mode(testnet) return BinanceLiveClient(exchange=exchange, market_type=market_type) __all__ = ["BinanceLiveClient", "LiveTradingConfigError", "build_binance_client"]