""" Bitget 实盘交易执行器 """ from typing import Dict, Any, List, Optional from app.crypto_agent.executor.base_executor import BaseExecutor from app.services.bitget_live_trading_service import get_bitget_live_service from app.utils.logger import logger import re class BitgetExecutor(BaseExecutor): """Bitget 实盘交易执行器""" def __init__(self, service=None, account_id: str = "default"): super().__init__("Bitget") self.account_id = (account_id or "default").strip() or "default" self.bitget = service or get_bitget_live_service(self.account_id) def _notification_context(self) -> Dict[str, str]: account_id = getattr(self, 'account_id', 'default') or 'default' return { 'account_id': account_id, 'target_key': f'Bitget:{account_id}', } # ==================== 核心执行方法 ==================== async def execute_open(self, decision: Dict[str, Any], current_price: float) -> Dict[str, Any]: """执行开仓""" try: symbol = decision.get('symbol', '').replace('USDT', '') action = decision.get('signal_action', decision.get('action')) # buy/sell margin = decision.get('margin', decision.get('quantity', 0)) entry_price = decision.get('entry_price', current_price) stop_loss = decision.get('stop_loss') take_profit = decision.get('take_profit') leverage = min(decision.get('leverage', self.bitget.settings.bitget_default_leverage), 10) # 决定订单类型 order_type, order_reason = self.decide_order_type(decision, current_price) logger.info(f" 订单类型: {order_reason}") # 调整保证金(预留手续费) account_state = self.bitget.get_account_state() available = account_state.get('available_balance', 0) adjusted_margin = self.calculate_effective_margin(available, margin) # 计算合约张数,必须与实际执行杠杆保持一致 contracts = self._calculate_contracts(symbol, adjusted_margin, entry_price, leverage) actual_position_value = contracts * self.bitget.get_contract_size(symbol) * entry_price leverage_ok, leverage_reason, effective_leverage = self.validate_effective_leverage( decision, adjusted_margin, actual_position_value, ) if contracts < 1: return { 'success': False, 'error': ( f'仓位计算结果 {contracts} 张,低于最小下单量 ' f'(保证金=${adjusted_margin:.2f}, 杠杆={leverage}x)' ) } if not leverage_ok: return { 'success': False, 'error': leverage_reason, 'effective_leverage': effective_leverage, } # 设置杠杆 self.bitget.update_leverage(symbol, leverage) # 下单 is_buy = (action == 'buy') if order_type == 'market': result = self.bitget.place_market_order(symbol, is_buy=is_buy, size=contracts) else: result = self.bitget.place_limit_order(symbol, is_buy=is_buy, size=contracts, price=entry_price) if not result.get('success'): return result order_id = result.get('order_id') order_status = result.get('order_status', 'filled') result['contracts'] = contracts result['margin'] = adjusted_margin result['leverage'] = leverage result['order_type'] = order_type result['entry_price'] = entry_price result['actual_position_value'] = actual_position_value result['effective_leverage'] = effective_leverage # 设置止盈止损 if stop_loss or take_profit: is_buy = (action == 'buy') if order_status == 'filled': # 市价单已成交,直接设置 TP/SL try: tp_sl_result = self.bitget.set_tp_sl( symbol=symbol, is_long=is_buy, size=contracts, tp_price=take_profit, sl_price=stop_loss ) tp_set = tp_sl_result.get('tp_set', False) sl_set = tp_sl_result.get('sl_set', False) if tp_set and sl_set: logger.info(f" ✅ 止盈止损已设置: TP={take_profit}, SL={stop_loss}") elif tp_set or sl_set: # 部分成功:记录缺失侧到 pending missing_tp = take_profit if not tp_set else None missing_sl = stop_loss if not sl_set else None result['pending_tp_sl'] = { 'tp_price': missing_tp, 'sl_price': missing_sl } result['contracts'] = contracts set_text = "TP" if tp_set else "SL" fail_text = "TP" if not tp_set else "SL" logger.warning(f" ⚠️ 止盈止损部分成功: {set_text}已设, {fail_text}待补设") result['tp_sl_warning'] = f"{fail_text}设置失败,已加入待补设列表" else: # 全部失败:记录到 pending 等待补救 result['pending_tp_sl'] = { 'tp_price': take_profit, 'sl_price': stop_loss } result['contracts'] = contracts errors = tp_sl_result.get('errors', []) logger.warning(f" ⚠️ 止盈止损设置失败,已加入待补设列表: {errors}") result['tp_sl_warning'] = f"TP/SL设置失败: {'; '.join(errors)}" except Exception as tp_sl_err: logger.error(f" ⚠️ 止盈止损设置异常: {tp_sl_err}") result['pending_tp_sl'] = { 'tp_price': take_profit, 'sl_price': stop_loss } result['contracts'] = contracts result['tp_sl_warning'] = str(tp_sl_err) else: # 限价单未成交,延迟到持仓确认后设置 result['pending_tp_sl'] = { 'tp_price': take_profit, 'sl_price': stop_loss } result['contracts'] = contracts logger.info(f" 📌 限价单待成交,TP/SL 将在成交后自动设置: TP={take_profit}, SL={stop_loss}") logger.info(f" ✅ 开仓成功: {symbol} {contracts}张 @ ${order_type}") # 开仓成功通知由 crypto_agent 统一发送,避免与执行摘要重复 return result except Exception as e: logger.error(f"Bitget 开仓失败: {e}") error_result = {'success': False, 'error': str(e)} # 发送失败通知 await self.send_execution_notification( operation='OPEN', symbol=decision.get('symbol', ''), result=error_result, details=self._notification_context() ) return error_result async def execute_close(self, decision: Dict[str, Any], current_price: float) -> Dict[str, Any]: """执行平仓""" try: symbol = decision.get('symbol', '').replace('USDT', '') orders_to_close = decision.get('orders_to_close', []) # Bitget 持仓是按 symbol 聚合管理,不能按 order_id 精确平仓。 result = self.bitget.market_close_position(symbol) if result.get('success'): logger.info(f" ✅ 平仓成功: {symbol}") else: logger.warning(f" ⚠️ 平仓失败: {symbol} - {result.get('error', '未知错误')}") if orders_to_close: result['requested_order_ids'] = orders_to_close # 发送飞书通知 await self.send_execution_notification( operation='CLOSE', symbol=symbol, result=result, details=self._notification_context() ) return result except Exception as e: logger.error(f"Bitget 平仓失败: {e}") error_result = {'success': False, 'error': str(e)} # 发送失败通知 await self.send_execution_notification( operation='CLOSE', symbol=decision.get('symbol', ''), result=error_result, details=self._notification_context() ) return error_result async def execute_cancel(self, order_id: str, symbol: str) -> Dict[str, Any]: """执行撤单""" try: result = self.bitget.cancel_order(symbol.replace('USDT', ''), order_id) if result.get('success'): logger.info(f" ✅ 撤单成功: {order_id}") # 发送飞书通知 await self.send_execution_notification( operation='CANCEL', symbol=symbol, result=result, details={'order_id': order_id, **self._notification_context()} ) return result except Exception as e: logger.error(f"Bitget 撤单失败: {e}") error_result = {'success': False, 'error': str(e), 'order_id': order_id} # 发送失败通知 await self.send_execution_notification( operation='CANCEL', symbol=symbol, result=error_result, details={'order_id': order_id, **self._notification_context()} ) return error_result async def set_stop_loss_take_profit(self, symbol: str, order_id: str, stop_loss: Optional[float], take_profit: Optional[float], position_size: float) -> Dict[str, Any]: """设置止盈止损""" try: # Bitget 需要知道方向 positions = self.bitget.get_open_positions() pos = next((p for p in positions if p.get('coin') == symbol.replace('USDT', '')), None) if not pos: return {'success': False, 'message': f'找不到 {symbol} 的持仓'} is_long = pos['size'] > 0 result = self.bitget.set_tp_sl( symbol=symbol.replace('USDT', ''), is_long=is_long, size=position_size, tp_price=take_profit, sl_price=stop_loss ) if result.get('success'): logger.info(f" ✅ 止盈止损设置成功: SL=${stop_loss}, TP={take_profit}") return result except Exception as e: logger.error(f"Bitget 设置止盈止损失败: {e}") return {'success': False, 'message': str(e)} def should_set_tp_sl_on_order(self) -> bool: """Bitget 不支持在下单时设置 TP/SL""" return False # ==================== 平台特定配置 ==================== def get_market_order_threshold(self) -> float: """市价单阈值: 0.2%""" return 0.2 def get_pending_order_timeout(self) -> float: """挂单超时: 24 小时(Bitget 流动性好)""" return 24.0 def get_position_exit_rules(self) -> tuple: """持仓退出规则:(目标盈利 3%, 无最大持仓时间限制)""" return (3.0, float('inf')) def get_fee_rate(self) -> float: """手续费率: 0.06% (taker)""" return 0.0006 def get_max_retries(self) -> int: """最大重试次数: 5(Bitget API 限流严格)""" return 5 def is_rate_limit_error(self, error_msg: str) -> bool: """判断是否是限流错误""" rate_limit_indicators = [ 'rate limit', 'too many requests', '429', 'limit exceeded', '请求频率' ] return any(indicator in error_msg.lower() for indicator in rate_limit_indicators) def get_rate_limit_wait_time(self, error_msg: str, attempt: int) -> float: """ 获取限流等待时间 Bitget API 限流:指数退避 + 抖动 """ base_wait = min(2 ** attempt, 60) # 指数退避,最大 60s # 检查错误信息中是否包含等待时间 wait_match = re.search(r'wait\s+(\d+)\s*s', error_msg, re.IGNORECASE) if wait_match: suggested_wait = int(wait_match.group(1)) return min(suggested_wait, 60) # 指数退避 + 随机抖动 import random jitter = random.uniform(0.5, 1.5) return base_wait * jitter def get_price_update_threshold(self) -> float: """价格更新阈值: 0.5%""" return 0.5 # ==================== 移动止损 ==================== async def move_stop_loss(self, symbol: str, new_stop_loss: float, current_stop_loss: Optional[float] = None) -> Dict[str, Any]: """ 移动止损(Bitget) Args: symbol: 交易对(如 BTCUSDT) new_stop_loss: 新止损价 current_stop_loss: 当前止损价(可选) Returns: {'success': bool, 'message': str} """ try: position = self.bitget.get_position_for_symbol(symbol) if not position: return {'success': False, 'message': f'找不到 {symbol} 的持仓'} tp_sl_prices = self.bitget.get_tp_sl_prices(symbol.replace('USDT', '')) result = self.bitget.set_tp_sl( symbol=symbol.replace('USDT', ''), is_long=position['size'] > 0, size=abs(position['size']), tp_price=tp_sl_prices.get('take_profit'), sl_price=new_stop_loss ) if result.get('success'): logger.info(f" ✅ 移动止损成功: {symbol} → ${new_stop_loss:.2f}") return {'success': True, 'message': f'移动止损成功: {new_stop_loss:.2f}'} else: errors = result.get('errors', []) return {'success': False, 'message': '; '.join(errors) if errors else '移动止损失败'} except Exception as e: logger.error(f"Bitget 移动止损失败: {e}") return {'success': False, 'message': str(e)} # ==================== 辅助方法 ==================== def _calculate_contracts(self, symbol: str, margin: float, price: float, leverage: int) -> int: """计算合约张数""" try: # 获取合约规格 contract_size = self.bitget.get_contract_size(symbol.replace('USDT', '')) # 计算持仓价值 position_value = margin * leverage # 计算币数量 coin_amount = position_value / price # 计算合约张数(向下取整) contracts = int(coin_amount / contract_size) logger.info( f" 仓位计算: ${margin:.2f} × {leverage}x = ${position_value:.2f} " f"→ {coin_amount:.6f} {symbol} → {contracts} 张" ) return contracts except Exception as e: logger.error(f"计算合约张数失败: {e}") return 0