diff --git a/backend/app/config.py b/backend/app/config.py index 5dcfc4c..c7b16ba 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -116,6 +116,7 @@ class Settings(BaseSettings): paper_trading_leverage: int = 10 # 杠杆倍数 paper_trading_margin_per_order: float = 1000 # 每单保证金 (USDT) paper_trading_max_orders: int = 10 # 最大持仓+挂单总数 + paper_trading_auto_close_opposite: bool = False # 是否自动平掉反向持仓(智能策略) # 废弃的配置(保留兼容性) paper_trading_position_a: float = 1000 # A级信号仓位 (USDT) paper_trading_position_b: float = 500 # B级信号仓位 (USDT) diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index bbf4da9..cd0756d 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -24,6 +24,7 @@ class PaperTradingService: self.leverage = self.settings.paper_trading_leverage # 杠杆倍数 self.margin_per_order = self.settings.paper_trading_margin_per_order # 每单保证金 self.max_orders = self.settings.paper_trading_max_orders # 最大订单数 + self.auto_close_opposite = self.settings.paper_trading_auto_close_opposite # 是否自动平掉反向持仓 # 确保表已创建 self._ensure_table_exists() @@ -31,7 +32,7 @@ class PaperTradingService: # 加载活跃订单到内存 self._load_active_orders() - logger.info("模拟交易服务初始化完成") + logger.info(f"模拟交易服务初始化完成(自动平反向持仓: {'启用' if self.auto_close_opposite else '禁用'})") def _ensure_table_exists(self): """确保数据表已创建""" @@ -89,6 +90,15 @@ class PaperTradingService: side = OrderSide.LONG if action == 'buy' else OrderSide.SHORT entry_price = signal.get('entry_price') or signal.get('price', 0) + # === 反向订单处理 === + # 1. 总是取消同一交易对的反向挂单(混合策略) + self._cancel_opposite_pending_orders(symbol, side) + + # 2. 可选:智能平掉反向持仓(需要配置启用) + if self.auto_close_opposite and current_price: + grade = signal.get('signal_grade') or signal.get('grade', 'D') + self._close_opposite_positions(symbol, side, grade, current_price) + # === 限制检查 === # 1. 检查总订单数(持仓+挂单)是否超过最大限制 total_orders = len(self.active_orders) @@ -474,6 +484,100 @@ class PaperTradingService: finally: db.close() + def _cancel_opposite_pending_orders(self, symbol: str, new_side: OrderSide) -> int: + """ + 取消同一交易对的反向挂单 + + Args: + symbol: 交易对 + new_side: 新信号的方向 + + Returns: + 取消的订单数量 + """ + # 找出所有反向挂单 + opposite_side = OrderSide.SHORT if new_side == OrderSide.LONG else OrderSide.LONG + opposite_pending = [ + order for order in self.active_orders.values() + if order.symbol == symbol + and order.side == opposite_side + and order.status == OrderStatus.PENDING + ] + + if not opposite_pending: + return 0 + + cancelled_count = 0 + for order in opposite_pending: + result = self._cancel_pending_order(order) + if result: + cancelled_count += 1 + logger.info(f"自动取消反向挂单: {order.order_id} | {symbol} {opposite_side.value} @ ${order.entry_price:,.2f}") + + if cancelled_count > 0: + logger.info(f"已取消 {cancelled_count} 个反向挂单({symbol} {opposite_side.value}),新信号方向: {new_side.value}") + + return cancelled_count + + def _close_opposite_positions(self, symbol: str, new_side: OrderSide, signal_grade: str, current_price: float) -> int: + """ + 智能平掉反向持仓(可选策略) + + Args: + symbol: 交易对 + new_side: 新信号的方向 + signal_grade: 新信号等级 + current_price: 当前价格 + + Returns: + 平仓的订单数量 + """ + # 找出所有反向持仓 + opposite_side = OrderSide.SHORT if new_side == OrderSide.LONG else OrderSide.LONG + opposite_positions = [ + order for order in self.active_orders.values() + if order.symbol == symbol + and order.side == opposite_side + and order.status == OrderStatus.OPEN + ] + + if not opposite_positions: + return 0 + + closed_count = 0 + for order in opposite_positions: + # 计算当前盈亏 + if order.side == OrderSide.LONG: + pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100 + else: + pnl_percent = ((order.filled_price - current_price) / order.filled_price) * 100 + + # 智能决策:只在特定条件下平仓 + should_close = False + reason = "" + + # 条件1:A级信号 + 反向持仓亏损超过3% + if signal_grade == 'A' and pnl_percent < -3: + should_close = True + reason = f"A级反向信号且当前亏损{pnl_percent:.2f}%,提前止损" + + # 条件2:A级信号 + 反向持仓盈利但接近止损(盈利<1%) + elif signal_grade == 'A' and 0 < pnl_percent < 1: + should_close = True + reason = f"A级反向信号且盈利较小({pnl_percent:.2f}%),落袋为安" + + if should_close: + result = self._close_order(order, OrderStatus.CLOSED_MANUAL, current_price) + if result: + closed_count += 1 + logger.info(f"自动平掉反向持仓: {order.order_id} | {symbol} {opposite_side.value} | 原因: {reason}") + + if closed_count > 0: + logger.info(f"已平掉 {closed_count} 个反向持仓({symbol} {opposite_side.value})") + + return closed_count + + def get_active_orders(self, symbol: Optional[str] = None) -> List[Dict[str, Any]]: """获取活跃订单""" orders = list(self.active_orders.values())