""" 交易执行器基类 为不同平台提供统一的交易执行接口,各平台可根据自身特性实现具体逻辑。 """ 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): """交易执行器基类""" MIN_EFFECTIVE_LEVERAGE_BY_SIGNAL_TYPE = { 'short_term': 4.0, 'medium_term': 2.0, 'long_term': 2.0, } def __init__(self, platform_name: str): self.platform_name = platform_name self.account_id = "default" # 初始化飞书通知服务 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], volatility_data: Optional[Dict[str, float]] = None) -> List[Dict[str, Any]]: """ 持仓管理检查 Args: positions: 持仓列表 current_prices: 当前价格 {symbol: price} volatility_data: 波动率数据 {symbol: atr_pct}(1h ATR / price) 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: 动态移动止损(基于 ATR 波动率) current_sl = pos.get('stop_loss') if not current_sl: continue # 获取波动率:ATR 占价格的百分比(如 0.015 = 1.5%) atr_pct = (volatility_data.get(symbol) if volatility_data else None) or 0.02 # 波动率倍数:低波动(震荡)给更多空间,高波动(趋势)收得更紧 atr_multiplier = max(1.5, 2.0 / atr_pct) # 两级移动止损 # 第一级:盈利 >= 0.6 * ATR% * multiplier → SL 移到入场价 ± 1倍ATR(给足震荡空间) # 第二级:盈利 >= 1.0 * ATR% * multiplier → SL 移到保本 step1_threshold = atr_pct * 100 * atr_multiplier * 0.6 step2_threshold = atr_pct * 100 * atr_multiplier * 1.0 if pnl_pct >= step2_threshold: # 盈利充足,移到保本 if (side == 'buy' and current_sl < entry_price) or \ (side == 'sell' and current_sl > entry_price): actions.append({ 'symbol': symbol, 'action': 'MOVE_SL', 'new_sl': entry_price, 'pnl_pct': pnl_pct, 'reason': f"盈利 {pnl_pct:.1f}% >= {step2_threshold:.1f}%,移动止损到保本", 'priority': 3 }) elif pnl_pct >= step1_threshold: # 盈利初步,移到入场价 ± 1倍ATR(保留震荡空间,不急于保本) if side == 'buy' and current_sl < entry_price: new_sl = entry_price * (1 - atr_pct) if new_sl > current_sl: actions.append({ 'symbol': symbol, 'action': 'MOVE_SL', 'new_sl': new_sl, 'pnl_pct': pnl_pct, 'reason': f"盈利 {pnl_pct:.1f}% >= {step1_threshold:.1f}%,移动止损到入场价-1ATR (${new_sl:.2f})", 'priority': 3 }) elif side == 'sell' and current_sl > entry_price: new_sl = entry_price * (1 + atr_pct) if new_sl < current_sl: actions.append({ 'symbol': symbol, 'action': 'MOVE_SL', 'new_sl': new_sl, 'pnl_pct': pnl_pct, 'reason': f"盈利 {pnl_pct:.1f}% >= {step1_threshold:.1f}%,移动止损到入场价+1ATR (${new_sl:.2f})", '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 def get_min_effective_leverage(self, decision: Dict[str, Any]) -> float: signal_type = decision.get('timeframe') or decision.get('type') or 'medium_term' return float(self.MIN_EFFECTIVE_LEVERAGE_BY_SIGNAL_TYPE.get(signal_type, 2.0)) def validate_effective_leverage(self, decision: Dict[str, Any], margin: float, actual_position_value: float) -> tuple[bool, str, float]: if margin <= 0: return False, "保证金无效", 0.0 effective_leverage = actual_position_value / margin if margin > 0 else 0.0 min_effective_leverage = self.get_min_effective_leverage(decision) if effective_leverage + 1e-9 < min_effective_leverage: signal_type = decision.get('timeframe') or decision.get('type') or 'medium_term' return ( False, f"{signal_type} 实际有效杠杆 {effective_leverage:.2f}x < 最小要求 {min_effective_leverage:.1f}x", effective_leverage, ) return True, "", effective_leverage @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 # ==================== 飞书通知 ==================== def _normalize_notification_context(self, details: Optional[Dict[str, Any]] = None, account_id: str = "default", target_key: str = "") -> Dict[str, str]: resolved_account_id = str(account_id or getattr(self, 'account_id', 'default') or 'default') resolved_target_key = target_key or ( f"{self.platform_name}:{resolved_account_id}" if resolved_account_id and resolved_account_id != "default" else self.platform_name ) return { "account_id": resolved_account_id, "target_key": resolved_target_key, "platform_label": self.platform_name, } def _append_notification_detail(self, content_parts: List[str], label: str, value: Any): if value is None or value == "": return content_parts.append(f"**{label}**: {value}") def _build_notification_header(self, symbol: str, account_id: str, target_key: str) -> List[str]: return [ f"**执行目标**: {target_key}", f"**平台**: {self.platform_name}", f"**账号**: {account_id}", f"**交易对**: {symbol}", ] 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: details = dict(details or {}) account_id = details.get('account_id') or getattr(self, 'account_id', 'default') target_key = details.get('target_key') or ( f"{self.platform_name}:{account_id}" if account_id and account_id != "default" else self.platform_name ) # 根据操作类型选择通知方法 if operation == 'OPEN': await self._send_open_notification(symbol, result, details, account_id, target_key) elif operation == 'CLOSE': await self._send_close_notification(symbol, result, details, account_id, target_key) elif operation == 'CANCEL': await self._send_cancel_notification(symbol, result, details, account_id, target_key) elif operation == 'TP_SL': await self._send_tp_sl_notification(symbol, result, details, account_id, target_key) elif operation == 'POSITION_MANAGEMENT': await self._send_position_management_notification(symbol, result, details, account_id, target_key) else: # 通用通知 await self._send_generic_notification(operation, symbol, result, details, account_id, target_key) 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, account_id: str = "default", target_key: str = ""): """发送开仓通知""" success = result.get('success', False) order_id = result.get('order_id', '') error_msg = result.get('error', result.get('message', '')) if success: # 成功开仓 title = f"✅ [{target_key or self.platform_name}] 开仓成功 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "订单ID", order_id) # 添加详情 if details: self._append_notification_detail(content_parts, "数量", details.get('size')) if details.get('price') is not None: self._append_notification_detail(content_parts, "价格", f"${details['price']:,.2f}") if details.get('margin') is not None: self._append_notification_detail(content_parts, "保证金", f"${details['margin']:,.2f}") if details.get('notional') is not None: self._append_notification_detail(content_parts, "名义仓位", f"${details['notional']:,.2f}") if details.get('leverage') is not None: self._append_notification_detail(content_parts, "杠杆", f"{details['leverage']}x") if details.get('stop_loss') is not None: self._append_notification_detail(content_parts, "止损", f"${details['stop_loss']:,.2f}") if details.get('take_profit') is not None: self._append_notification_detail(content_parts, "止盈", f"${details['take_profit']:,.2f}") self._append_notification_detail(content_parts, "订单类型", details.get('order_type')) content = "\n".join(content_parts) color = "green" else: # 开仓失败 title = f"❌ [{target_key or self.platform_name}] 开仓失败 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "错误", error_msg) if details and 'reason' in details: self._append_notification_detail(content_parts, "原因", 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, account_id: str = "default", target_key: str = ""): """发送平仓通知""" success = result.get('success', False) error_msg = result.get('error', result.get('message', '')) if success: title = f"✅ [{target_key or self.platform_name}] 平仓成功 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) if details: if 'pnl' in details: pnl = details['pnl'] pnl_color = "盈利" if pnl >= 0 else "亏损" self._append_notification_detail(content_parts, pnl_color, f"${pnl:,.2f}") if 'pnl_percent' in details: self._append_notification_detail(content_parts, "收益率", f"{details['pnl_percent']:.2f}%") if 'exit_reason' in details: self._append_notification_detail(content_parts, "平仓原因", details['exit_reason']) content = "\n".join(content_parts) color = "green" else: title = f"❌ [{target_key or self.platform_name}] 平仓失败 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "错误", 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, account_id: str = "default", target_key: str = ""): """发送撤单通知""" success = result.get('success', False) order_id = result.get('order_id', '') error_msg = result.get('error', result.get('message', '')) if success: title = f"✅ [{target_key or self.platform_name}] 撤单成功 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "订单ID", order_id) if details and 'reason' in details: self._append_notification_detail(content_parts, "撤单原因", details['reason']) content = "\n".join(content_parts) color = "green" else: title = f"❌ [{target_key or self.platform_name}] 撤单失败 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "订单ID", order_id) self._append_notification_detail(content_parts, "错误", 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, account_id: str = "default", target_key: str = ""): """发送止盈止损设置通知""" success = result.get('success', False) message = result.get('message', '') if success: title = f"✅ [{target_key or self.platform_name}] 止盈止损设置成功 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) if details: if 'stop_loss' in details and details['stop_loss'] is not None: self._append_notification_detail(content_parts, "止损", f"${details['stop_loss']:,.2f}") if 'take_profit' in details and details['take_profit'] is not None: self._append_notification_detail(content_parts, "止盈", f"${details['take_profit']:,.2f}") if 'move_sl_reason' in details: self._append_notification_detail(content_parts, "移动止损", details['move_sl_reason']) content = "\n".join(content_parts) color = "green" else: title = f"⚠️ [{target_key or self.platform_name}] 止盈止损设置失败 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "错误", 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, account_id: str = "default", target_key: str = ""): """发送持仓管理通知""" action = result.get('action', '') reason = result.get('reason', '') title = f"📊 [{target_key or self.platform_name}] 持仓管理 - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "操作", action) self._append_notification_detail(content_parts, "原因", reason) if details: if 'pnl_percent' in details: self._append_notification_detail(content_parts, "盈亏", f"{details['pnl_percent']:.2f}%") if 'hold_hours' in details: self._append_notification_detail(content_parts, "持仓时长", 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, account_id: str = "default", target_key: str = ""): """发送通用通知""" success = result.get('success', False) message = result.get('message', result.get('error', '')) title = f"[{target_key or self.platform_name}] {operation} - {symbol}" content_parts = self._build_notification_header(symbol, account_id, target_key) self._append_notification_detail(content_parts, "操作", operation) self._append_notification_detail(content_parts, "状态", '成功' if success else '失败') if message: self._append_notification_detail(content_parts, "信息", message) if details: for key, value in details.items(): if key in {'account_id', 'target_key'}: continue self._append_notification_detail(content_parts, key, value) content = "\n".join(content_parts) color = "green" if success else "red" await self.feishu.send_card(title, content, color)