diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 7adab17..a7e9ba3 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -927,11 +927,11 @@ class CryptoAgent: await self._send_signal_notification(market_signal, decision, current_price) else: logger.error(f" ❌ 订单对象无效: 缺少order_id属性") - # 订单创建失败,理由已在日志中记录,无需单独通知 + await self._notify_execution_failure(market_signal, decision, "订单对象无效(缺少order_id)", prefix="[模拟盘]") else: - # 订单创建失败,理由已在日志中记录,无需单独通知 reason = result.get('message', '订单创建失败') if result else '订单创建失败' logger.warning(f" ⚠️ 交易未执行: {reason}") + await self._notify_execution_failure(market_signal, decision, reason, prefix="[模拟盘]") elif decision_type == 'CLOSE': close_success = await self._execute_close(decision, current_price) # CLOSE 操作也发送执行通知 @@ -1271,8 +1271,10 @@ class CryptoAgent: async def _send_signal_notification(self, market_signal: Dict[str, Any], decision: Dict[str, Any], current_price: float, - prefix: str = ""): - """发送交易执行通知(第三阶段)""" + prefix: str = "", hl_order_status: str = None): + """发送交易执行通知(第三阶段) + hl_order_status: Hyperliquid 限价单实际状态 'resting'|'filled'|None + """ try: decision_type = decision.get('decision', 'HOLD') @@ -1292,7 +1294,11 @@ class CryptoAgent: quantity = decision.get('quantity', 'N/A') stop_loss = decision.get('stop_loss', '') take_profit = decision.get('take_profit', '') - confidence = decision.get('confidence', 0) + # confidence 优先从决策本身读取,否则从市场信号的最佳信号读取 + confidence = decision.get('confidence') + if confidence is None: + _best = self._get_best_signal_from_market(market_signal) + confidence = _best.get('confidence', 0) if _best else 0 # 决策类型映射 decision_map = { @@ -1320,17 +1326,31 @@ class CryptoAgent: # 从市场信号中获取入场方式(需要在构建标题之前) best_signal = self._get_best_signal_from_market(market_signal) entry_type = best_signal.get('entry_type', 'market') if best_signal else 'market' - entry_type_text = '现价单' if entry_type == 'market' else '挂单' - entry_type_icon = '⚡' if entry_type == 'market' else '⏳' + + # 对 Hyperliquid 限价单:用实际订单状态决定显示 + # resting=真的在挂单中, filled=已立即成交, None=非HL或市价单 + if hl_order_status == 'resting': + entry_type_text = '挂单' + entry_type_icon = '⏳' + elif hl_order_status == 'filled': + entry_type_text = '现价成交' + entry_type_icon = '⚡' + else: + entry_type_text = '现价单' if entry_type == 'market' else '挂单' + entry_type_icon = '⚡' if entry_type == 'market' else '⏳' # 仓位图标 position_map = {'heavy': '🔥 重仓', 'medium': '📊 中仓', 'light': '🌱 轻仓'} position_display = position_map.get(position_size, '🌱 轻仓') - # 构建卡片标题和颜色 - 考虑入场方式,添加 [执行] 前缀区分 - # 挂单时标题显示"挂单",现价单时显示"开仓"/"平仓"等 + # 构建卡片标题:Hyperliquid 限价单区分实际状态 if decision_type == 'OPEN': - decision_title = '挂单' if entry_type == 'limit' else '开仓' + if hl_order_status == 'resting': + decision_title = '挂单中' + elif hl_order_status == 'filled': + decision_title = '开仓(立即成交)' + else: + decision_title = '挂单' if entry_type == 'limit' else '开仓' title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "green" elif decision_type == 'CLOSE': @@ -1338,7 +1358,12 @@ class CryptoAgent: title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "orange" elif decision_type == 'ADD': - decision_title = '挂单' if entry_type == 'limit' else '加仓' + if hl_order_status == 'resting': + decision_title = '加仓挂单中' + elif hl_order_status == 'filled': + decision_title = '加仓(立即成交)' + else: + decision_title = '挂单' if entry_type == 'limit' else '加仓' title = f"{title_prefix}[执行] {account_type} {symbol} {decision_title}" color = "green" elif decision_type == 'REDUCE': @@ -1403,6 +1428,37 @@ class CryptoAgent: except Exception as e: logger.warning(f"发送交易执行通知失败: {e}") + async def _notify_execution_failure(self, market_signal: Dict[str, Any], + decision: Dict[str, Any], reason: str, + prefix: str = ""): + """发送执行失败通知(决策给出了 OPEN/ADD 但实际未能开仓)""" + try: + symbol = market_signal.get('symbol', '') + decision_type = decision.get('decision', 'OPEN') + action = decision.get('action', '') + title_prefix = f"{prefix} " if prefix else "" + + action_text = "做多" if 'buy' in action.lower() else ("做空" if 'sell' in action.lower() else action) + decision_text = {'OPEN': '开仓', 'ADD': '加仓'}.get(decision_type, decision_type) + + title = f"{title_prefix}⚠️ {symbol} {decision_text}未执行" + content = "\n".join([ + f"🔴 **决策**: {decision_text}({action_text})", + f"❌ **未执行原因**: {reason}", + f"🕐 **时间**: {datetime.now().strftime('%H:%M:%S')}", + ]) + + if self.settings.feishu_enabled: + await self.feishu_paper.send_card(title, content, "red") + if self.settings.telegram_enabled: + await self.telegram.send_message(f"{title}\n\n{content}") + if self.settings.dingtalk_enabled: + await self.dingtalk.send_action_card(title, content) + + logger.info(f" 📤 已发送执行失败通知: {reason}") + except Exception as e: + logger.warning(f"发送执行失败通知失败: {e}") + async def _execute_paper_trade(self, decision: Dict[str, Any], market_signal: Dict[str, Any], current_price: float): """执行模拟交易""" try: @@ -1871,7 +1927,11 @@ class CryptoAgent: if result.get('success'): logger.info(f" ✅ Hyperliquid 交易成功") - await self._send_signal_notification(market_signal, decision, current_price, prefix="[Hyperliquid]") + # 根据实际订单状态决定通知文案:resting=真挂单,filled=已成交 + order_status = result.get('verified_order_status', 'filled') + await self._send_signal_notification(market_signal, decision, current_price, + prefix="[Hyperliquid]", + hl_order_status=order_status) # 止盈止损设置失败时单独告警 if result.get('tp_sl_warning'): await self._notify_hyperliquid_error(symbol, "设置止盈止损", result['tp_sl_warning']) @@ -1948,27 +2008,48 @@ class CryptoAgent: price=entry_price ) - # 如果开仓成功,设置止盈止损 + # 如果开仓成功,处理止盈止损 + 验证订单实际状态 if result.get('success'): + order_status = result.get('order_status', 'filled') # market单默认filled + + # 限价单:如果立即成交(filled),验证持仓是否存在 + if entry_type == 'limit' and order_status == 'filled': + position = self.hyperliquid.get_position_for_symbol(symbol) + if position: + logger.info(f" ✅ 限价单立即成交,持仓确认: {symbol} size={position['size']}") + else: + logger.warning(f" ⚠️ 限价单显示 filled 但未查到持仓,可能已被平仓或数据延迟") + + # 限价单:如果仍在挂单中(resting),验证订单是否在挂单列表 + elif entry_type == 'limit' and order_status == 'resting': + order_id = result.get('order_id') + open_orders = self.hyperliquid.get_open_orders(symbol) + if any(o.get('order_id') == order_id for o in open_orders): + logger.info(f" ✅ 限价单已挂出并确认可见: oid={order_id}") + else: + logger.warning(f" ⚠️ 限价单 oid={order_id} 未在挂单列表中查到(可能已成交或延迟)") + + # 将实际状态写回 result,供通知层使用 + result['verified_order_status'] = order_status + tp_price = decision.get('take_profit') sl_price = decision.get('stop_loss') if tp_price or sl_price: - # 判断方向 - is_long = (side == 'buy') + # 只有已成交的订单才设置止盈止损(挂单中的不设,等成交后再设) + if order_status != 'resting': + is_long = (side == 'buy') + tp_sl_result = self.hyperliquid.set_tp_sl( + symbol=symbol, + is_long=is_long, + size=size, + tp_price=tp_price, + sl_price=sl_price + ) - # 设置止盈止损 - tp_sl_result = self.hyperliquid.set_tp_sl( - symbol=symbol, - is_long=is_long, - size=size, - tp_price=tp_price, - sl_price=sl_price - ) - - if not tp_sl_result.get('success'): - logger.warning(f" ⚠️ 设置止盈止损失败: {tp_sl_result.get('error')}") - result['tp_sl_warning'] = tp_sl_result.get('error', '设置止盈止损失败') + if not tp_sl_result.get('success'): + logger.warning(f" ⚠️ 设置止盈止损失败: {tp_sl_result.get('error')}") + result['tp_sl_warning'] = tp_sl_result.get('error', '设置止盈止损失败') return result diff --git a/backend/app/services/hyperliquid_trading_service.py b/backend/app/services/hyperliquid_trading_service.py index 14b5161..30bc8bc 100644 --- a/backend/app/services/hyperliquid_trading_service.py +++ b/backend/app/services/hyperliquid_trading_service.py @@ -181,6 +181,11 @@ class HyperliquidTradingService: logger.error(f"❌ Hyperliquid 市价单错误: {error_statuses}") return {"success": False, "error": str(error_statuses), "result": result} + # statuses 为空 → 静默拒绝 + if not statuses: + logger.error(f"❌ Hyperliquid 市价单:返回 statuses 为空,订单未成功提交") + return {"success": False, "error": "Empty order statuses (order not placed)", "result": result} + side = "买入" if is_buy else "卖出" order_type = "平仓" if reduce_only else "开仓" logger.info(f"✅ Hyperliquid 市价单: {order_type} {side} {symbol} {size}") @@ -224,16 +229,42 @@ class HyperliquidTradingService: # 检查单个订单状态 statuses = result.get("response", {}).get("data", {}).get("statuses", []) + + # 有错误 → 失败 error_statuses = [s for s in statuses if "error" in s] if error_statuses: logger.error(f"❌ Hyperliquid 限价单错误: {error_statuses}") return {"success": False, "error": str(error_statuses), "result": result} - side = "买入" if is_buy else "卖出" - logger.info(f"✅ Hyperliquid 限价单: {side} {symbol} {size} @ ${price}") + # statuses 为空 → Hyperliquid 静默拒绝,视为失败 + if not statuses: + logger.error(f"❌ Hyperliquid 限价单:返回 statuses 为空,订单未成功提交") + return {"success": False, "error": "Empty order statuses (order not placed)", "result": result} + # 判断订单实际状态:resting(挂单中)还是 filled(立即成交) + first_status = statuses[0] + if "resting" in first_status: + order_id = first_status["resting"].get("oid") + order_status = "resting" + side = "买入" if is_buy else "卖出" + logger.info(f"✅ Hyperliquid 限价单已挂出: {side} {symbol} {size} @ ${price} (oid={order_id})") + elif "filled" in first_status: + order_status = "filled" + filled_info = first_status["filled"] + avg_px = filled_info.get("avgPx", price) + logger.info(f"✅ Hyperliquid 限价单立即成交: {symbol} {size} @ ${avg_px}") + order_id = filled_info.get("oid") + else: + # 未知状态,记录并视为成功但标记 unknown + order_status = "unknown" + order_id = None + logger.warning(f"⚠️ Hyperliquid 限价单状态未知: {first_status}") + + side = "买入" if is_buy else "卖出" return { "success": True, + "order_status": order_status, # "resting" | "filled" | "unknown" + "order_id": order_id, "symbol": symbol, "side": "buy" if is_buy else "sell", "size": size,