""" 交易执行器基类 为不同平台提供统一的交易执行接口,各平台可根据自身特性实现具体逻辑。 """ from abc import ABC, abstractmethod from typing import Dict, Any, List, Optional from datetime import datetime, timedelta from app.utils.logger import logger class BaseExecutor(ABC): """交易执行器基类""" def __init__(self, platform_name: str): self.platform_name = platform_name # 初始化飞书通知服务 try: from app.services.feishu_service import get_feishu_paper_trading_service self.feishu = get_feishu_paper_trading_service() except Exception as e: logger.warning(f"[{self.platform_name}] 飞书服务初始化失败: {e}") self.feishu = None # 延迟导入飞书服务,避免循环依赖 self._feishu_service = None # ==================== 核心执行方法 ==================== @abstractmethod async def execute_open(self, decision: Dict[str, Any], current_price: float) -> Dict[str, Any]: """ 执行开仓 Args: decision: 决策字典(包含 symbol, action, margin, stop_loss, take_profit 等) current_price: 当前价格 Returns: 执行结果 {'success': bool, 'order_id': str, 'message': str, ...} """ pass @abstractmethod async def execute_close(self, decision: Dict[str, Any], current_price: float) -> Dict[str, Any]: """执行平仓""" pass @abstractmethod async def execute_cancel(self, order_id: str, symbol: str) -> Dict[str, Any]: """执行撤单""" pass # ==================== 订单类型决策 ==================== def decide_order_type(self, signal: Dict[str, Any], current_price: float) -> tuple: """ 决定订单类型(市价/限价) Returns: (order_type, reason) - 'market' 或 'limit' """ entry_price = signal.get('entry_price', current_price) if not entry_price or entry_price == 0: return 'market', "无入场价,使用市价单" price_diff_pct = abs(entry_price - current_price) / current_price * 100 # 平台特定的阈值 threshold = self.get_market_order_threshold() if price_diff_pct < threshold: return 'market', f"价格差 {price_diff_pct:.3f}% < {threshold}%,使用市价单" else: return 'limit', f"价格差 {price_diff_pct:.3f}% >= {threshold}%,使用限价单 @ ${entry_price:.2f}" @abstractmethod def get_market_order_threshold(self) -> float: """ 获取市价单阈值(百分比) 价格差小于此阈值时使用市价单 """ pass # ==================== 止盈止损设置 ==================== @abstractmethod 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]: """ 设置止盈止损 Args: symbol: 交易对 order_id: 订单ID(或持仓ID) stop_loss: 止损价 take_profit: 止盈价 position_size: 持仓数量 Returns: {'success': bool, 'message': str} """ pass def should_set_tp_sl_on_order(self) -> bool: """ 是否在下单时设置止盈止损 Returns: True: 下单参数中携带 TP/SL False: 成交后单独设置 """ return False # 默认成交后设置 # ==================== 挂单超时管理 ==================== def check_pending_order_timeout(self, pending_orders: List[Dict], timeout_hours: Optional[float] = None) -> List[Dict[str, Any]]: """ 检查挂单超时 Returns: 需要取消的订单列表 """ if timeout_hours is None: timeout_hours = self.get_pending_order_timeout() timeout_orders = [] now = datetime.now() for order in pending_orders: created_at = order.get('created_at') if not created_at: continue # 解析时间 if isinstance(created_at, str): created_at = datetime.fromisoformat(created_at.replace('Z', '+00:00')) age_hours = (now - created_at).total_seconds() / 3600 if age_hours > timeout_hours: timeout_orders.append({ 'order_id': order.get('order_id'), 'symbol': order.get('symbol'), 'age_hours': age_hours, 'action': 'CANCEL', 'reason': f"挂单超时 {age_hours:.1f}h > {timeout_hours}h" }) return timeout_orders @abstractmethod def get_pending_order_timeout(self) -> float: """ 获取挂单超时时间(小时) Returns: 超时小时数 """ pass # ==================== 持仓管理 ==================== def check_position_management(self, positions: List[Dict], current_prices: Dict[str, float]) -> List[Dict[str, Any]]: """ 持仓管理检查 Returns: 建议的操作列表 """ actions = [] now = datetime.now() for pos in positions: symbol = pos.get('symbol') current_price = current_prices.get(symbol, pos.get('entry_price', 0)) if current_price <= 0: continue # 计算盈亏百分比 entry_price = pos.get('entry_price', 0) side = pos.get('side') if side == 'buy': pnl_pct = (current_price - entry_price) / entry_price * 100 else: pnl_pct = (entry_price - current_price) / entry_price * 100 # 计算持仓时长 opened_at = pos.get('opened_at') if opened_at: if isinstance(opened_at, str): opened_at = datetime.fromisoformat(opened_at.replace('Z', '+00:00')) hold_hours = (now - opened_at).total_seconds() / 3600 else: hold_hours = 0 # 获取平台特定规则 target_profit_pct, max_hold_hours = self.get_position_exit_rules() # 规则1: 达到目标盈利 if pnl_pct >= target_profit_pct: actions.append({ 'symbol': symbol, 'action': 'TAKE_PROFIT', 'reason': f"盈利 {pnl_pct:.1f}% >= {target_profit_pct}%", 'priority': 1 }) # 规则2: 持仓超时 if hold_hours > max_hold_hours: actions.append({ 'symbol': symbol, 'action': 'TIME_EXIT', 'reason': f"持仓 {hold_hours:.1f}h > {max_hold_hours}h", 'priority': 2 }) # 规则3: 移动止损 if pnl_pct >= 2: current_sl = pos.get('stop_loss') if side == 'buy' and current_sl and current_sl < entry_price: actions.append({ 'symbol': symbol, 'action': 'MOVE_SL', 'new_sl': entry_price, 'reason': f"盈利 {pnl_pct:.1f}% >= 2%,移动止损到入场价", 'priority': 3 }) elif side == 'sell' and current_sl and current_sl > entry_price: actions.append({ 'symbol': symbol, 'action': 'MOVE_SL', 'new_sl': entry_price, 'reason': f"盈利 {pnl_pct:.1f}% >= 2%,移动止损到入场价", 'priority': 3 }) # 按优先级排序 actions.sort(key=lambda x: x.get('priority', 99)) return actions @abstractmethod def get_position_exit_rules(self) -> tuple: """ 获取持仓退出规则 Returns: (target_profit_pct, max_hold_hours) """ pass # ==================== 交易成本管理 ==================== def calculate_effective_margin(self, available: float, margin: float) -> float: """ 计算实际可用保证金(预留手续费) Args: available: 可用余额 margin: 计算出的保证金 Returns: 调整后的保证金 """ # 获取平台手续费率 fee_rate = self.get_fee_rate() # 预留开仓 + 平仓手续费 fee_reserve = margin * fee_rate * 2 # 调整保证金 adjusted_margin = margin + fee_reserve # 不超过可用余额的 99% max_usable = available * 0.99 adjusted_margin = min(adjusted_margin, max_usable) if adjusted_margin < margin: logger.info(f"[{self.platform_name}] 保证金调整: ${margin:.2f} → ${adjusted_margin:.2f} " f"(预留手续费 ${(fee_reserve):.2f})") return adjusted_margin @abstractmethod def get_fee_rate(self) -> float: """ 获取手续费率 Returns: 手续费率(如 0.0006 = 0.06%) """ pass # ==================== API 重试机制 ==================== async def execute_with_retry(self, func, max_retries: int = None, delay: float = 1.0) -> Any: """ 带 API 限流感知的重试机制 Args: func: 要执行的异步函数 max_retries: 最大重试次数(None 则使用平台默认) delay: 初始延迟秒数 Returns: 函数返回值 """ if max_retries is None: max_retries = self.get_max_retries() last_error = None for attempt in range(max_retries): try: return await func() except Exception as e: last_error = e error_msg = str(e) # 检查是否是限流错误 if self.is_rate_limit_error(error_msg): wait_time = self.get_rate_limit_wait_time(error_msg, attempt) logger.warning(f"[{self.platform_name}] API 限流,等待 {wait_time}s 后重试 " f"(尝试 {attempt + 1}/{max_retries})") import asyncio await asyncio.sleep(wait_time) # 其他错误 else: if attempt < max_retries - 1: wait_time = delay * (attempt + 1) logger.warning(f"[{self.platform_name}] 执行失败: {error_msg}," f"{wait_time}s 后重试 (尝试 {attempt + 1}/{max_retries})") import asyncio await asyncio.sleep(wait_time) else: logger.error(f"[{self.platform_name}] 执行失败,已达最大重试次数: {error_msg}") raise last_error @abstractmethod def get_max_retries(self) -> int: """获取最大重试次数""" pass @abstractmethod def is_rate_limit_error(self, error_msg: str) -> bool: """判断是否是限流错误""" pass @abstractmethod def get_rate_limit_wait_time(self, error_msg: str, attempt: int) -> float: """ 获取限流等待时间 Args: error_msg: 错误信息 attempt: 当前尝试次数 Returns: 等待秒数 """ pass # ==================== 挂单价格优化 ==================== def should_update_pending_order(self, new_price: float, old_price: float, side: str) -> tuple: """ 是否需要更新挂单价格 Args: new_price: 新价格 old_price: 旧价格 side: 方向 (buy/sell) Returns: (should_update, reason) """ price_diff_pct = abs(new_price - old_price) / old_price * 100 threshold = self.get_price_update_threshold() if price_diff_pct < threshold: return False, f"价格差 {price_diff_pct:.3f}% < {threshold}%,保持原挂单" # 检查是否更优 if side == 'buy': # 做多:价格更低更优 is_better = new_price < old_price if is_better: return True, f"新价格更低(更优),更新挂单 ${old_price:.2f} → ${new_price:.2f}" else: return False, "新价格更高(更差),保持原挂单" else: # 做空:价格更高更优 is_better = new_price > old_price if is_better: return True, f"新价格更高(更优),更新挂单 ${old_price:.2f} → ${new_price:.2f}" else: return False, "新价格更低(更差),保持原挂单" @abstractmethod def get_price_update_threshold(self) -> float: """ 获取价格更新阈值(百分比) Returns: 价格差异阈值(如 0.5 = 0.5%) """ pass # ==================== 移动止损 ==================== @abstractmethod async def move_stop_loss(self, symbol: str, new_stop_loss: float, current_stop_loss: Optional[float] = None) -> Dict[str, Any]: """ 移动止损 Args: symbol: 交易对 new_stop_loss: 新的止损价 current_stop_loss: 当前止损价(可选) Returns: {'success': bool, 'message': str} """ pass # ==================== 飞书通知 ==================== async def send_execution_notification(self, operation: str, symbol: str, result: Dict[str, Any], details: Optional[Dict[str, Any]] = None): """ 发送执行结果通知(统一入口) Args: operation: 操作类型 ('OPEN', 'CLOSE', 'CANCEL', 'TP_SL') symbol: 交易对 result: 执行结果 {'success': bool, 'order_id': str, ...} details: 额外详情 """ if not self.feishu: return try: success = result.get('success', False) order_id = result.get('order_id', '') error_msg = result.get('error', '') # 根据操作类型选择通知方法 if operation == 'OPEN': await self._send_open_notification(symbol, result, details) elif operation == 'CLOSE': await self._send_close_notification(symbol, result, details) elif operation == 'CANCEL': await self._send_cancel_notification(symbol, result, details) elif operation == 'TP_SL': await self._send_tp_sl_notification(symbol, result, details) elif operation == 'POSITION_MANAGEMENT': await self._send_position_management_notification(symbol, result, details) else: # 通用通知 await self._send_generic_notification(operation, symbol, result, details) except Exception as e: logger.error(f"[{self.platform_name}] 发送执行通知失败: {e}") async def _send_open_notification(self, symbol: str, result: Dict[str, Any], details: Optional[Dict[str, Any]] = None): """发送开仓通知""" success = result.get('success', False) order_id = result.get('order_id', '') error_msg = result.get('error', '') if success: # 成功开仓 title = f"✅ [{self.platform_name}] 开仓成功 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", f"**订单ID**: {order_id}", ] # 添加详情 if details: if 'size' in details: content_parts.append(f"**数量**: {details['size']}") if 'price' in details: content_parts.append(f"**价格**: ${details['price']:,.2f}") if 'margin' in details: content_parts.append(f"**保证金**: ${details['margin']:,.2f}") if 'leverage' in details: content_parts.append(f"**杠杆**: {details['leverage']}x") if 'stop_loss' in details and details['stop_loss']: content_parts.append(f"**止损**: ${details['stop_loss']:,.2f}") if 'take_profit' in details and details['take_profit']: content_parts.append(f"**止盈**: ${details['take_profit']:,.2f}") if 'order_type' in details: content_parts.append(f"**订单类型**: {details['order_type']}") content = "\n".join(content_parts) color = "green" else: # 开仓失败 title = f"❌ [{self.platform_name}] 开仓失败 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", f"**错误**: {error_msg}", ] if details and 'reason' in details: content_parts.append(f"**原因**: {details['reason']}") content = "\n".join(content_parts) color = "red" await self.feishu.send_card(title, content, color) async def _send_close_notification(self, symbol: str, result: Dict[str, Any], details: Optional[Dict[str, Any]] = None): """发送平仓通知""" success = result.get('success', False) error_msg = result.get('error', '') if success: title = f"✅ [{self.platform_name}] 平仓成功 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", ] if details: if 'pnl' in details: pnl = details['pnl'] pnl_color = "盈利" if pnl >= 0 else "亏损" content_parts.append(f"**{pnl_color}**: ${pnl:,.2f}") if 'pnl_percent' in details: content_parts.append(f"**收益率**: {details['pnl_percent']:.2f}%") if 'exit_reason' in details: content_parts.append(f"**平仓原因**: {details['exit_reason']}") content = "\n".join(content_parts) color = "green" else: title = f"❌ [{self.platform_name}] 平仓失败 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", f"**错误**: {error_msg}", ] content = "\n".join(content_parts) color = "red" await self.feishu.send_card(title, content, color) async def _send_cancel_notification(self, symbol: str, result: Dict[str, Any], details: Optional[Dict[str, Any]] = None): """发送撤单通知""" success = result.get('success', False) order_id = result.get('order_id', '') error_msg = result.get('error', '') if success: title = f"✅ [{self.platform_name}] 撤单成功 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", f"**订单ID**: {order_id}", ] if details and 'reason' in details: content_parts.append(f"**撤单原因**: {details['reason']}") content = "\n".join(content_parts) color = "green" else: title = f"❌ [{self.platform_name}] 撤单失败 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", f"**订单ID**: {order_id}", f"**错误**: {error_msg}", ] content = "\n".join(content_parts) color = "red" await self.feishu.send_card(title, content, color) async def _send_tp_sl_notification(self, symbol: str, result: Dict[str, Any], details: Optional[Dict[str, Any]] = None): """发送止盈止损设置通知""" success = result.get('success', False) message = result.get('message', '') if success: title = f"✅ [{self.platform_name}] 止盈止损设置成功 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", ] if details: if 'stop_loss' in details and details['stop_loss']: content_parts.append(f"**止损**: ${details['stop_loss']:,.2f}") if 'take_profit' in details and details['take_profit']: content_parts.append(f"**止盈**: ${details['take_profit']:,.2f}") if 'move_sl_reason' in details: content_parts.append(f"**移动止损**: {details['move_sl_reason']}") content = "\n".join(content_parts) color = "green" else: title = f"⚠️ [{self.platform_name}] 止盈止损设置失败 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", f"**错误**: {message}", ] content = "\n".join(content_parts) color = "orange" await self.feishu.send_card(title, content, color) async def _send_position_management_notification(self, symbol: str, result: Dict[str, Any], details: Optional[Dict[str, Any]] = None): """发送持仓管理通知""" action = result.get('action', '') reason = result.get('reason', '') title = f"📊 [{self.platform_name}] 持仓管理 - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**交易对**: {symbol}", f"**操作**: {action}", f"**原因**: {reason}", ] if details: if 'pnl_percent' in details: content_parts.append(f"**盈亏**: {details['pnl_percent']:.2f}%") if 'hold_hours' in details: content_parts.append(f"**持仓时长**: {details['hold_hours']:.1f}h") content = "\n".join(content_parts) # 根据操作类型选择颜色 if action == 'TAKE_PROFIT': color = "green" elif action == 'TIME_EXIT': color = "orange" elif action == 'MOVE_SL': color = "blue" else: color = "blue" await self.feishu.send_card(title, content, color) async def _send_generic_notification(self, operation: str, symbol: str, result: Dict[str, Any], details: Optional[Dict[str, Any]] = None): """发送通用通知""" success = result.get('success', False) message = result.get('message', result.get('error', '')) title = f"[{self.platform_name}] {operation} - {symbol}" content_parts = [ f"**平台**: {self.platform_name}", f"**操作**: {operation}", f"**交易对**: {symbol}", f"**状态**: {'成功' if success else '失败'}", ] if message: content_parts.append(f"**信息**: {message}") if details: for key, value in details.items(): content_parts.append(f"**{key}**: {value}") content = "\n".join(content_parts) color = "green" if success else "red" await self.feishu.send_card(title, content, color)