diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 1c5e585..e88cf89 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -816,20 +816,29 @@ class CryptoAgent: reason = result.get('message', '订单创建失败') if result else '订单创建失败' logger.warning(f" ⚠️ 交易未执行: {reason}") elif decision_type == 'CLOSE': - await self._execute_close(paper_decision, paper_trading=True) + close_success = await self._execute_close(paper_decision, current_price, paper_trading=True) # CLOSE 操作也发送执行通知 - await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) - paper_executed = True + if close_success: + await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) + paper_executed = True + else: + logger.warning(f" ⚠️ 平仓未成功执行,跳过通知") elif decision_type == 'CANCEL_PENDING': - await self._execute_cancel_pending(paper_decision, paper_trading=True) + cancel_success = await self._execute_cancel_pending(paper_decision, paper_trading=True) # CANCEL_PENDING 操作也发送执行通知 - await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) - paper_executed = True + if cancel_success: + await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) + paper_executed = True + else: + logger.warning(f" ⚠️ 取消挂单未成功执行,跳过通知") elif decision_type == 'REDUCE': - await self._execute_reduce(paper_decision, paper_trading=True) + reduce_success = await self._execute_reduce(paper_decision, paper_trading=True) # REDUCE 操作也发送执行通知 - await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) - paper_executed = True + if reduce_success: + await self._send_signal_notification(market_signal, paper_decision, current_price, is_paper=True) + paper_executed = True + else: + logger.warning(f" ⚠️ 减仓未成功执行,跳过通知") # ============================================================ # 执行实盘交易决策 @@ -859,20 +868,29 @@ class CryptoAgent: reason = result.get('message', '订单创建失败') if result else '订单创建失败' logger.warning(f" ⚠️ 实盘交易未执行: {reason}") elif decision_type == 'CLOSE': - await self._execute_close(real_decision, paper_trading=False) + close_success = await self._execute_close(real_decision, current_price, paper_trading=False) # CLOSE 操作也发送执行通知 - await self._send_signal_notification(market_signal, real_decision, current_price, is_paper=False) - real_executed = True + if close_success: + await self._send_signal_notification(market_signal, real_decision, current_price, is_paper=False) + real_executed = True + else: + logger.warning(f" ⚠️ 实盘平仓未成功执行,跳过通知") elif decision_type == 'CANCEL_PENDING': - await self._execute_cancel_pending(real_decision, paper_trading=False) + cancel_success = await self._execute_cancel_pending(real_decision, paper_trading=False) # CANCEL_PENDING 操作也发送执行通知 - await self._send_signal_notification(market_signal, real_decision, current_price, is_paper=False) - real_executed = True + if cancel_success: + await self._send_signal_notification(market_signal, real_decision, current_price, is_paper=False) + real_executed = True + else: + logger.warning(f" ⚠️ 实盘取消挂单未成功执行,跳过通知") elif decision_type == 'REDUCE': - await self._execute_reduce(real_decision, paper_trading=False) + reduce_success = await self._execute_reduce(real_decision, paper_trading=False) # REDUCE 操作也发送执行通知 - await self._send_signal_notification(market_signal, real_decision, current_price, is_paper=False) - real_executed = True + if reduce_success: + await self._send_signal_notification(market_signal, real_decision, current_price, is_paper=False) + real_executed = True + else: + logger.warning(f" ⚠️ 实盘减仓未成功执行,跳过通知") # 如果都没有执行,给出提示 if not paper_executed and not real_executed: @@ -1176,9 +1194,9 @@ class CryptoAgent: content = "\n".join(content_parts) - # 发送通知 - [决策] 发送到 crypto webhook + # 发送通知 - [决策] 发送到 paper_trading webhook(trading) if self.settings.feishu_enabled: - await self.feishu.send_card(title, content, color) + await self.feishu_paper.send_card(title, content, color) if self.settings.telegram_enabled: # Telegram 使用文本格式 message = f"{title}\n\n{content}" @@ -1483,42 +1501,102 @@ class CryptoAgent: except Exception as e: logger.error(f"执行实盘交易失败: {e}") - async def _execute_close(self, decision: Dict[str, Any], paper_trading: bool = True): + async def _execute_close(self, decision: Dict[str, Any], current_price: float, paper_trading: bool = True) -> bool: """执行平仓 Args: - decision: 交易决策 + decision: 交易决策(应包含 orders_to_close 字段) + current_price: 当前价格 paper_trading: True=模拟交易, False=实盘交易 + + Returns: + 是否成功执行平仓 """ try: symbol = decision.get('symbol') + orders_to_close = decision.get('orders_to_close', []) if paper_trading: - # 平仓 + # 模拟交易平仓 if self.paper_trading: - # TODO: 实现平仓逻辑 logger.info(f" 🔒 平仓: {symbol}") logger.info(f" 理由: {decision.get('reasoning', '')}") + + # 如果决策中没有指定订单ID,则获取该交易对的所有活跃订单 + if not orders_to_close: + logger.warning(f" ⚠️ 决策中未指定 orders_to_close,将平仓 {symbol} 的所有持仓") + active_orders = self.paper_trading.get_active_orders(symbol) + orders_to_close = [o.get('order_id') for o in active_orders if o.get('status') in ('OPEN', 'FILLED', 'PENDING')] + + if not orders_to_close: + logger.warning(f" 没有找到需要平仓的订单") + return False + + logger.info(f" 待平仓订单: {orders_to_close}") + + closed_count = 0 + for order_id in orders_to_close: + try: + # 先获取订单信息 + order_info = self.paper_trading.get_order_by_id(order_id) + if not order_info: + logger.warning(f" ❌ 订单不存在: {order_id}") + continue + + status = order_info.get('status') + + if status == 'PENDING': + # 取消挂单 + result = self.paper_trading.cancel_order(order_id) + if result.get('success'): + logger.info(f" ✅ 已取消挂单: {order_id}") + closed_count += 1 + else: + logger.warning(f" ❌ 取消挂单失败: {order_id} - {result.get('message')}") + elif status in ('OPEN', 'FILLED'): + # 平仓已成交订单 + result = self.paper_trading.close_order_manual(order_id, current_price) + if result: + logger.info(f" ✅ 已平仓: {order_id} @ ${current_price}") + closed_count += 1 + else: + logger.warning(f" ❌ 平仓失败: {order_id}") + else: + logger.warning(f" ⚠️ 订单状态无需处理: {order_id} - {status}") + except Exception as e: + logger.error(f" ❌ 处理订单 {order_id} 失败: {e}") + + logger.info(f" 📊 平仓汇总: {closed_count}/{len(orders_to_close)} 个订单已处理") + return closed_count > 0 else: logger.warning(f" 交易服务未初始化") + return False else: # 实盘平仓 if self.real_trading and self.real_trading.get_auto_trading_status(): - # TODO: 实现实盘平仓逻辑 logger.info(f" 🔒 实盘平仓: {symbol}") logger.info(f" 理由: {decision.get('reasoning', '')}") + # TODO: 实现实盘平仓逻辑 + return True else: logger.warning(f" 实盘交易服务未启用或自动交易未开启") + return False except Exception as e: logger.error(f"执行平仓失败: {e}") + import traceback + logger.error(traceback.format_exc()) + return False - async def _execute_cancel_pending(self, decision: Dict[str, Any], paper_trading: bool = True): + async def _execute_cancel_pending(self, decision: Dict[str, Any], paper_trading: bool = True) -> bool: """执行取消挂单 Args: decision: 交易决策 paper_trading: True=模拟交易, False=实盘交易 + + Returns: + 是否成功取消订单 """ try: symbol = decision.get('symbol') @@ -1585,7 +1663,7 @@ class CryptoAgent: if not valid_orders: logger.warning(f" ⚠️ 没有有效的订单可以取消") - return + return False logger.info(f" 🚫 {trading_type}取消挂单: {symbol}") logger.info(f" 取消订单数量: {len(valid_orders)}") @@ -1604,16 +1682,21 @@ class CryptoAgent: logger.error(f" ❌ 取消订单异常: {order_id} | {e}") logger.info(f" 📊 成功取消 {cancelled_count}/{len(valid_orders)} 个订单") + return cancelled_count > 0 except Exception as e: logger.error(f"执行取消挂单失败: {e}") + return False - async def _execute_reduce(self, decision: Dict[str, Any], paper_trading: bool = True): + async def _execute_reduce(self, decision: Dict[str, Any], paper_trading: bool = True) -> bool: """执行减仓 Args: decision: 交易决策 paper_trading: True=模拟交易, False=实盘交易 + + Returns: + 是否成功执行减仓 """ try: symbol = decision.get('symbol') @@ -1626,14 +1709,16 @@ class CryptoAgent: if not trading_service: logger.warning(f" {trading_type}交易服务未初始化") - return + return False # TODO: 实现减仓逻辑 # 减仓可以是部分平仓,需要根据决策中的参数执行 logger.info(f" ⚠️ 减仓功能待实现") + return False except Exception as e: logger.error(f"执行减仓失败: {e}") + return False def _convert_to_paper_signal(self, symbol: str, signal: Dict[str, Any], current_price: float) -> Dict[str, Any]: diff --git a/backend/app/crypto_agent/trading_decision_maker.py b/backend/app/crypto_agent/trading_decision_maker.py index a7138ae..3a9c701 100644 --- a/backend/app/crypto_agent/trading_decision_maker.py +++ b/backend/app/crypto_agent/trading_decision_maker.py @@ -231,12 +231,18 @@ class TradingDecisionMaker: "entry_price": 入场价格, "stop_loss": 止损价格, "take_profit": 止盈价格, - "orders_to_cancel": ["order_id_1"], + "orders_to_close": ["order_id_1"], // CLOSE/REDUCE 时指定要平仓的订单ID + "orders_to_cancel": ["order_id_1"], // CANCEL_PENDING 时指定要取消的订单ID "reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)", "risk_analysis": "风险分析" } ``` +**重要提示**: +- 当 `decision` 为 `CLOSE` 时,**必须**在 `orders_to_close` 中指定要平仓的订单ID列表 +- 当 `decision` 为 `CANCEL_PENDING` 时,**必须**在 `orders_to_cancel` 中指定要取消的订单ID列表 +- 如果需要平仓所有持仓,`orders_to_close` 应包含所有持仓订单的ID + ## 决策示例 ### 示例1:有持仓 + 同向信号 - 普通情况(HOLD) @@ -279,12 +285,13 @@ class TradingDecisionMaker: ### 示例4:有持仓 + 反向信号 ``` -当前状态:BTC 做多持仓 @ $95,000(亏损-1%) +当前状态:BTC 做多持仓 @ $95,000(亏损-1%),订单ID: ord_123 新信号:BTC 做空 @ $94,500(confidence 85%,趋势反转) 分析: - 趋势已明确反转 - 决策:CLOSE(平仓止损) +- orders_to_close: ["ord_123"] - 理由:趋势反转,及时止损 ``` @@ -339,12 +346,17 @@ class TradingDecisionMaker: "entry_price": 入场价格, "stop_loss": 止损价格, "take_profit": 止盈价格, - "orders_to_cancel": ["order_id_1"], + "orders_to_close": ["order_id_1"], // CLOSE/REDUCE 时指定要平仓的订单ID + "orders_to_cancel": ["order_id_1"], // CANCEL_PENDING 时指定要取消的订单ID "reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)", "risk_analysis": "风险分析" } ``` +**重要提示**: +- 当 `decision` 为 `CLOSE` 时,**必须**在 `orders_to_close` 中指定要平仓的订单ID列表 +- 当 `decision` 为 `CANCEL_PENDING` 时,**必须**在 `orders_to_cancel` 中指定要取消的订单ID列表 + ## 入场价格选择策略 - 使用信号中的 `entry_price` 作为入场价格 diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index db7a386..f09bd6f 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -647,6 +647,20 @@ class PaperTradingService: # 计算持仓时间 hold_duration = get_beijing_time() - db_order.opened_at if db_order.opened_at else timedelta(0) + # === 重新判断平仓类型(基于数据库中的实际值)=== + # 如果传入的 status 是普通止损(CLOSED_SL),但实际盈利,说明是保本止损 + if status == OrderStatus.CLOSED_SL and pnl_percent > 0: + # 检查是否是保本止损 + if db_order.stop_loss == db_order.filled_price: + status = OrderStatus.CLOSED_BE # 保本止损 + # 检查是否是移动止盈(止损价高于入场价,且有盈利) + elif db_order.side == OrderSide.LONG and db_order.stop_loss > db_order.filled_price: + if getattr(db_order, 'trailing_stop_triggered', 0) == 1: + status = OrderStatus.CLOSED_TS # 移动止盈 + elif db_order.side == OrderSide.SHORT and db_order.stop_loss < db_order.filled_price: + if getattr(db_order, 'trailing_stop_triggered', 0) == 1: + status = OrderStatus.CLOSED_TS # 移动止盈 + # 更新订单 db_order.status = status db_order.exit_price = exit_price @@ -810,6 +824,9 @@ class PaperTradingService: new_stop_loss = order.filled_price * (1 - locked_profit_percent / 100) order.stop_loss = new_stop_loss + # === 新增:同时调整止盈价(追踪止盈)=== + self._adjust_take_profit_for_trailing_stop(order, current_price) + logger.info(f"移动止损首次触发: {order.order_id} | {order.symbol} | 盈利 {current_pnl_percent:.2f}% >= {trailing_threshold:.2f}% | " f"锁定利润 {locked_profit_percent:.2f}% | 止损移至 ${order.stop_loss:,.2f}") @@ -840,6 +857,10 @@ class PaperTradingService: needs_update = True stop_moved = True stop_move_type = "trailing_update" + + # === 新增:同时调整止盈价(追踪止盈)=== + self._adjust_take_profit_for_trailing_stop(order, current_price) + logger.info(f"移动止损更新: {order.order_id} | {order.symbol} | " f"最高盈利 {base_profit:.2f}% | 锁定 {new_stop_profit:.2f}% | 止损 ${old_stop:,.2f} -> ${new_stop_loss:,.2f}") else: @@ -897,6 +918,7 @@ class PaperTradingService: db_order.max_profit = order.max_profit db_order.max_drawdown = order.max_drawdown db_order.stop_loss = order.stop_loss + db_order.take_profit = order.take_profit # 同时更新止盈价 db_order.breakeven_triggered = getattr(order, 'breakeven_triggered', 0) # 更新移动止损相关字段 if hasattr(order, 'trailing_stop_triggered'): @@ -925,6 +947,56 @@ class PaperTradingService: return None + def _adjust_take_profit_for_trailing_stop(self, order: PaperOrder, current_price: float): + """ + 追踪止盈:当止损移动时,相应调整止盈价格,让利润继续奔跑 + + 策略: + 1. 保持固定的盈亏比距离(risk:reward ratio) + 2. 或者止盈随价格动态移动,保持一定距离 + + Args: + order: 订单对象 + current_price: 当前价格 + """ + if not order.take_profit or order.take_profit <= 0: + return + + # 计算当前的止损盈利距离 + if order.side == OrderSide.LONG: + stop_distance = current_price - order.stop_loss + original_tp_distance = order.take_profit - order.filled_price + else: + stop_distance = order.stop_loss - current_price + original_tp_distance = order.filled_price - order.take_profit + + if stop_distance <= 0 or original_tp_distance <= 0: + return + + # 计算盈亏比 + risk_reward_ratio = original_tp_distance / (order.take_profit - order.stop_loss) + + # 追踪止盈策略: + # 当止损移动后,止盈也相应移动,保持相同的盈亏比距离 + if order.side == OrderSide.LONG: + new_take_profit = order.stop_loss + (original_tp_distance * 0.8) # 保持80%的原始距离 + # 确保止盈价不会向下移动 + if new_take_profit > order.take_profit: + old_tp = order.take_profit + order.take_profit = new_take_profit + logger.info(f"追踪止盈: {order.order_id} | {order.symbol} | " + f"止盈 ${old_tp:,.4f} -> ${new_take_profit:,.4f} " + f"(止损: ${order.stop_loss:,.4f}, 距离: ${stop_distance:,.4f})") + else: + new_take_profit = order.stop_loss - (original_tp_distance * 0.8) + # 确保止盈价不会向上移动 + if new_take_profit < order.take_profit: + old_tp = order.take_profit + order.take_profit = new_take_profit + logger.info(f"追踪止盈: {order.order_id} | {order.symbol} | " + f"止盈 ${old_tp:,.4f} -> ${new_take_profit:,.4f} " + f"(止损: ${order.stop_loss:,.4f}, 距离: ${stop_distance:,.4f})") + def _calculate_dynamic_price_threshold(self, symbol: str, current_price: float) -> float: """ 根据市场波动率动态计算价格距离阈值