alphax/app/integrations/binance_live.py
2026-05-22 23:17:37 +08:00

180 lines
7.6 KiB
Python

"""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"]