diff --git a/backend/app/config.py b/backend/app/config.py index c7b16ba..c675800 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -117,6 +117,7 @@ class Settings(BaseSettings): paper_trading_margin_per_order: float = 1000 # 每单保证金 (USDT) paper_trading_max_orders: int = 10 # 最大持仓+挂单总数 paper_trading_auto_close_opposite: bool = False # 是否自动平掉反向持仓(智能策略) + paper_trading_breakeven_threshold: float = 1 # 保本止损触发阈值(盈利百分比),0表示禁用 # 废弃的配置(保留兼容性) paper_trading_position_a: float = 1000 # A级信号仓位 (USDT) paper_trading_position_b: float = 500 # B级信号仓位 (USDT) diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index a97565f..0c77d0c 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -65,6 +65,8 @@ class CryptoAgent: event_type = result.get('event_type', 'order_closed') if event_type == 'order_filled': asyncio.run_coroutine_threadsafe(self._notify_order_filled(result), self._event_loop) + elif event_type == 'breakeven_triggered': + asyncio.run_coroutine_threadsafe(self._notify_breakeven_triggered(result), self._event_loop) else: asyncio.run_coroutine_threadsafe(self._notify_order_closed(result), self._event_loop) else: @@ -106,6 +108,24 @@ class CryptoAgent: await self.telegram.send_message(message) logger.info(f"已发送挂单撤销通知: {result.get('order_id')}") + async def _notify_breakeven_triggered(self, result: Dict[str, Any]): + """发送保本止损触发通知""" + side_text = "做多" if result.get('side') == 'long' else "做空" + + message = f"""🛡️ 保本止损已启动 + +交易对: {result.get('symbol')} +方向: {side_text} +开仓价: ${result.get('filled_price', 0):,.2f} +当前盈利: {result.get('current_pnl_percent', 0):.2f}% +止损已移至: ${result.get('new_stop_loss', 0):,.2f} + +✅ 本单已锁定保本,不会亏损""" + + await self.feishu.send_text(message) + await self.telegram.send_message(message) + logger.info(f"已发送保本止损通知: {result.get('order_id')}") + async def _notify_order_closed(self, result: Dict[str, Any]): """发送订单平仓通知""" status = result.get('status', '') diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index e42862e..5e34272 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -25,6 +25,7 @@ class PaperTradingService: 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.breakeven_threshold = self.settings.paper_trading_breakeven_threshold # 保本止损触发阈值 # 确保表已创建 self._ensure_table_exists() @@ -32,7 +33,7 @@ class PaperTradingService: # 加载活跃订单到内存 self._load_active_orders() - logger.info(f"模拟交易服务初始化完成(自动平反向持仓: {'启用' if self.auto_close_opposite else '禁用'})") + logger.info(f"模拟交易服务初始化完成(自动平反向持仓: {'启用' if self.auto_close_opposite else '禁用'},保本止损阈值: {self.breakeven_threshold}%)") def _ensure_table_exists(self): """确保数据表已创建""" @@ -231,8 +232,10 @@ class PaperTradingService: if result: triggered.append(result) else: - # 更新最大回撤和最大盈利 - self._update_order_extremes(order, current_price) + # 更新最大回撤和最大盈利,并检查保本止损 + breakeven_result = self._update_order_extremes(order, current_price) + if breakeven_result: + triggered.append(breakeven_result) return triggered @@ -413,8 +416,8 @@ class PaperTradingService: finally: db.close() - def _update_order_extremes(self, order: PaperOrder, current_price: float): - """更新订单的最大回撤和最大盈利""" + def _update_order_extremes(self, order: PaperOrder, current_price: float) -> Optional[Dict[str, Any]]: + """更新订单的最大回撤和最大盈利,并检查是否需要移动止损到保本""" if order.side == OrderSide.LONG: current_pnl_percent = ((current_price - order.filled_price) / order.filled_price) * 100 else: @@ -422,13 +425,34 @@ class PaperTradingService: # 检查是否需要更新极值 needs_update = False + breakeven_triggered = False + if current_pnl_percent > order.max_profit: order.max_profit = current_pnl_percent needs_update = True + if current_pnl_percent < order.max_drawdown: order.max_drawdown = current_pnl_percent needs_update = True + # 保本止损逻辑:当盈利达到阈值时,将止损移动到开仓价 + if self.breakeven_threshold > 0 and current_pnl_percent >= self.breakeven_threshold: + # 检查止损是否还没有移动到保本位 + if order.side == OrderSide.LONG: + # 做多:止损应该低于开仓价,如果止损还在开仓价下方,则移动到开仓价 + if order.stop_loss < order.filled_price: + order.stop_loss = order.filled_price + needs_update = True + breakeven_triggered = True + logger.info(f"保本止损触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {self.breakeven_threshold}% | 止损移至开仓价 ${order.filled_price:,.2f}") + else: + # 做空:止损应该高于开仓价,如果止损还在开仓价上方,则移动到开仓价 + if order.stop_loss > order.filled_price: + order.stop_loss = order.filled_price + needs_update = True + breakeven_triggered = True + logger.info(f"保本止损触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {self.breakeven_threshold}% | 止损移至开仓价 ${order.filled_price:,.2f}") + # 如果有更新,持久化到数据库 if needs_update: db = db_service.get_session() @@ -437,6 +461,7 @@ class PaperTradingService: if db_order: db_order.max_profit = order.max_profit db_order.max_drawdown = order.max_drawdown + db_order.stop_loss = order.stop_loss db.commit() except Exception as e: logger.error(f"更新订单极值失败: {e}") @@ -444,6 +469,20 @@ class PaperTradingService: finally: db.close() + # 如果触发了保本止损,返回通知信息 + if breakeven_triggered: + return { + 'event_type': 'breakeven_triggered', + 'order_id': order.order_id, + 'symbol': order.symbol, + 'side': order.side.value, + 'filled_price': order.filled_price, + 'new_stop_loss': order.stop_loss, + 'current_pnl_percent': current_pnl_percent + } + + return None + def close_order_manual(self, order_id: str, exit_price: float) -> Optional[Dict[str, Any]]: """手动平仓或取消挂单""" if order_id not in self.active_orders: