This commit is contained in:
aaron 2026-03-24 22:14:53 +08:00
parent 7817632847
commit 14f1695350
2 changed files with 141 additions and 29 deletions

View File

@ -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

View File

@ -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,