""" Bitget 实盘交易服务 提供与 HyperliquidTradingService 一致的接口,底层调用 BitgetTradingAPI(ccxt)。 供 crypto_agent.py 的决策执行层使用。 """ import math from typing import Dict, List, Optional, Any from app.config import get_settings from app.services.bitget_trading_api_sdk import get_bitget_trading_api from app.utils.logger import logger # 合约面值表(张 → 币数量) CONTRACT_SIZES: Dict[str, float] = { 'BTC': 0.01, 'ETH': 0.1, 'LTC': 0.1, 'BCH': 0.1, 'BNB': 0.1, 'SOL': 1.0, 'AVAX': 1.0, 'LINK': 1.0, 'UNI': 1.0, 'ATOM': 1.0, 'FIL': 1.0, 'DOT': 1.0, 'XRP': 10.0, 'DOGE': 100.0, 'MATIC': 10.0, 'POL': 10.0, } class BitgetLiveTradingService: """ Bitget 实盘交易服务 接口与 HyperliquidTradingService 保持一致,方便 crypto_agent.py 统一调用。 """ def __init__(self): self.settings = get_settings() self.max_total_leverage: float = self.settings.bitget_max_total_leverage self.max_single_position: float = self.settings.bitget_max_single_position self.circuit_breaker_drawdown: float = self.settings.hyperliquid_circuit_breaker_drawdown self.trading_api = get_bitget_trading_api() if not self.trading_api: raise RuntimeError("Bitget 交易 API 初始化失败,请检查 API Key 配置") # 初始余额(用于回撤计算) self.initial_balance: Optional[float] = None self._initialize_account() logger.info( f"✅ BitgetLiveTradingService 初始化完成 " f"(最大总杠杆: {self.max_total_leverage}x, " f"单笔上限: ${self.max_single_position}, " f"熔断阈值: {self.circuit_breaker_drawdown * 100:.0f}%)" ) def _initialize_account(self): """初始化账户状态,记录初始余额""" try: state = self.get_account_state() self.initial_balance = state["account_value"] logger.info(f"Bitget 初始账户余额: ${self.initial_balance:.2f}") except Exception as e: logger.warning(f"Bitget 初始化账户余额失败: {e}") # ==================== 账户 ==================== def get_account_state(self) -> Dict[str, Any]: """ 获取账户状态 Returns: { "account_value": float, # 账户总价值(USDT) "total_margin_used": float, # 已用保证金 "available_balance": float, # 可用余额 } """ balance = self.trading_api.get_balance() usdt = balance.get('USDT', {}) available = float(usdt.get('available', 0) or 0) frozen = float(usdt.get('frozen', 0) or 0) account_value = available + frozen return { "account_value": account_value, "total_margin_used": frozen, "available_balance": available, } def check_risk_limits(self) -> Dict[str, Any]: """ 检查风控限制 Returns: {"allowed": bool, "reason": str} """ try: state = self.get_account_state() account_value = state["account_value"] # 1. 熔断检查 if self.initial_balance and self.initial_balance > 0: drawdown = (self.initial_balance - account_value) / self.initial_balance if drawdown >= self.circuit_breaker_drawdown: return { "allowed": False, "reason": f"熔断触发: 回撤 {drawdown * 100:.1f}% >= 阈值 {self.circuit_breaker_drawdown * 100:.0f}%" } # 2. 总杠杆检查 positions = self.get_open_positions() total_position_value = sum( abs(p["size"]) * p["entry_price"] for p in positions ) if account_value > 0: current_leverage = total_position_value / account_value if current_leverage >= self.max_total_leverage: return { "allowed": False, "reason": f"总杠杆超限: {current_leverage:.1f}x >= {self.max_total_leverage}x" } return {"allowed": True, "reason": ""} except Exception as e: logger.error(f"Bitget 风控检查异常: {e}") return {"allowed": False, "reason": f"风控检查异常: {e}"} # ==================== 持仓 ==================== def get_open_positions(self) -> List[Dict[str, Any]]: """ 获取所有持仓 Returns: [ { "coin": "BTC", "size": float, # 正数=多, 负数=空(以币为单位) "entry_price": float, "unrealized_pnl": float, "leverage": int, "liquidation_price": Optional[float], "position": dict, # 原始数据 } ] """ raw_positions = self.trading_api.get_position() result = [] for pos in raw_positions: contracts = float(pos.get('contracts', 0)) if contracts == 0: continue symbol_raw = pos.get('symbol', '') # e.g. "BTC/USDT:USDT" coin = symbol_raw.split('/')[0] if '/' in symbol_raw else symbol_raw contract_size = self.get_contract_size(coin) coin_amount = contracts * contract_size side = pos.get('side', 'long') size = coin_amount if side == 'long' else -coin_amount result.append({ "coin": coin, "size": size, "entry_price": float(pos.get('entryPrice', 0) or 0), "unrealized_pnl": float(pos.get('unrealizedPnl', 0) or 0), "leverage": int(float(pos.get('leverage', 1) or 1)), "liquidation_price": float(pos.get('liquidationPrice', 0) or 0) or None, "position": pos, }) return result def get_position_for_symbol(self, symbol: str) -> Optional[Dict[str, Any]]: """获取指定币种的持仓,无持仓返回 None""" coin = symbol.replace('USDT', '').replace('/', '').upper() for pos in self.get_open_positions(): if pos['coin'] == coin: return pos return None # ==================== 下单 ==================== def place_market_order( self, symbol: str, is_buy: bool, size: int, reduce_only: bool = False ) -> Dict[str, Any]: """ 市价下单 Args: symbol: 币种,如 "BTC" is_buy: True=买入/做多, False=卖出/做空 size: 合约数量(张) reduce_only: 是否只减仓 Returns: {"success": bool, "order_id": str, "symbol": str, "side": str, "size": int, "error"?: str} """ try: side = 'buy' if is_buy else 'sell' params = {} if reduce_only: params['reduceOnly'] = True ccxt_symbol = self.trading_api._standardize_symbol(symbol) contract_size = self.get_contract_size(symbol) actual_amount = size * contract_size order = self.trading_api.exchange.create_order( symbol=ccxt_symbol, type='market', side=side, amount=actual_amount, params={ 'tdMode': 'cross', 'marginCoin': 'USDT', 'holdMode': 'oneWay', **params } ) if not order: return {"success": False, "error": "下单返回空", "symbol": symbol, "side": side, "size": size} logger.info(f"✅ Bitget 市价单成功: {symbol} {side} {size}张") return { "success": True, "order_id": str(order.get('id', '')), "symbol": symbol, "side": side, "size": size, } except Exception as e: logger.error(f"❌ Bitget 市价单失败: {symbol} {e}") return {"success": False, "error": str(e), "symbol": symbol, "side": "buy" if is_buy else "sell", "size": size} def place_limit_order( self, symbol: str, is_buy: bool, size: int, price: float, reduce_only: bool = False ) -> Dict[str, Any]: """ 限价下单 Returns: { "success": bool, "order_status": "resting" | "filled", "order_id": str, "symbol": str, "side": str, "size": int, "price": float, "error"?: str } """ try: side = 'buy' if is_buy else 'sell' params = { 'tdMode': 'cross', 'marginCoin': 'USDT', 'holdMode': 'oneWay', } if reduce_only: params['reduceOnly'] = True ccxt_symbol = self.trading_api._standardize_symbol(symbol) contract_size = self.get_contract_size(symbol) actual_amount = size * contract_size order = self.trading_api.exchange.create_order( symbol=ccxt_symbol, type='limit', side=side, amount=actual_amount, price=price, params=params ) if not order: return { "success": False, "error": "下单返回空", "symbol": symbol, "side": side, "size": size, "price": price, } # 判断订单状态:open → resting(挂单中),closed → filled(立即成交) raw_status = order.get('status', 'open') order_status = 'filled' if raw_status == 'closed' else 'resting' logger.info(f"✅ Bitget 限价单: {symbol} {side} {size}张 @ {price} [{order_status}]") return { "success": True, "order_status": order_status, "order_id": str(order.get('id', '')), "symbol": symbol, "side": side, "size": size, "price": price, } except Exception as e: logger.error(f"❌ Bitget 限价单失败: {symbol} {e}") return { "success": False, "error": str(e), "symbol": symbol, "side": "buy" if is_buy else "sell", "size": size, "price": price, } # ==================== 止盈止损 ==================== def set_tp_sl( self, symbol: str, is_long: bool, size: int, tp_price: Optional[float] = None, sl_price: Optional[float] = None ) -> Dict[str, Any]: """ 设置止盈止损 Returns: {"success": bool, "results": [...], "error"?: str} """ try: success = self.trading_api.modify_sl_tp( symbol=symbol, stop_loss=sl_price, take_profit=tp_price, ) if success: logger.info(f"✅ Bitget TP/SL 设置成功: {symbol} TP={tp_price} SL={sl_price}") return { "success": True, "results": [ {"type": "take_profit", "price": tp_price}, {"type": "stop_loss", "price": sl_price}, ] } else: return {"success": False, "error": "modify_sl_tp 返回 False", "results": []} except Exception as e: logger.error(f"❌ Bitget 设置 TP/SL 失败: {symbol} {e}") return {"success": False, "error": str(e), "results": []} def get_tp_sl_prices(self, symbol: str) -> Dict[str, Optional[float]]: """ 从挂单中读取止盈止损价格 Returns: {"take_profit": float|None, "stop_loss": float|None} """ result = {"take_profit": None, "stop_loss": None} try: orders = self.trading_api.get_open_orders(symbol) for order in orders: if not order.get('reduceOnly'): continue order_side = order.get('side', '') price = float(order.get('price', 0) or 0) order_type = order.get('type', '') # stop 类型通常是止损,limit 类型通常是止盈 if 'stop' in order_type.lower(): result['stop_loss'] = price elif order_type == 'limit' and price: result['take_profit'] = price except Exception as e: logger.warning(f"Bitget 获取 TP/SL 价格失败: {symbol} {e}") return result # ==================== 挂单管理 ==================== def get_open_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: """ 获取挂单列表 Returns: [{"order_id", "symbol", "side", "size", "price", "is_reduce_only"}] """ raw_orders = self.trading_api.get_open_orders(symbol) result = [] for order in raw_orders: sym_raw = order.get('symbol', '') coin = sym_raw.split('/')[0] if '/' in sym_raw else sym_raw contracts = float(order.get('amount', 0) or 0) contract_size = self.get_contract_size(coin) size_in_coins = contracts # ccxt amount 已是币数量 result.append({ "order_id": str(order.get('id', '')), "symbol": coin, "side": order.get('side', ''), "size": size_in_coins, "price": float(order.get('price', 0) or 0), "is_reduce_only": bool(order.get('reduceOnly', False)), "order_type": order.get('type', ''), }) return result def cancel_all_orders(self, symbol: Optional[str] = None) -> Dict[str, Any]: """ 撤销指定币种的所有挂单 Returns: {"success": bool, "cancelled": int, "error"?: str} """ try: success = self.trading_api.cancel_all_orders(symbol or '') if success: logger.info(f"✅ Bitget 撤销挂单成功: {symbol or '全部'}") return {"success": True, "cancelled": 1} else: return {"success": False, "cancelled": 0, "error": "cancel_all_orders 返回 False"} except Exception as e: logger.error(f"❌ Bitget 撤销挂单失败: {symbol} {e}") return {"success": False, "cancelled": 0, "error": str(e)} def cancel_tp_sl_orders(self, symbol: str) -> Dict[str, Any]: """撤销止盈止损单(reduce-only 挂单)""" return self.cancel_all_orders(symbol) # ==================== 杠杆 ==================== def update_leverage(self, symbol: str, leverage: int): """设置杠杆倍数""" try: self.trading_api.set_leverage(symbol, leverage) except Exception as e: logger.warning(f"Bitget 设置杠杆失败: {symbol} {leverage}x: {e}") # ==================== 辅助方法 ==================== def get_contract_size(self, symbol: str) -> float: """ 获取合约面值(1张合约对应的币数量) 优先从硬编码表获取,不存在则查询 ccxt 市场信息。 """ coin = symbol.replace('USDT', '').replace('/', '').upper() if coin in CONTRACT_SIZES: return CONTRACT_SIZES[coin] # fallback: 从 ccxt market info 获取 try: ccxt_symbol = self.trading_api._standardize_symbol(symbol) market = self.trading_api.exchange.market(ccxt_symbol) size = float(market.get('contractSize', 1) or 1) logger.info(f"Bitget 从市场信息获取合约面值: {coin} = {size}") return size except Exception: logger.warning(f"Bitget 无法获取 {coin} 合约面值,使用默认值 1") return 1.0 def coins_to_contracts(self, symbol: str, coin_amount: float, price: float = 1.0) -> int: """ 将币数量转换为合约张数(向下取整) Args: symbol: 币种 coin_amount: 要转换的币数量 price: 当前价格(未使用,保留接口兼容) Returns: 整数合约张数 """ contract_size = self.get_contract_size(symbol) if contract_size <= 0: return 0 return math.floor(coin_amount / contract_size) def market_close_all(self) -> Dict[str, Any]: """市价平仓所有持仓""" results = [] positions = self.get_open_positions() for pos in positions: coin = pos['coin'] is_long = pos['size'] > 0 contracts = self.coins_to_contracts(coin, abs(pos['size'])) if contracts < 1: continue result = self.place_market_order(coin, is_buy=not is_long, size=contracts, reduce_only=True) results.append(result) all_ok = all(r.get('success') for r in results) return {"success": all_ok, "results": results} # ==================== 单例工厂 ==================== _bitget_live_service: Optional[BitgetLiveTradingService] = None def get_bitget_live_service() -> Optional[BitgetLiveTradingService]: """ 获取 BitgetLiveTradingService 单例。 bitget_trading_enabled=False 时返回 None(功能关闭)。 """ global _bitget_live_service settings = get_settings() if not settings.bitget_trading_enabled: return None if _bitget_live_service is None: try: _bitget_live_service = BitgetLiveTradingService() except Exception as e: logger.error(f"❌ BitgetLiveTradingService 初始化失败: {e}") return None return _bitget_live_service def reset_bitget_live_service(): """重置单例(测试用)""" global _bitget_live_service _bitget_live_service = None