diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index e88cf89..258dc97 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -831,6 +831,14 @@ class CryptoAgent: paper_executed = True else: logger.warning(f" ⚠️ 取消挂单未成功执行,跳过通知") + elif decision_type == 'UPDATE_PENDING': + update_success = await self._execute_update_pending(paper_decision, paper_trading=True) + # UPDATE_PENDING 操作也发送执行通知 + if update_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': reduce_success = await self._execute_reduce(paper_decision, paper_trading=True) # REDUCE 操作也发送执行通知 @@ -883,6 +891,14 @@ class CryptoAgent: real_executed = True else: logger.warning(f" ⚠️ 实盘取消挂单未成功执行,跳过通知") + elif decision_type == 'UPDATE_PENDING': + update_success = await self._execute_update_pending(real_decision, paper_trading=False) + # UPDATE_PENDING 操作也发送执行通知 + if update_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': reduce_success = await self._execute_reduce(real_decision, paper_trading=False) # REDUCE 操作也发送执行通知 @@ -1688,6 +1704,124 @@ class CryptoAgent: logger.error(f"执行取消挂单失败: {e}") return False + async def _execute_update_pending(self, decision: Dict[str, Any], paper_trading: bool = True) -> bool: + """执行更新挂单参数 + + Args: + decision: 交易决策 + paper_trading: True=模拟交易, False=实盘交易 + + Returns: + 是否成功更新订单 + """ + try: + symbol = decision.get('symbol') + decision_action = decision.get('action', '') # buy/sell + orders_to_update = decision.get('orders_to_update', []) + + # 获取新参数 + new_entry_price = decision.get('entry_price') + new_stop_loss = decision.get('stop_loss') + new_take_profit = decision.get('take_profit') + + if not orders_to_update: + logger.info(f" ⚠️ 没有需要更新的订单") + return False + + if not all([new_entry_price, new_stop_loss, new_take_profit]): + logger.warning(f" ⚠️ 更新参数不完整: entry_price={new_entry_price}, stop_loss={new_stop_loss}, take_profit={new_take_profit}") + return False + + trading_service = self.paper_trading if paper_trading else self.real_trading + trading_type = "模拟" if paper_trading else "实盘" + + if not trading_service: + logger.warning(f" {trading_type}交易服务未启用") + return False + + # 安全检查:验证要更新的订单是否属于当前symbol且方向相同 + active_orders = trading_service.get_active_orders() + valid_orders = [] + invalid_orders = [] + wrong_direction_orders = [] + + for order_id in orders_to_update: + # 查找订单 + order = next((o for o in active_orders if o.order_id == order_id), None) + if not order: + logger.warning(f" ⚠️ 订单不存在: {order_id}") + invalid_orders.append(order_id) + continue + + # 检查订单是否已成交(只能更新未成交的挂单) + if order.filled_price and order.filled_price > 0: + logger.warning(f" ⚠️ 订单已成交,无法更新: {order_id}") + invalid_orders.append(order_id) + continue + + # 检查订单是否属于当前symbol + if order.symbol != symbol: + logger.error(f" ❌ 安全拦截:订单 {order_id} 属于 {order.symbol},不是当前分析标的 {symbol}") + invalid_orders.append(order_id) + continue + + # 检查订单方向是否与决策相同 + order_side = order.side.value # LONG/SHORT + # 决策是buy时应该更新LONG(做多),决策是sell时应该更新SHORT(做空) + should_update = False + if decision_action == 'buy' and order_side == 'LONG': + should_update = True + elif decision_action == 'sell' and order_side == 'SHORT': + should_update = True + + if not should_update: + logger.error(f" ❌ 安全拦截:订单 {order_id} 方向为 {order_side},与决策 {decision_action} 方向不一致") + wrong_direction_orders.append(order_id) + invalid_orders.append(order_id) + continue + + valid_orders.append(order) + + if invalid_orders: + logger.error(f" 🚫 拒绝更新 {len(invalid_orders)} 个不符合条件的订单") + + if wrong_direction_orders: + logger.error(f" ⚠️ {len(wrong_direction_orders)} 个方向不一致的订单被拦截") + + if not valid_orders: + logger.warning(f" ⚠️ 没有有效的订单可以更新") + return False + + logger.info(f" 🔄 {trading_type}更新挂单: {symbol}") + logger.info(f" 新参数: 入场=${new_entry_price:,.2f}, 止损=${new_stop_loss:,.2f}, 止盈=${new_take_profit:,.2f}") + + updated_count = 0 + for order in valid_orders: + try: + # 更新订单参数 + result = trading_service.update_order( + order.order_id, + entry_price=new_entry_price, + stop_loss=new_stop_loss, + take_profit=new_take_profit + ) + + if result and result.get('success'): + updated_count += 1 + logger.info(f" ✅ 订单更新成功: {order.order_id}") + logger.info(f" 旧参数: 入场=${order.entry_price:,.2f}, 止损=${order.stop_loss:,.2f}, 止盈=${order.take_profit:,.2f}") + else: + logger.warning(f" ⚠️ 更新订单失败: {order.order_id} | {result.get('message', '')}") + except Exception as e: + logger.error(f" ❌ 更新订单异常: {order.order_id} | {e}") + + logger.info(f" 📊 成功更新 {updated_count}/{len(valid_orders)} 个订单") + return updated_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) -> bool: """执行减仓 diff --git a/backend/app/crypto_agent/trading_decision_maker.py b/backend/app/crypto_agent/trading_decision_maker.py index 44a64a4..439d8af 100644 --- a/backend/app/crypto_agent/trading_decision_maker.py +++ b/backend/app/crypto_agent/trading_decision_maker.py @@ -129,34 +129,45 @@ class TradingDecisionMaker: 3. **HOLD(观望)** - 如果反转信号不强 #### 情况C:无持仓 + 有同向挂单 -**优先选择:HOLD(等待挂单成交)** +**优先选择:UPDATE_PENDING(更新挂单参数)** -**❌ 严禁取消挂单追涨**: -- 即使有新信号,也不要取消挂单去市价追涨 -- 挂单价格通常更优,耐心等待 -- 价格快速移动时更应该观望,而不是追涨 +**核心原则:适应新信号,不要等老信号** +- 新信号代表最新的市场分析 +- 应该用新信号的参数更新挂单 +- 不要死守过时的挂单价格 -**只有以下极端情况才考虑调整**: -- ✅ 趋势明确反转(如从涨转跌) -- ✅ 新价格更优且距离 >= 3%(仍用 limit,不用 market) -- ✅ 挂单已无意义(如突破后回踩确认) +**更新挂单的条件**: +- ✅ 新信号与挂单**方向相同**(都是 buy 或都是 sell) +- ✅ 新信号提供了**更新的入场价、止损、止盈** +- ✅ 新信号置信度 >= 60(C级以上) + +**更新内容**(根据新信号调整): +- 入场价格:使用新信号的 entry_price +- 止损价格:使用新信号的 stop_loss +- 止盈价格:使用新信号的 take_profit +- 入场方式:保持 limit(不要改成 market) **示例**: ``` -当前:BTC 做多挂单 @ $94,000(未成交) -新信号:BTC 做多 @ $96,500(B级,75%置信度) +当前:BTC 做多挂单 @ $94,000,止损 $93,500,止盈 $95,500 +新信号:BTC 做多 @ $93,800(B级,75%置信度),止损 $93,200,止盈 $95,200 分析: -- 新价格更高,但挂单价格更优 -- 价格正在快速移动($94,000 → $96,500) -- 决策:HOLD(继续等待挂单成交) -- 理由:挂单价格更优,不要追涨,耐心等待 +- 新信号更保守,入场价更低 +- 应该更新挂单参数以适应新的市场分析 +- 决策:UPDATE_PENDING(更新挂单) +- 理由:根据新信号更新入场价 $93,800,止损 $93,200,止盈 $95,200 ``` +**⚠️ 例外情况(保持 HOLD)**: +- 价格正在快速加速移动(5m 连续大阳/阴线) +- 新信号置信度 < 60(D级信号,质量太低) +- 新信号入场价距离当前价格 >= 2%(追涨杀跌风险) +- 新信号是 market 入场(不要改成 market 去追) + **❌ 严禁**: - 取消挂单后市价追涨/杀跌 -- 价格距离 < 3% 时调整挂单 -- 价格快速移动时任何操作 +- 价格快速移动时的任何更新操作 #### 情况D:无持仓 + 有反向挂单 **优先选择**: @@ -237,7 +248,7 @@ class TradingDecisionMaker: ## 输出格式 ```json { - "decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/HOLD", + "decision": "OPEN/CLOSE/ADD/REDUCE/CANCEL_PENDING/UPDATE_PENDING/HOLD", "action": "buy/sell", "quantity": 保证金金额(USDT), "entry_price": 入场价格, @@ -245,6 +256,7 @@ class TradingDecisionMaker: "take_profit": 止盈价格, "orders_to_close": ["order_id_1"], // CLOSE/REDUCE 时指定要平仓的订单ID "orders_to_cancel": ["order_id_1"], // CANCEL_PENDING 时指定要取消的订单ID + "orders_to_update": ["order_id_1"], // UPDATE_PENDING 时指定要更新的订单ID "reasoning": "决策理由(必须说明当前持仓/挂单状态以及为什么选择这个操作)", "risk_analysis": "风险分析" } @@ -253,6 +265,7 @@ class TradingDecisionMaker: **重要提示**: - 当 `decision` 为 `CLOSE` 时,**必须**在 `orders_to_close` 中指定要平仓的订单ID列表 - 当 `decision` 为 `CANCEL_PENDING` 时,**必须**在 `orders_to_cancel` 中指定要取消的订单ID列表 +- 当 `decision` 为 `UPDATE_PENDING` 时,**必须**在 `orders_to_update` 中指定要更新的订单ID列表,并提供新的 entry_price、stop_loss、take_profit - 如果需要平仓所有持仓,`orders_to_close` 应包含所有持仓订单的ID ## 决策示例 @@ -307,29 +320,31 @@ class TradingDecisionMaker: - 理由:趋势反转,及时止损 ``` -### 示例5:有挂单 + 同向信号 - 普通情况(HOLD) +### 示例5:有挂单 + 同向信号 - 更新挂单参数(UPDATE_PENDING) ``` -当前状态:BTC 做多挂单 @ $94,500(未成交) -新信号:BTC 做多 @ $96,000(confidence 70%,B级) +当前状态:BTC 做多挂单 @ $94,500(未成交),订单ID: ord_456 +新信号:BTC 做多 @ $93,800(confidence 75%,B级,回调至支撑位) 分析: -- 挂单价格更优($94,500 < $96,000) -- 信号不是A级 -- 价格正在快速移动 -- 决策:HOLD(等待挂单成交) -- 理由:挂单价格更优,不要追涨 +- 新信号提供了更优的入场价格($93,800 < $94,500) +- 市场出现回调机会,应该调整挂单价格 +- 决策:UPDATE_PENDING(更新挂单参数) +- orders_to_update: ["ord_456"] +- 新参数:entry_price=$93,800, stop_loss=$93,200, take_profit=$95,200 +- 理由:根据新信号更新挂单参数,适应新的市场分析 ``` -### 示例6:有挂单 + 同向信号 - 不要追涨 +### 示例6:有挂单 + 同向信号 - 价格加速时保持(HOLD) ``` 当前状态:BTC 做多挂单 @ $94,000(未成交) 新信号:BTC 做多 @ $97,000(confidence 85%,B级,突破) 分析: -- 新价格更高,但挂单价格更优 -- 价格快速移动($94,000 → $97,000) -- 决策:HOLD(等待挂单成交或等待回调) -- 理由:不要追涨,挂单价格更优 +- 价格快速移动($94,000 → $97,000,涨幅3.2%) +- 5m 连续2根大阳线,正在加速 +- 新信号入场价距离当前价格 >= 2% +- 决策:HOLD(不要追涨) +- 理由:价格正在加速,禁止追涨杀跌,等待回调 ``` ### 示例7:完全无持仓无挂单 @@ -965,7 +980,7 @@ class TradingDecisionMaker: market_signal: Dict[str, Any] = None) -> Dict[str, Any]: """验证决策安全性""" # 检查杠杆限制 - if decision.get('decision') in ['OPEN', 'ADD']: + if decision.get('decision') in ['OPEN', 'ADD', 'UPDATE_PENDING']: balance = float(account.get('current_balance', 0)) total_position_value = float(account.get('total_position_value', 0)) max_leverage = 20 @@ -984,6 +999,7 @@ class TradingDecisionMaker: ) # 盈亏比检查:所有交易必须满足盈亏比 >= 1:1.5 + # UPDATE_PENDING 也需要验证盈亏比 action = decision.get('action', '') entry_price = decision.get('entry_price') stop_loss = decision.get('stop_loss') diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index 13a579c..5c4d55a 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -1264,6 +1264,103 @@ class PaperTradingService: 'message': '取消挂单失败' } + def update_order(self, order_id: str, entry_price: float = None, + stop_loss: float = None, take_profit: float = None) -> Dict[str, Any]: + """ + 更新挂单参数 + + Args: + order_id: 订单ID + entry_price: 新的入场价格 + stop_loss: 新的止损价格 + take_profit: 新的止盈价格 + + Returns: + 更新结果字典 + """ + if order_id not in self.active_orders: + return { + 'success': False, + 'message': f'订单不存在: {order_id}' + } + + order = self.active_orders[order_id] + + # 只能更新挂单状态的订单 + if order.status != OrderStatus.PENDING: + return { + 'success': False, + 'message': f'只能更新挂单状态的订单,当前状态: {order.status.value}' + } + + # 至少需要一个参数 + if entry_price is None and stop_loss is None and take_profit is None: + return { + 'success': False, + 'message': '至少需要提供一个更新参数(entry_price, stop_loss, 或 take_profit)' + } + + result = self._update_pending_order(order, entry_price, stop_loss, take_profit) + if result: + return { + 'success': True, + 'order_id': order_id, + 'message': '挂单已更新' + } + else: + return { + 'success': False, + 'message': '更新挂单失败' + } + + def _update_pending_order(self, order: PaperOrder, entry_price: float = None, + stop_loss: float = None, take_profit: float = None) -> Dict[str, Any]: + """更新挂单参数""" + db = db_service.get_session() + try: + # 记录旧值用于日志 + old_entry = order.entry_price + old_sl = order.stop_loss + old_tp = order.take_profit + + # 更新参数 + if entry_price is not None: + order.entry_price = entry_price + if stop_loss is not None: + order.stop_loss = stop_loss + if take_profit is not None: + order.take_profit = take_profit + + db.merge(order) + db.commit() + + # 更新内存缓存 + self.active_orders[order.order_id] = order + + logger.info(f"挂单已更新: {order.order_id} | {order.symbol}") + logger.info(f" 入场: ${old_entry:,.2f} → ${order.entry_price:,.2f}") + if stop_loss is not None: + logger.info(f" 止损: ${old_sl:,.2f} → ${order.stop_loss:,.2f}") + if take_profit is not None: + logger.info(f" 止盈: ${old_tp:,.2f} → ${order.take_profit:,.2f}") + + return { + 'order_id': order.order_id, + 'symbol': order.symbol, + 'side': order.side.value, + 'status': 'updated', + 'entry_price': order.entry_price, + 'stop_loss': order.stop_loss, + 'take_profit': order.take_profit, + 'message': '挂单已更新' + } + except Exception as e: + logger.error(f"更新挂单失败: {e}") + db.rollback() + return None + finally: + db.close() + def _cancel_pending_order(self, order: PaperOrder) -> Dict[str, Any]: """取消挂单""" db = db_service.get_session()