update
This commit is contained in:
parent
f303e19a46
commit
b1fcb6d29e
@ -11,7 +11,11 @@ import pandas as pd
|
|||||||
from app.utils.logger import logger
|
from app.utils.logger import logger
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
from app.services.bitget_service import bitget_service
|
from app.services.bitget_service import bitget_service
|
||||||
from app.services.feishu_service import get_feishu_service, get_feishu_paper_trading_service
|
from app.services.feishu_service import (
|
||||||
|
get_feishu_service,
|
||||||
|
get_feishu_paper_trading_service,
|
||||||
|
get_feishu_error_service,
|
||||||
|
)
|
||||||
from app.services.telegram_service import get_telegram_service
|
from app.services.telegram_service import get_telegram_service
|
||||||
from app.services.dingtalk_service import get_dingtalk_service
|
from app.services.dingtalk_service import get_dingtalk_service
|
||||||
from app.services.paper_trading_service import get_paper_trading_service
|
from app.services.paper_trading_service import get_paper_trading_service
|
||||||
@ -96,6 +100,12 @@ class CryptoAgent:
|
|||||||
'long_term': 2.5,
|
'long_term': 2.5,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SIGNAL_MIN_EFFECTIVE_LEVERAGE = {
|
||||||
|
'short_term': 4.0,
|
||||||
|
'medium_term': 2.0,
|
||||||
|
'long_term': 2.0,
|
||||||
|
}
|
||||||
|
|
||||||
SIGNAL_EXECUTION_RULES = {
|
SIGNAL_EXECUTION_RULES = {
|
||||||
'short_term': {
|
'short_term': {
|
||||||
'min_add_price_gap_pct': 1.0,
|
'min_add_price_gap_pct': 1.0,
|
||||||
@ -123,6 +133,11 @@ class CryptoAgent:
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TP_SL_RETRY_ALERT_THRESHOLD = 3
|
||||||
|
TP_SL_MAX_RETRY_BEFORE_ERROR = 6
|
||||||
|
TP_SL_ALERT_COOLDOWN_MINUTES = 15
|
||||||
|
ANALYSIS_HEARTBEAT_INTERVAL_MINUTES = 60
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
def __new__(cls, *args, **kwargs):
|
||||||
"""单例模式 - 确保只有一个实例"""
|
"""单例模式 - 确保只有一个实例"""
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
@ -140,6 +155,7 @@ class CryptoAgent:
|
|||||||
self.exchange = bitget_service # 交易所服务
|
self.exchange = bitget_service # 交易所服务
|
||||||
self.feishu = get_feishu_service() # 通用飞书服务(crypto等)
|
self.feishu = get_feishu_service() # 通用飞书服务(crypto等)
|
||||||
self.feishu_paper = get_feishu_paper_trading_service() # 模拟交易专用飞书服务
|
self.feishu_paper = get_feishu_paper_trading_service() # 模拟交易专用飞书服务
|
||||||
|
self.feishu_error = get_feishu_error_service() # 异常/风控专用飞书服务
|
||||||
self.telegram = get_telegram_service()
|
self.telegram = get_telegram_service()
|
||||||
self.dingtalk = get_dingtalk_service() # 添加钉钉服务
|
self.dingtalk = get_dingtalk_service() # 添加钉钉服务
|
||||||
|
|
||||||
@ -217,6 +233,11 @@ class CryptoAgent:
|
|||||||
"last_analysis_detail": "",
|
"last_analysis_detail": "",
|
||||||
"next_scheduled_run_at": None,
|
"next_scheduled_run_at": None,
|
||||||
}
|
}
|
||||||
|
self._analysis_notification_state: Dict[str, Any] = {
|
||||||
|
"last_signal_at": None,
|
||||||
|
"last_signal_symbol": None,
|
||||||
|
"last_heartbeat_notified_at": None,
|
||||||
|
}
|
||||||
|
|
||||||
# 挂单 TP/SL 追踪:挂单成交后自动补设止盈止损
|
# 挂单 TP/SL 追踪:挂单成交后自动补设止盈止损
|
||||||
# key=order_id, value={symbol, is_long, size/contracts, tp_price, sl_price}
|
# key=order_id, value={symbol, is_long, size/contracts, tp_price, sl_price}
|
||||||
@ -282,6 +303,205 @@ class CryptoAgent:
|
|||||||
def _touch_analysis_heartbeat(self):
|
def _touch_analysis_heartbeat(self):
|
||||||
self._analysis_monitor["last_heartbeat_at"] = datetime.now().isoformat()
|
self._analysis_monitor["last_heartbeat_at"] = datetime.now().isoformat()
|
||||||
|
|
||||||
|
def _parse_iso_datetime(self, value: Optional[str]) -> Optional[datetime]:
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
return datetime.fromisoformat(str(value))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _classify_execution_block_reason(self,
|
||||||
|
platform_name: str,
|
||||||
|
decision: Optional[Dict[str, Any]]) -> tuple[str, str]:
|
||||||
|
if self._is_platform_halted(platform_name):
|
||||||
|
halt_info = self._platform_halts.get(platform_name, {})
|
||||||
|
halt_reason = halt_info.get("reason") or "平台已触发风控停机"
|
||||||
|
return "平台停机", halt_reason
|
||||||
|
|
||||||
|
decision = decision or {}
|
||||||
|
decision_type = str(decision.get("decision", "")).upper()
|
||||||
|
reason = str(decision.get("reason") or decision.get("reasoning") or "未返回具体原因").strip()
|
||||||
|
if any(keyword in reason for keyword in ["无适配信号", "未匹配到信号", "未选中信号"]):
|
||||||
|
return "无适配信号", "该平台本轮没有匹配到可执行信号"
|
||||||
|
|
||||||
|
if any(keyword in reason for keyword in ["账户余额无效", "余额不足", "可用余额不足", "保证金不足", "账户权益不足"]):
|
||||||
|
return "资金不足", reason
|
||||||
|
|
||||||
|
if any(keyword in reason for keyword in ["有效杠杆", "最小保证金", "最小下单", "合约张数", "名义仓位", "下单数量不足", "仓位价值过小"]):
|
||||||
|
return "仓位不达标", reason
|
||||||
|
|
||||||
|
if any(keyword in reason for keyword in ["同向持仓", "已有持仓", "无需重复开仓", "已有同向仓位"]):
|
||||||
|
return "已有同向仓位", reason
|
||||||
|
|
||||||
|
if any(keyword in reason for keyword in ["挂单等待", "挂单中", "待成交", "已有挂单", "等待成交"]):
|
||||||
|
return "等待挂单成交", reason
|
||||||
|
|
||||||
|
if any(keyword in reason for keyword in ["撤销反向挂单", "先撤销", "取消挂单", "撤单后再"]):
|
||||||
|
return "挂单切换中", reason
|
||||||
|
|
||||||
|
if any(keyword in reason for keyword in ["风控", "回撤", "熔断", "风险控制", "止损停机"]):
|
||||||
|
return "风控拦截", reason
|
||||||
|
|
||||||
|
if any(keyword in reason for keyword in ["未启用", "未初始化", "不可用"]):
|
||||||
|
return "平台不可用", reason
|
||||||
|
|
||||||
|
if decision_type in {"CLOSE", "REDUCE"}:
|
||||||
|
return "平仓未执行", reason
|
||||||
|
if decision_type == "CANCEL_PENDING":
|
||||||
|
return "撤单未执行", reason
|
||||||
|
if decision_type in {"OPEN", "ADD"}:
|
||||||
|
return "未满足执行条件", reason
|
||||||
|
|
||||||
|
return "未执行", reason
|
||||||
|
|
||||||
|
async def _maybe_send_analysis_heartbeat(self):
|
||||||
|
if not self.settings.feishu_enabled:
|
||||||
|
return
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
interval = timedelta(minutes=self.ANALYSIS_HEARTBEAT_INTERVAL_MINUTES)
|
||||||
|
state = self._analysis_notification_state
|
||||||
|
last_heartbeat_notified_at = self._parse_iso_datetime(state.get("last_heartbeat_notified_at"))
|
||||||
|
if last_heartbeat_notified_at and now - last_heartbeat_notified_at < interval:
|
||||||
|
return
|
||||||
|
|
||||||
|
last_signal_at = self._parse_iso_datetime(state.get("last_signal_at"))
|
||||||
|
if last_signal_at and now - last_signal_at < interval:
|
||||||
|
return
|
||||||
|
|
||||||
|
window_start = now - interval
|
||||||
|
recent_events = [
|
||||||
|
event for event in self._analysis_events
|
||||||
|
if self._parse_iso_datetime(event.get("timestamp")) and self._parse_iso_datetime(event.get("timestamp")) >= window_start
|
||||||
|
]
|
||||||
|
if not recent_events:
|
||||||
|
return
|
||||||
|
|
||||||
|
cycle_completed = sum(1 for event in recent_events if event.get("event_type") == "cycle_completed")
|
||||||
|
symbol_completed = sum(1 for event in recent_events if event.get("event_type") == "symbol_analysis_completed")
|
||||||
|
symbol_skipped = sum(1 for event in recent_events if event.get("event_type") == "symbol_analysis_skipped")
|
||||||
|
symbol_errors = sum(1 for event in recent_events if event.get("event_type") == "symbol_analysis_error")
|
||||||
|
valid_signal_total = sum(int(event.get("valid_signals", 0) or 0) for event in recent_events)
|
||||||
|
|
||||||
|
if cycle_completed <= 0 or valid_signal_total > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
last_cycle_completed_at = self._parse_iso_datetime(self._analysis_monitor.get("last_cycle_completed_at"))
|
||||||
|
if not last_cycle_completed_at or now - last_cycle_completed_at > interval:
|
||||||
|
return
|
||||||
|
|
||||||
|
last_symbol = self._analysis_monitor.get("last_analysis_symbol") or "-"
|
||||||
|
last_status = self._analysis_monitor.get("last_analysis_status") or "unknown"
|
||||||
|
last_detail = self._analysis_monitor.get("last_analysis_detail") or "最近一轮分析已完成"
|
||||||
|
threshold = self.settings.crypto_llm_threshold * 100
|
||||||
|
|
||||||
|
title = "💓 [分析心跳] 系统运行正常"
|
||||||
|
content = "\n".join([
|
||||||
|
f"最近 {self.ANALYSIS_HEARTBEAT_INTERVAL_MINUTES} 分钟持续完成市场分析,但暂无达到阈值的可执行信号。",
|
||||||
|
"",
|
||||||
|
f"**分析轮次**: {cycle_completed}",
|
||||||
|
f"**完成分析**: {symbol_completed} 个交易对",
|
||||||
|
f"**跳过分析**: {symbol_skipped} 次",
|
||||||
|
f"**分析异常**: {symbol_errors} 次",
|
||||||
|
f"**信号阈值**: {threshold:.0f}%",
|
||||||
|
f"**最近分析对象**: {last_symbol}",
|
||||||
|
f"**最近状态**: {last_status}",
|
||||||
|
f"**最近说明**: {last_detail}",
|
||||||
|
f"**最近完成时间**: {last_cycle_completed_at.strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
|
])
|
||||||
|
await self.feishu.send_card(title, content, "blue")
|
||||||
|
state["last_heartbeat_notified_at"] = now.isoformat()
|
||||||
|
self._record_analysis_event(
|
||||||
|
"heartbeat_notified",
|
||||||
|
status="info",
|
||||||
|
detail=f"已发送分析心跳通知,最近 {self.ANALYSIS_HEARTBEAT_INTERVAL_MINUTES} 分钟无有效信号",
|
||||||
|
extra={
|
||||||
|
"window_minutes": self.ANALYSIS_HEARTBEAT_INTERVAL_MINUTES,
|
||||||
|
"last_signal_at": state.get("last_signal_at"),
|
||||||
|
"last_signal_symbol": state.get("last_signal_symbol"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _build_pending_tp_sl_task(self,
|
||||||
|
symbol: str,
|
||||||
|
is_long: bool,
|
||||||
|
size: float,
|
||||||
|
tp_price: Optional[float],
|
||||||
|
sl_price: Optional[float],
|
||||||
|
order_status: Optional[str] = None,
|
||||||
|
has_real_order_id: bool = True,
|
||||||
|
retry_count: int = 0,
|
||||||
|
first_seen_at: Optional[str] = None,
|
||||||
|
last_alert_at: Optional[str] = None) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"symbol": symbol,
|
||||||
|
"is_long": is_long,
|
||||||
|
"size": size,
|
||||||
|
"tp_price": tp_price,
|
||||||
|
"sl_price": sl_price,
|
||||||
|
"order_status": order_status,
|
||||||
|
"has_real_order_id": has_real_order_id,
|
||||||
|
"retry_count": retry_count,
|
||||||
|
"first_seen_at": first_seen_at or datetime.now().isoformat(),
|
||||||
|
"last_alert_at": last_alert_at,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _maybe_alert_tp_sl_incomplete(self,
|
||||||
|
platform: str,
|
||||||
|
tracking_key: str,
|
||||||
|
task: Dict[str, Any],
|
||||||
|
reason: str,
|
||||||
|
force: bool = False):
|
||||||
|
now = datetime.now()
|
||||||
|
last_alert_at_raw = task.get("last_alert_at")
|
||||||
|
last_alert_at = None
|
||||||
|
if last_alert_at_raw:
|
||||||
|
try:
|
||||||
|
last_alert_at = datetime.fromisoformat(str(last_alert_at_raw))
|
||||||
|
except ValueError:
|
||||||
|
last_alert_at = None
|
||||||
|
|
||||||
|
should_alert = force
|
||||||
|
if not should_alert and task.get("retry_count", 0) >= self.TP_SL_RETRY_ALERT_THRESHOLD:
|
||||||
|
should_alert = (
|
||||||
|
last_alert_at is None or
|
||||||
|
now - last_alert_at >= timedelta(minutes=self.TP_SL_ALERT_COOLDOWN_MINUTES)
|
||||||
|
)
|
||||||
|
|
||||||
|
severity = "error" if task.get("retry_count", 0) >= self.TP_SL_MAX_RETRY_BEFORE_ERROR else "warning"
|
||||||
|
self._record_execution_event(
|
||||||
|
platform,
|
||||||
|
"tp_sl_incomplete",
|
||||||
|
symbol=f"{task.get('symbol', '')}USDT",
|
||||||
|
reason=reason,
|
||||||
|
status=severity,
|
||||||
|
extra={
|
||||||
|
"tracking_key": tracking_key,
|
||||||
|
"retry_count": task.get("retry_count", 0),
|
||||||
|
"missing_take_profit": task.get("tp_price") is not None,
|
||||||
|
"missing_stop_loss": task.get("sl_price") is not None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if should_alert:
|
||||||
|
missing_parts = []
|
||||||
|
if task.get("tp_price") is not None:
|
||||||
|
missing_parts.append(f"TP={task.get('tp_price')}")
|
||||||
|
if task.get("sl_price") is not None:
|
||||||
|
missing_parts.append(f"SL={task.get('sl_price')}")
|
||||||
|
missing_desc = " / ".join(missing_parts) or "保护单缺失"
|
||||||
|
await self._send_alert_notification(
|
||||||
|
f"⚠️ [{platform}] 保护单不完整 - {task.get('symbol', '')}USDT",
|
||||||
|
"\n".join([
|
||||||
|
f"追踪ID: {tracking_key}",
|
||||||
|
f"缺失项目: {missing_desc}",
|
||||||
|
f"重试次数: {task.get('retry_count', 0)}",
|
||||||
|
f"原因: {reason}",
|
||||||
|
])
|
||||||
|
)
|
||||||
|
task["last_alert_at"] = now.isoformat()
|
||||||
|
|
||||||
def _record_analysis_event(self,
|
def _record_analysis_event(self,
|
||||||
event_type: str,
|
event_type: str,
|
||||||
symbol: str = "",
|
symbol: str = "",
|
||||||
@ -620,6 +840,7 @@ class CryptoAgent:
|
|||||||
# 检查实盘挂单是否已成交,补设止盈止损
|
# 检查实盘挂单是否已成交,补设止盈止损
|
||||||
if self.hyperliquid:
|
if self.hyperliquid:
|
||||||
await self._check_and_set_pending_tp_sl_hyperliquid()
|
await self._check_and_set_pending_tp_sl_hyperliquid()
|
||||||
|
await self._check_hyperliquid_missing_tp_sl()
|
||||||
if self.bitget:
|
if self.bitget:
|
||||||
await self._check_and_set_pending_tp_sl_bitget()
|
await self._check_and_set_pending_tp_sl_bitget()
|
||||||
await self._check_bitget_missing_tp_sl() # 兜底:检查缺少的 TP/SL 并补救
|
await self._check_bitget_missing_tp_sl() # 兜底:检查缺少的 TP/SL 并补救
|
||||||
@ -638,6 +859,7 @@ class CryptoAgent:
|
|||||||
status="success",
|
status="success",
|
||||||
detail=f"本轮分析完成,共扫描 {len(self.symbols)} 个交易对",
|
detail=f"本轮分析完成,共扫描 {len(self.symbols)} 个交易对",
|
||||||
)
|
)
|
||||||
|
await self._maybe_send_analysis_heartbeat()
|
||||||
logger.info("\n" + "─" * 60)
|
logger.info("\n" + "─" * 60)
|
||||||
logger.info(f"✅ 本轮分析完成,共分析 {len(self.symbols)} 个交易对")
|
logger.info(f"✅ 本轮分析完成,共分析 {len(self.symbols)} 个交易对")
|
||||||
logger.info("─" * 60 + "\n")
|
logger.info("─" * 60 + "\n")
|
||||||
@ -1375,6 +1597,100 @@ class CryptoAgent:
|
|||||||
status="warning",
|
status="warning",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self._notify_execution_summary_if_needed(
|
||||||
|
market_signal=market_signal,
|
||||||
|
current_price=current_price,
|
||||||
|
decisions={
|
||||||
|
"PaperTrading": paper_decision,
|
||||||
|
"Hyperliquid": hyperliquid_decision,
|
||||||
|
"Bitget": bitget_decision,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _notify_execution_summary_if_needed(
|
||||||
|
self,
|
||||||
|
market_signal: Dict[str, Any],
|
||||||
|
current_price: float,
|
||||||
|
decisions: Dict[str, Dict[str, Any]],
|
||||||
|
):
|
||||||
|
"""当存在可交易信号,但本轮所有平台都未真正执行时,仅发送一条汇总通知。"""
|
||||||
|
actionable: Dict[str, Dict[str, Any]] = {}
|
||||||
|
executed = False
|
||||||
|
|
||||||
|
for platform_name, decision in (decisions or {}).items():
|
||||||
|
if not decision:
|
||||||
|
continue
|
||||||
|
if decision.get('_execution_succeeded'):
|
||||||
|
executed = True
|
||||||
|
if decision.get('decision') in {'OPEN', 'ADD', 'CLOSE', 'CANCEL_PENDING'}:
|
||||||
|
actionable[platform_name] = decision
|
||||||
|
|
||||||
|
if executed or not actionable:
|
||||||
|
return
|
||||||
|
|
||||||
|
symbol = market_signal.get('symbol', '')
|
||||||
|
signal = self._get_best_signal_from_market(market_signal)
|
||||||
|
if not signal:
|
||||||
|
return
|
||||||
|
|
||||||
|
confidence = signal.get('confidence', 0)
|
||||||
|
entry_type = signal.get('entry_type', 'market')
|
||||||
|
entry_price = signal.get('entry_price', current_price)
|
||||||
|
signal_timeframe = signal.get('timeframe', signal.get('type', 'unknown'))
|
||||||
|
timeframe_map = {'short_term': '短线', 'medium_term': '趋势', 'long_term': '长线'}
|
||||||
|
timeframe_text = timeframe_map.get(signal_timeframe, signal_timeframe)
|
||||||
|
action = signal.get('action', 'wait')
|
||||||
|
action_text = {'buy': '做多', 'sell': '做空', 'wait': '观望'}.get(action, action)
|
||||||
|
|
||||||
|
title = f"[执行汇总] {symbol} 信号未落单"
|
||||||
|
content_parts = [
|
||||||
|
f"**信号**: {action_text} | {timeframe_text} | 📈 **{confidence}%**",
|
||||||
|
"",
|
||||||
|
f"**入场方式**: {entry_type}",
|
||||||
|
f"**建议入场价**: ${entry_price:,.2f}" if isinstance(entry_price, (int, float)) else f"**建议入场价**: {entry_price}",
|
||||||
|
f"**当前价格**: ${current_price:,.2f}",
|
||||||
|
"",
|
||||||
|
"**平台结果**:",
|
||||||
|
]
|
||||||
|
blocked_platforms: List[Dict[str, Any]] = []
|
||||||
|
|
||||||
|
for platform_name, decision in actionable.items():
|
||||||
|
tag, detail = self._classify_execution_block_reason(platform_name, decision)
|
||||||
|
content_parts.append(f"- {platform_name}: **{tag}** | {detail}")
|
||||||
|
blocked_platforms.append({
|
||||||
|
"platform": platform_name,
|
||||||
|
"tag": tag,
|
||||||
|
"detail": detail,
|
||||||
|
"decision": decision.get("decision"),
|
||||||
|
})
|
||||||
|
|
||||||
|
content = "\n".join(content_parts)
|
||||||
|
self._record_execution_event(
|
||||||
|
"SYSTEM",
|
||||||
|
"execution_blocked_summary",
|
||||||
|
symbol=symbol,
|
||||||
|
reason=f"{action_text} {timeframe_text} 信号未落单",
|
||||||
|
status="warning",
|
||||||
|
extra={
|
||||||
|
"signal_action": action,
|
||||||
|
"signal_action_text": action_text,
|
||||||
|
"signal_timeframe": signal_timeframe,
|
||||||
|
"signal_timeframe_text": timeframe_text,
|
||||||
|
"confidence": confidence,
|
||||||
|
"entry_type": entry_type,
|
||||||
|
"entry_price": entry_price,
|
||||||
|
"current_price": current_price,
|
||||||
|
"blocked_platforms": blocked_platforms,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.settings.feishu_enabled:
|
||||||
|
await self.feishu.send_card(title, content, "orange")
|
||||||
|
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)
|
||||||
|
|
||||||
async def _execute_paper_decisions(self, decision: Dict[str, Any],
|
async def _execute_paper_decisions(self, decision: Dict[str, Any],
|
||||||
market_signal: Dict[str, Any],
|
market_signal: Dict[str, Any],
|
||||||
current_price: float):
|
current_price: float):
|
||||||
@ -1405,12 +1721,12 @@ class CryptoAgent:
|
|||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
order_id = result.get('order_id', 'unknown')
|
order_id = result.get('order_id', 'unknown')
|
||||||
logger.info(f" ✅ 交易成功: 订单ID {order_id}")
|
logger.info(f" ✅ 交易成功: 订单ID {order_id}")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event(
|
self._record_execution_event(
|
||||||
"PaperTrading", "open_success", decision=decision, status="success",
|
"PaperTrading", "open_success", decision=decision, status="success",
|
||||||
reason=decision.get('reason', decision.get('reasoning', '')),
|
reason=decision.get('reason', decision.get('reasoning', '')),
|
||||||
extra={"order_id": order_id},
|
extra={"order_id": order_id},
|
||||||
)
|
)
|
||||||
await self._send_signal_notification(market_signal, decision, current_price)
|
|
||||||
|
|
||||||
# TP/SL 警告
|
# TP/SL 警告
|
||||||
if result.get('tp_sl_warning'):
|
if result.get('tp_sl_warning'):
|
||||||
@ -1427,8 +1743,8 @@ class CryptoAgent:
|
|||||||
|
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
logger.info(f" ✅ 平仓成功")
|
logger.info(f" ✅ 平仓成功")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event("PaperTrading", "close_success", decision=decision, status="success")
|
self._record_execution_event("PaperTrading", "close_success", decision=decision, status="success")
|
||||||
await self._send_signal_notification(market_signal, decision, current_price)
|
|
||||||
if next_decision:
|
if next_decision:
|
||||||
await self._execute_paper_decisions(next_decision, market_signal, current_price)
|
await self._execute_paper_decisions(next_decision, market_signal, current_price)
|
||||||
else:
|
else:
|
||||||
@ -1450,11 +1766,11 @@ class CryptoAgent:
|
|||||||
|
|
||||||
if success_count > 0:
|
if success_count > 0:
|
||||||
logger.info(f" ✅ 成功取消 {success_count} 个挂单")
|
logger.info(f" ✅ 成功取消 {success_count} 个挂单")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event(
|
self._record_execution_event(
|
||||||
"PaperTrading", "cancel_success", decision=decision, status="success",
|
"PaperTrading", "cancel_success", decision=decision, status="success",
|
||||||
extra={"cancelled_count": success_count},
|
extra={"cancelled_count": success_count},
|
||||||
)
|
)
|
||||||
await self._send_signal_notification(market_signal, decision, current_price)
|
|
||||||
if next_decision:
|
if next_decision:
|
||||||
await self._execute_paper_decisions(next_decision, market_signal, current_price)
|
await self._execute_paper_decisions(next_decision, market_signal, current_price)
|
||||||
else:
|
else:
|
||||||
@ -1908,6 +2224,8 @@ class CryptoAgent:
|
|||||||
# 根据配置发送通知 - [信号] 发送到 crypto webhook
|
# 根据配置发送通知 - [信号] 发送到 crypto webhook
|
||||||
if self.settings.feishu_enabled:
|
if self.settings.feishu_enabled:
|
||||||
await self.feishu.send_card(title, content, color)
|
await self.feishu.send_card(title, content, color)
|
||||||
|
self._analysis_notification_state["last_signal_at"] = datetime.now().isoformat()
|
||||||
|
self._analysis_notification_state["last_signal_symbol"] = symbol
|
||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
# Telegram 使用文本格式
|
# Telegram 使用文本格式
|
||||||
message = f"{title}\n\n{content}"
|
message = f"{title}\n\n{content}"
|
||||||
@ -2583,8 +2901,8 @@ class CryptoAgent:
|
|||||||
f"🕐 **时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
f"🕐 **时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
])
|
])
|
||||||
logger.error(f"[Hyperliquid] {operation} 失败 | {symbol} | {error}")
|
logger.error(f"[Hyperliquid] {operation} 失败 | {symbol} | {error}")
|
||||||
if self.settings.feishu_enabled:
|
if self.settings.feishu_enabled and self.feishu_error:
|
||||||
await self.feishu.send_card(title, content, "red")
|
await self.feishu_error.send_card(title, content, "red")
|
||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
await self.telegram.send_message(f"{title}\n\n{content}")
|
await self.telegram.send_message(f"{title}\n\n{content}")
|
||||||
if self.settings.dingtalk_enabled:
|
if self.settings.dingtalk_enabled:
|
||||||
@ -2599,8 +2917,8 @@ class CryptoAgent:
|
|||||||
f"🕐 **时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
f"🕐 **时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
])
|
])
|
||||||
logger.error(f"[Bitget] {operation} 失败 | {symbol} | {error}")
|
logger.error(f"[Bitget] {operation} 失败 | {symbol} | {error}")
|
||||||
if self.settings.feishu_enabled:
|
if self.settings.feishu_enabled and self.feishu_error:
|
||||||
await self.feishu.send_card(title, content, "red")
|
await self.feishu_error.send_card(title, content, "red")
|
||||||
if self.settings.telegram_enabled:
|
if self.settings.telegram_enabled:
|
||||||
await self.telegram.send_message(f"{title}\n\n{content}")
|
await self.telegram.send_message(f"{title}\n\n{content}")
|
||||||
if self.settings.dingtalk_enabled:
|
if self.settings.dingtalk_enabled:
|
||||||
@ -2735,6 +3053,7 @@ class CryptoAgent:
|
|||||||
current_leverage = account.get('current_total_leverage', 0)
|
current_leverage = account.get('current_total_leverage', 0)
|
||||||
max_leverage = account.get('max_total_leverage', 10)
|
max_leverage = account.get('max_total_leverage', 10)
|
||||||
order_leverage = account.get('order_leverage', 10)
|
order_leverage = account.get('order_leverage', 10)
|
||||||
|
min_effective_leverage = self.SIGNAL_MIN_EFFECTIVE_LEVERAGE.get(signal_type, 2.0)
|
||||||
|
|
||||||
target_margin_pct, sizing_reason, _, _ = resolve_target_margin_pct(
|
target_margin_pct, sizing_reason, _, _ = resolve_target_margin_pct(
|
||||||
position_size=position_size,
|
position_size=position_size,
|
||||||
@ -2773,6 +3092,7 @@ class CryptoAgent:
|
|||||||
target_margin_pct=target_margin_pct,
|
target_margin_pct=target_margin_pct,
|
||||||
max_margin_pct=max_margin_pct,
|
max_margin_pct=max_margin_pct,
|
||||||
min_margin=min_margin,
|
min_margin=min_margin,
|
||||||
|
min_effective_leverage=min_effective_leverage,
|
||||||
)
|
)
|
||||||
|
|
||||||
if margin <= 0:
|
if margin <= 0:
|
||||||
@ -2780,6 +3100,7 @@ class CryptoAgent:
|
|||||||
|
|
||||||
return margin, (
|
return margin, (
|
||||||
f"{sizing_reason} | 平台: {platform_name} | "
|
f"{sizing_reason} | 平台: {platform_name} | "
|
||||||
|
f"最小有效杠杆 {min_effective_leverage:.1f}x | "
|
||||||
f"限制后保证金 ${margin:.2f} ({budget_reason})"
|
f"限制后保证金 ${margin:.2f} ({budget_reason})"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -3258,6 +3579,7 @@ class CryptoAgent:
|
|||||||
order_id = result.get('order_id', 'unknown')
|
order_id = result.get('order_id', 'unknown')
|
||||||
order_status = result.get('order_status', 'filled')
|
order_status = result.get('order_status', 'filled')
|
||||||
logger.info(f" ✅ Bitget 交易成功: {order_id} ({order_status})")
|
logger.info(f" ✅ Bitget 交易成功: {order_id} ({order_status})")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event(
|
self._record_execution_event(
|
||||||
"Bitget", "open_success", decision=decision, status="success",
|
"Bitget", "open_success", decision=decision, status="success",
|
||||||
reason=decision.get('reason', decision.get('reasoning', '')),
|
reason=decision.get('reason', decision.get('reasoning', '')),
|
||||||
@ -3279,12 +3601,15 @@ class CryptoAgent:
|
|||||||
if result.get('pending_tp_sl'):
|
if result.get('pending_tp_sl'):
|
||||||
order_id = result.get('order_id')
|
order_id = result.get('order_id')
|
||||||
if order_id:
|
if order_id:
|
||||||
self._bg_pending_tp_sl[order_id] = {
|
signal_action = decision.get('signal_action', decision.get('action'))
|
||||||
'symbol': symbol,
|
pending_tp_sl = result.get('pending_tp_sl') or {}
|
||||||
'is_long': decision.get('action') == 'buy',
|
self._bg_pending_tp_sl[order_id] = self._build_pending_tp_sl_task(
|
||||||
'contracts': result.get('contracts', 0),
|
symbol=symbol,
|
||||||
**result['pending_tp_sl']
|
is_long=signal_action == 'buy',
|
||||||
}
|
size=result.get('contracts', 0),
|
||||||
|
tp_price=pending_tp_sl.get('tp_price'),
|
||||||
|
sl_price=pending_tp_sl.get('sl_price'),
|
||||||
|
)
|
||||||
logger.info(f" 📌 已记录挂单 TP/SL (oid={order_id})")
|
logger.info(f" 📌 已记录挂单 TP/SL (oid={order_id})")
|
||||||
else:
|
else:
|
||||||
error = result.get('error', result.get('message', '未知错误'))
|
error = result.get('error', result.get('message', '未知错误'))
|
||||||
@ -3299,8 +3624,8 @@ class CryptoAgent:
|
|||||||
|
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
logger.info(f" ✅ Bitget 平仓成功")
|
logger.info(f" ✅ Bitget 平仓成功")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event("Bitget", "close_success", decision=decision, status="success")
|
self._record_execution_event("Bitget", "close_success", decision=decision, status="success")
|
||||||
await self._send_signal_notification(market_signal, decision, current_price, prefix="[Bitget]")
|
|
||||||
if next_decision:
|
if next_decision:
|
||||||
await self._execute_bitget_decisions(next_decision, market_signal, current_price)
|
await self._execute_bitget_decisions(next_decision, market_signal, current_price)
|
||||||
else:
|
else:
|
||||||
@ -3325,6 +3650,7 @@ class CryptoAgent:
|
|||||||
|
|
||||||
if success_count > 0:
|
if success_count > 0:
|
||||||
logger.info(f" ✅ Bitget 取消成功: {success_count} 个挂单")
|
logger.info(f" ✅ Bitget 取消成功: {success_count} 个挂单")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event(
|
self._record_execution_event(
|
||||||
"Bitget", "cancel_success", decision=decision, status="success",
|
"Bitget", "cancel_success", decision=decision, status="success",
|
||||||
extra={"cancelled_count": success_count},
|
extra={"cancelled_count": success_count},
|
||||||
@ -3613,6 +3939,7 @@ class CryptoAgent:
|
|||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
order_status = result.get('order_status', 'filled')
|
order_status = result.get('order_status', 'filled')
|
||||||
logger.info(f" ✅ Hyperliquid 交易成功 ({order_status})")
|
logger.info(f" ✅ Hyperliquid 交易成功 ({order_status})")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event(
|
self._record_execution_event(
|
||||||
"Hyperliquid", "open_success", decision=decision, status="success",
|
"Hyperliquid", "open_success", decision=decision, status="success",
|
||||||
reason=decision.get('reason', decision.get('reasoning', '')),
|
reason=decision.get('reason', decision.get('reasoning', '')),
|
||||||
@ -3623,6 +3950,21 @@ class CryptoAgent:
|
|||||||
prefix="[Hyperliquid]",
|
prefix="[Hyperliquid]",
|
||||||
hl_order_status=order_status
|
hl_order_status=order_status
|
||||||
)
|
)
|
||||||
|
if result.get('pending_tp_sl'):
|
||||||
|
order_id = str(result.get('order_id') or '')
|
||||||
|
pending_tp_sl = result.get('pending_tp_sl') or {}
|
||||||
|
tracking_key = order_id or f"{symbol}:{datetime.now().timestamp()}"
|
||||||
|
signal_action = decision.get('signal_action', decision.get('action'))
|
||||||
|
self._hl_pending_tp_sl[tracking_key] = self._build_pending_tp_sl_task(
|
||||||
|
symbol=symbol.replace('USDT', ''),
|
||||||
|
is_long=signal_action == 'buy',
|
||||||
|
size=result.get('position_size') or result.get('size') or 0,
|
||||||
|
tp_price=pending_tp_sl.get('tp_price'),
|
||||||
|
sl_price=pending_tp_sl.get('sl_price'),
|
||||||
|
order_status=order_status,
|
||||||
|
has_real_order_id=bool(order_id),
|
||||||
|
)
|
||||||
|
logger.info(f" 📌 已记录 Hyperliquid TP/SL 待补设任务 (key={tracking_key})")
|
||||||
if result.get('tp_sl_warning'):
|
if result.get('tp_sl_warning'):
|
||||||
await self._notify_hyperliquid_error(symbol, "设置止盈止损", result['tp_sl_warning'])
|
await self._notify_hyperliquid_error(symbol, "设置止盈止损", result['tp_sl_warning'])
|
||||||
else:
|
else:
|
||||||
@ -3636,8 +3978,8 @@ class CryptoAgent:
|
|||||||
result = await executor.execute_close(decision, current_price)
|
result = await executor.execute_close(decision, current_price)
|
||||||
if result.get('success'):
|
if result.get('success'):
|
||||||
logger.info(f" ✅ Hyperliquid 平仓成功")
|
logger.info(f" ✅ Hyperliquid 平仓成功")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event("Hyperliquid", "close_success", decision=decision, status="success")
|
self._record_execution_event("Hyperliquid", "close_success", decision=decision, status="success")
|
||||||
await self._send_signal_notification(market_signal, decision, current_price, prefix="[Hyperliquid]")
|
|
||||||
if next_decision:
|
if next_decision:
|
||||||
await self._execute_hyperliquid_decisions(next_decision, market_signal, current_price)
|
await self._execute_hyperliquid_decisions(next_decision, market_signal, current_price)
|
||||||
else:
|
else:
|
||||||
@ -3657,6 +3999,7 @@ class CryptoAgent:
|
|||||||
success_count += 1
|
success_count += 1
|
||||||
if success_count > 0:
|
if success_count > 0:
|
||||||
logger.info(f" ✅ Hyperliquid 取消成功: {success_count} 个")
|
logger.info(f" ✅ Hyperliquid 取消成功: {success_count} 个")
|
||||||
|
decision['_execution_succeeded'] = True
|
||||||
self._record_execution_event(
|
self._record_execution_event(
|
||||||
"Hyperliquid", "cancel_success", decision=decision, status="success",
|
"Hyperliquid", "cancel_success", decision=decision, status="success",
|
||||||
extra={"cancelled_count": success_count},
|
extra={"cancelled_count": success_count},
|
||||||
@ -3835,24 +4178,73 @@ class CryptoAgent:
|
|||||||
try:
|
try:
|
||||||
for order_id, info in list(self._hl_pending_tp_sl.items()):
|
for order_id, info in list(self._hl_pending_tp_sl.items()):
|
||||||
symbol = info['symbol']
|
symbol = info['symbol']
|
||||||
open_orders = self.hyperliquid.get_open_orders(symbol)
|
has_real_order_id = info.get('has_real_order_id', True)
|
||||||
still_open = any(str(o.get('order_id')) == order_id for o in open_orders)
|
if has_real_order_id:
|
||||||
|
open_orders = self.hyperliquid.get_open_orders(symbol)
|
||||||
|
still_open = any(str(o.get('order_id')) == order_id for o in open_orders)
|
||||||
|
else:
|
||||||
|
still_open = info.get('order_status') == 'resting'
|
||||||
if not still_open:
|
if not still_open:
|
||||||
# 订单已不在挂单列表 → 已成交,补设 TP/SL
|
# 订单已不在挂单列表 → 已成交,补设 TP/SL
|
||||||
tp_price = info.get('tp_price')
|
tp_price = info.get('tp_price')
|
||||||
sl_price = info.get('sl_price')
|
sl_price = info.get('sl_price')
|
||||||
|
position = self.hyperliquid.get_position_for_symbol(symbol)
|
||||||
|
if not position:
|
||||||
|
logger.info(f"[Hyperliquid] 挂单/持仓 {order_id} ({symbol}) 当前无持仓,跳过 TP/SL 补设")
|
||||||
|
del self._hl_pending_tp_sl[order_id]
|
||||||
|
continue
|
||||||
|
size = info.get('size') or abs(position.get('size', 0))
|
||||||
|
if size <= 0:
|
||||||
|
logger.warning(f"[Hyperliquid] 挂单/持仓 {order_id} ({symbol}) 数量无效,跳过 TP/SL 补设")
|
||||||
|
del self._hl_pending_tp_sl[order_id]
|
||||||
|
continue
|
||||||
logger.info(f"[Hyperliquid] 挂单 {order_id} ({symbol}) 已成交,补设 TP/SL...")
|
logger.info(f"[Hyperliquid] 挂单 {order_id} ({symbol}) 已成交,补设 TP/SL...")
|
||||||
tp_sl_result = self.hyperliquid.set_tp_sl(
|
tp_sl_result = self.hyperliquid.set_tp_sl(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
is_long=info['is_long'],
|
is_long=info['is_long'],
|
||||||
size=info['size'],
|
size=size,
|
||||||
tp_price=tp_price,
|
tp_price=tp_price,
|
||||||
sl_price=sl_price,
|
sl_price=sl_price,
|
||||||
)
|
)
|
||||||
if tp_sl_result.get('success'):
|
info['retry_count'] = int(info.get('retry_count', 0)) + 1
|
||||||
|
tp_set = tp_sl_result.get('tp_set', False)
|
||||||
|
sl_set = tp_sl_result.get('sl_set', False)
|
||||||
|
if tp_set and sl_set:
|
||||||
logger.info(f"[Hyperliquid] ✅ TP/SL 补设成功: {symbol} TP={tp_price} SL={sl_price}")
|
logger.info(f"[Hyperliquid] ✅ TP/SL 补设成功: {symbol} TP={tp_price} SL={sl_price}")
|
||||||
|
elif tp_set or sl_set:
|
||||||
|
missing_tp = tp_price if not tp_set else None
|
||||||
|
missing_sl = sl_price if not sl_set else None
|
||||||
|
self._hl_pending_tp_sl[order_id] = self._build_pending_tp_sl_task(
|
||||||
|
symbol=info['symbol'],
|
||||||
|
is_long=info['is_long'],
|
||||||
|
size=size,
|
||||||
|
tp_price=missing_tp,
|
||||||
|
sl_price=missing_sl,
|
||||||
|
order_status=info.get('order_status'),
|
||||||
|
has_real_order_id=info.get('has_real_order_id', True),
|
||||||
|
retry_count=info.get('retry_count', 0),
|
||||||
|
first_seen_at=info.get('first_seen_at'),
|
||||||
|
last_alert_at=info.get('last_alert_at'),
|
||||||
|
)
|
||||||
|
set_text = "TP" if tp_set else "SL"
|
||||||
|
fail_text = "TP" if not tp_set else "SL"
|
||||||
|
logger.warning(f"[Hyperliquid] ⚠️ TP/SL 部分成功: {symbol} {set_text}已设, {fail_text}待下轮补设")
|
||||||
|
await self._maybe_alert_tp_sl_incomplete(
|
||||||
|
"Hyperliquid",
|
||||||
|
order_id,
|
||||||
|
self._hl_pending_tp_sl[order_id],
|
||||||
|
f"{set_text}已设,{fail_text}补设失败",
|
||||||
|
)
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[Hyperliquid] ⚠️ TP/SL 补设失败: {tp_sl_result.get('error')}")
|
logger.warning(f"[Hyperliquid] ⚠️ TP/SL 补设失败: {tp_sl_result.get('errors') or tp_sl_result.get('error')}")
|
||||||
|
await self._maybe_alert_tp_sl_incomplete(
|
||||||
|
"Hyperliquid",
|
||||||
|
order_id,
|
||||||
|
info,
|
||||||
|
str(tp_sl_result.get('errors') or tp_sl_result.get('error') or 'TP/SL补设失败'),
|
||||||
|
)
|
||||||
|
continue
|
||||||
del self._hl_pending_tp_sl[order_id]
|
del self._hl_pending_tp_sl[order_id]
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[Hyperliquid] 检查挂单 TP/SL 补设异常: {e}")
|
logger.error(f"[Hyperliquid] 检查挂单 TP/SL 补设异常: {e}")
|
||||||
@ -3876,10 +4268,11 @@ class CryptoAgent:
|
|||||||
tp_sl_result = self.bitget.set_tp_sl(
|
tp_sl_result = self.bitget.set_tp_sl(
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
is_long=info['is_long'],
|
is_long=info['is_long'],
|
||||||
size=info['contracts'],
|
size=info['size'],
|
||||||
tp_price=tp_price,
|
tp_price=tp_price,
|
||||||
sl_price=sl_price,
|
sl_price=sl_price,
|
||||||
)
|
)
|
||||||
|
info['retry_count'] = int(info.get('retry_count', 0)) + 1
|
||||||
tp_set = tp_sl_result.get('tp_set', False)
|
tp_set = tp_sl_result.get('tp_set', False)
|
||||||
sl_set = tp_sl_result.get('sl_set', False)
|
sl_set = tp_sl_result.get('sl_set', False)
|
||||||
|
|
||||||
@ -3890,17 +4283,34 @@ class CryptoAgent:
|
|||||||
missing_tp = tp_price if not tp_set else None
|
missing_tp = tp_price if not tp_set else None
|
||||||
missing_sl = sl_price if not sl_set else None
|
missing_sl = sl_price if not sl_set else None
|
||||||
if missing_tp or missing_sl:
|
if missing_tp or missing_sl:
|
||||||
self._bg_pending_tp_sl[order_id] = {
|
self._bg_pending_tp_sl[order_id] = self._build_pending_tp_sl_task(
|
||||||
**info,
|
symbol=info['symbol'],
|
||||||
'tp_price': missing_tp,
|
is_long=info['is_long'],
|
||||||
'sl_price': missing_sl,
|
size=info['size'],
|
||||||
}
|
tp_price=missing_tp,
|
||||||
|
sl_price=missing_sl,
|
||||||
|
retry_count=info.get('retry_count', 0),
|
||||||
|
first_seen_at=info.get('first_seen_at'),
|
||||||
|
last_alert_at=info.get('last_alert_at'),
|
||||||
|
)
|
||||||
set_text = "TP" if tp_set else "SL"
|
set_text = "TP" if tp_set else "SL"
|
||||||
fail_text = "TP" if not tp_set else "SL"
|
fail_text = "TP" if not tp_set else "SL"
|
||||||
logger.warning(f"[Bitget] ⚠️ TP/SL 部分成功: {symbol} {set_text}已设, {fail_text}待下轮补设")
|
logger.warning(f"[Bitget] ⚠️ TP/SL 部分成功: {symbol} {set_text}已设, {fail_text}待下轮补设")
|
||||||
|
await self._maybe_alert_tp_sl_incomplete(
|
||||||
|
"Bitget",
|
||||||
|
order_id,
|
||||||
|
self._bg_pending_tp_sl[order_id],
|
||||||
|
f"{set_text}已设,{fail_text}补设失败",
|
||||||
|
)
|
||||||
continue # 不删除,下轮继续
|
continue # 不删除,下轮继续
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[Bitget] ⚠️ TP/SL 补设失败: {tp_sl_result.get('errors')}")
|
logger.warning(f"[Bitget] ⚠️ TP/SL 补设失败: {tp_sl_result.get('errors')}")
|
||||||
|
await self._maybe_alert_tp_sl_incomplete(
|
||||||
|
"Bitget",
|
||||||
|
order_id,
|
||||||
|
info,
|
||||||
|
str(tp_sl_result.get('errors') or 'TP/SL补设失败'),
|
||||||
|
)
|
||||||
continue # 不删除,下轮继续重试
|
continue # 不删除,下轮继续重试
|
||||||
|
|
||||||
del self._bg_pending_tp_sl[order_id]
|
del self._bg_pending_tp_sl[order_id]
|
||||||
@ -3981,10 +4391,110 @@ class CryptoAgent:
|
|||||||
logger.info(f"[Bitget] ✅ 补救成功: {symbol} {' & '.join(set_parts)}")
|
logger.info(f"[Bitget] ✅ 补救成功: {symbol} {' & '.join(set_parts)}")
|
||||||
else:
|
else:
|
||||||
logger.warning(f"[Bitget] ⚠️ 补救失败: {tp_sl_result.get('errors')}")
|
logger.warning(f"[Bitget] ⚠️ 补救失败: {tp_sl_result.get('errors')}")
|
||||||
|
await self._maybe_alert_tp_sl_incomplete(
|
||||||
|
"Bitget",
|
||||||
|
f"fallback:{symbol}",
|
||||||
|
self._build_pending_tp_sl_task(
|
||||||
|
symbol=coin,
|
||||||
|
is_long=pos.get('size', 0) > 0,
|
||||||
|
size=size,
|
||||||
|
tp_price=set_tp,
|
||||||
|
sl_price=set_sl,
|
||||||
|
retry_count=self.TP_SL_RETRY_ALERT_THRESHOLD,
|
||||||
|
),
|
||||||
|
str(tp_sl_result.get('errors') or '兜底补设失败'),
|
||||||
|
force=True,
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[Bitget] 止盈止损兜底检查异常: {e}")
|
logger.error(f"[Bitget] 止盈止损兜底检查异常: {e}")
|
||||||
|
|
||||||
|
async def _check_hyperliquid_missing_tp_sl(self):
|
||||||
|
"""定时检查 Hyperliquid 持仓是否缺少止盈止损,缺少则从最新信号补救"""
|
||||||
|
if not self.hyperliquid:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
positions = self.hyperliquid.get_open_positions()
|
||||||
|
if not positions:
|
||||||
|
return
|
||||||
|
|
||||||
|
for pos in positions:
|
||||||
|
symbol = pos.get('symbol', '')
|
||||||
|
if not symbol:
|
||||||
|
continue
|
||||||
|
|
||||||
|
coin = symbol.replace('USDT', '')
|
||||||
|
tp_sl = self.hyperliquid.get_tp_sl_prices(coin)
|
||||||
|
has_tp = tp_sl.get('take_profit') is not None
|
||||||
|
has_sl = tp_sl.get('stop_loss') is not None
|
||||||
|
|
||||||
|
if has_tp and has_sl:
|
||||||
|
continue
|
||||||
|
|
||||||
|
latest_signal = self.signal_db.get_latest_signal('crypto', symbol)
|
||||||
|
if not latest_signal:
|
||||||
|
missing = ('止盈' if not has_tp else '') + ('/' if not has_tp and not has_sl else '') + ('止损' if not has_sl else '')
|
||||||
|
logger.warning(f"[Hyperliquid] ⚠️ {symbol} 缺少{missing},且无历史信号可补救")
|
||||||
|
continue
|
||||||
|
|
||||||
|
tp_price = latest_signal.get('take_profit')
|
||||||
|
sl_price = latest_signal.get('stop_loss')
|
||||||
|
|
||||||
|
if not tp_price and not sl_price:
|
||||||
|
logger.warning(f"[Hyperliquid] ⚠️ {symbol} 缺少止盈止损,最近信号也无 TP/SL")
|
||||||
|
continue
|
||||||
|
|
||||||
|
set_tp = tp_price if not has_tp else None
|
||||||
|
set_sl = sl_price if not has_sl else None
|
||||||
|
|
||||||
|
missing_parts = []
|
||||||
|
if not has_tp:
|
||||||
|
missing_parts.append(f"TP={set_tp}")
|
||||||
|
if not has_sl:
|
||||||
|
missing_parts.append(f"SL={set_sl}")
|
||||||
|
logger.warning(f"[Hyperliquid] 🔧 {symbol} 缺少 {' & '.join(missing_parts)},从信号补救...")
|
||||||
|
|
||||||
|
size = abs(pos.get('size', 0))
|
||||||
|
if size <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
tp_sl_result = self.hyperliquid.set_tp_sl(
|
||||||
|
symbol=coin,
|
||||||
|
is_long=pos.get('size', 0) > 0,
|
||||||
|
size=size,
|
||||||
|
tp_price=set_tp,
|
||||||
|
sl_price=set_sl,
|
||||||
|
)
|
||||||
|
|
||||||
|
tp_set = tp_sl_result.get('tp_set', False)
|
||||||
|
sl_set = tp_sl_result.get('sl_set', False)
|
||||||
|
if tp_set or sl_set:
|
||||||
|
set_parts = []
|
||||||
|
if tp_set:
|
||||||
|
set_parts.append(f"TP={set_tp}")
|
||||||
|
if sl_set:
|
||||||
|
set_parts.append(f"SL={set_sl}")
|
||||||
|
logger.info(f"[Hyperliquid] ✅ 补救成功: {symbol} {' & '.join(set_parts)}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[Hyperliquid] ⚠️ 补救失败: {tp_sl_result.get('errors') or tp_sl_result.get('error')}")
|
||||||
|
await self._maybe_alert_tp_sl_incomplete(
|
||||||
|
"Hyperliquid",
|
||||||
|
f"fallback:{symbol}",
|
||||||
|
self._build_pending_tp_sl_task(
|
||||||
|
symbol=coin,
|
||||||
|
is_long=pos.get('size', 0) > 0,
|
||||||
|
size=size,
|
||||||
|
tp_price=set_tp,
|
||||||
|
sl_price=set_sl,
|
||||||
|
retry_count=self.TP_SL_RETRY_ALERT_THRESHOLD,
|
||||||
|
),
|
||||||
|
str(tp_sl_result.get('errors') or tp_sl_result.get('error') or '兜底补设失败'),
|
||||||
|
force=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"[Hyperliquid] 止盈止损兜底检查异常: {e}")
|
||||||
|
|
||||||
def _calculate_hyperliquid_position_size(self, decision: Dict[str, Any], current_price: float) -> float:
|
def _calculate_hyperliquid_position_size(self, decision: Dict[str, Any], current_price: float) -> float:
|
||||||
"""
|
"""
|
||||||
计算 Hyperliquid 仓位大小(基于可用保证金和风控限制)
|
计算 Hyperliquid 仓位大小(基于可用保证金和风控限制)
|
||||||
@ -4202,89 +4712,6 @@ class CryptoAgent:
|
|||||||
if self.settings.dingtalk_enabled:
|
if self.settings.dingtalk_enabled:
|
||||||
await self.dingtalk.send_action_card(title, content)
|
await self.dingtalk.send_action_card(title, content)
|
||||||
|
|
||||||
async def _notify_signal_not_executed(
|
|
||||||
self,
|
|
||||||
market_signal: Dict[str, Any],
|
|
||||||
decision: Dict[str, Any],
|
|
||||||
current_price: float,
|
|
||||||
reason: str = "",
|
|
||||||
prefix: str = ""
|
|
||||||
):
|
|
||||||
"""发送有信号但未执行交易的通知"""
|
|
||||||
try:
|
|
||||||
symbol = market_signal.get('symbol')
|
|
||||||
account_type = "📊"
|
|
||||||
title_prefix = f"{prefix} " if prefix else ""
|
|
||||||
|
|
||||||
signal = self._get_signal_for_decision(market_signal, decision)
|
|
||||||
if not signal:
|
|
||||||
return
|
|
||||||
|
|
||||||
confidence = signal.get('confidence', 0)
|
|
||||||
entry_type = signal.get('entry_type', 'market')
|
|
||||||
entry_price = signal.get('entry_price', current_price)
|
|
||||||
signal_timeframe = signal.get('timeframe', signal.get('type', 'unknown'))
|
|
||||||
timeframe_map = {'short_term': '短线', 'medium_term': '趋势', 'long_term': '长线'}
|
|
||||||
timeframe_text = timeframe_map.get(signal_timeframe, signal_timeframe)
|
|
||||||
|
|
||||||
# 决策信息
|
|
||||||
decision_type = decision.get('decision', 'HOLD')
|
|
||||||
decision_reason = decision.get('reason', '')
|
|
||||||
decision_reasoning = decision.get('reasoning', '')
|
|
||||||
|
|
||||||
# 如果有外部传入的 reason(订单创建失败的具体原因),优先使用
|
|
||||||
if reason:
|
|
||||||
final_reason = reason
|
|
||||||
elif decision_reason:
|
|
||||||
final_reason = decision_reason
|
|
||||||
elif decision_reasoning:
|
|
||||||
final_reason = decision_reasoning
|
|
||||||
else:
|
|
||||||
final_reason = "未知原因"
|
|
||||||
|
|
||||||
# 方向图标
|
|
||||||
action = signal.get('action', 'wait')
|
|
||||||
if action == 'buy':
|
|
||||||
action_icon = '🟢'
|
|
||||||
action_text = '做多'
|
|
||||||
elif action == 'sell':
|
|
||||||
action_icon = '🔴'
|
|
||||||
action_text = '做空'
|
|
||||||
else:
|
|
||||||
action_icon = '➖'
|
|
||||||
action_text = '观望'
|
|
||||||
|
|
||||||
# 构建标题
|
|
||||||
title = f"{title_prefix}{account_type} {symbol} 信号未执行"
|
|
||||||
|
|
||||||
# 构建内容
|
|
||||||
content_parts = [
|
|
||||||
f"{action_icon} **信号**: {action_text} | {timeframe_text} | 📈 信心度: **{confidence}%**",
|
|
||||||
f"",
|
|
||||||
f"**入场方式**: {entry_type}",
|
|
||||||
f"**建议入场价**: ${entry_price:,.2f}" if isinstance(entry_price, (int, float)) else f"**建议入场价**: {entry_price}",
|
|
||||||
f"**当前价格**: ${current_price:,.2f}",
|
|
||||||
f"",
|
|
||||||
f"⚠️ **未执行原因**:",
|
|
||||||
f"{final_reason}",
|
|
||||||
]
|
|
||||||
|
|
||||||
content = "\n".join(content_parts)
|
|
||||||
|
|
||||||
# 发送通知
|
|
||||||
if self.settings.feishu_enabled:
|
|
||||||
await self.feishu.send_card(title, content, "orange")
|
|
||||||
if self.settings.telegram_enabled:
|
|
||||||
message = f"{title}\n\n{content}"
|
|
||||||
await self.telegram.send_message(message)
|
|
||||||
if self.settings.dingtalk_enabled:
|
|
||||||
await self.dingtalk.send_action_card(title, content)
|
|
||||||
|
|
||||||
logger.info(f" 📤 已发送信号未执行通知: {decision_type} - {final_reason[:50]}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"发送信号未执行通知失败: {e}")
|
|
||||||
|
|
||||||
async def analyze_once(self, symbol: str) -> Dict[str, Any]:
|
async def analyze_once(self, symbol: str) -> Dict[str, Any]:
|
||||||
"""单次分析并返回市场信号与平台执行预览"""
|
"""单次分析并返回市场信号与平台执行预览"""
|
||||||
data = self.exchange.get_multi_timeframe_data(symbol)
|
data = self.exchange.get_multi_timeframe_data(symbol)
|
||||||
@ -4365,6 +4792,7 @@ class CryptoAgent:
|
|||||||
'mode': 'LLM 驱动',
|
'mode': 'LLM 驱动',
|
||||||
'platform_halts': self.get_platform_halt_status(),
|
'platform_halts': self.get_platform_halt_status(),
|
||||||
'analysis_monitor': self._analysis_monitor,
|
'analysis_monitor': self._analysis_monitor,
|
||||||
|
'analysis_notifications': self._analysis_notification_state,
|
||||||
'last_signals': {
|
'last_signals': {
|
||||||
symbol: {
|
symbol: {
|
||||||
'type': sig.get('type'),
|
'type': sig.get('type'),
|
||||||
@ -4826,6 +5254,19 @@ class CryptoAgent:
|
|||||||
}
|
}
|
||||||
self._save_platform_halts()
|
self._save_platform_halts()
|
||||||
logger.warning(f"🛑 [{platform_name}] 已标记为平台熔断暂停")
|
logger.warning(f"🛑 [{platform_name}] 已标记为平台熔断暂停")
|
||||||
|
if self.settings.feishu_enabled and self.feishu_error:
|
||||||
|
asyncio.create_task(
|
||||||
|
self.feishu_error.send_card(
|
||||||
|
f"🛑 [{platform_name}] 平台已停机",
|
||||||
|
"\n".join([
|
||||||
|
f"**原因**: {reason}",
|
||||||
|
f"**回撤**: {drawdown_pct:.2f}%",
|
||||||
|
f"**当前权益**: ${current_balance:,.2f}",
|
||||||
|
f"**初始权益**: ${initial_balance:,.2f}",
|
||||||
|
]),
|
||||||
|
"red",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def get_platform_halt_status(self) -> Dict[str, Any]:
|
def get_platform_halt_status(self) -> Dict[str, Any]:
|
||||||
result = {}
|
result = {}
|
||||||
@ -4889,6 +5330,18 @@ class CryptoAgent:
|
|||||||
}
|
}
|
||||||
self._save_platform_halts()
|
self._save_platform_halts()
|
||||||
logger.info(f"✅ [{platform_name}] 已手动恢复,初始权益重置为 ${current_balance:.2f}")
|
logger.info(f"✅ [{platform_name}] 已手动恢复,初始权益重置为 ${current_balance:.2f}")
|
||||||
|
if self.settings.feishu_enabled and self.feishu_error:
|
||||||
|
asyncio.create_task(
|
||||||
|
self.feishu_error.send_card(
|
||||||
|
f"✅ [{platform_name}] 平台已恢复",
|
||||||
|
"\n".join([
|
||||||
|
f"**当前权益**: ${current_balance:,.2f}",
|
||||||
|
f"**重置初始权益**: ${current_balance:,.2f}",
|
||||||
|
f"**恢复时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
||||||
|
]),
|
||||||
|
"green",
|
||||||
|
)
|
||||||
|
)
|
||||||
return self._platform_halts[platform_name]
|
return self._platform_halts[platform_name]
|
||||||
|
|
||||||
# ==================== 初始余额持久化 ====================
|
# ==================== 初始余额持久化 ====================
|
||||||
@ -4953,7 +5406,9 @@ class CryptoAgent:
|
|||||||
"""发送告警通知(飞书/钉钉/Telegram)"""
|
"""发送告警通知(飞书/钉钉/Telegram)"""
|
||||||
try:
|
try:
|
||||||
# 飞书
|
# 飞书
|
||||||
if self.feishu:
|
if self.feishu_error:
|
||||||
|
await self.feishu_error.send_text(f"{title}\n\n{message}")
|
||||||
|
elif self.feishu:
|
||||||
await self.feishu.send_text(f"{title}\n\n{message}")
|
await self.feishu.send_text(f"{title}\n\n{message}")
|
||||||
|
|
||||||
# 钉钉
|
# 钉钉
|
||||||
|
|||||||
@ -12,6 +12,12 @@ from app.utils.logger import logger
|
|||||||
class BaseExecutor(ABC):
|
class BaseExecutor(ABC):
|
||||||
"""交易执行器基类"""
|
"""交易执行器基类"""
|
||||||
|
|
||||||
|
MIN_EFFECTIVE_LEVERAGE_BY_SIGNAL_TYPE = {
|
||||||
|
'short_term': 4.0,
|
||||||
|
'medium_term': 2.0,
|
||||||
|
'long_term': 2.0,
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, platform_name: str):
|
def __init__(self, platform_name: str):
|
||||||
self.platform_name = platform_name
|
self.platform_name = platform_name
|
||||||
|
|
||||||
@ -338,6 +344,30 @@ class BaseExecutor(ABC):
|
|||||||
|
|
||||||
return adjusted_margin
|
return adjusted_margin
|
||||||
|
|
||||||
|
def get_min_effective_leverage(self, decision: Dict[str, Any]) -> float:
|
||||||
|
signal_type = decision.get('timeframe') or decision.get('type') or 'medium_term'
|
||||||
|
return float(self.MIN_EFFECTIVE_LEVERAGE_BY_SIGNAL_TYPE.get(signal_type, 2.0))
|
||||||
|
|
||||||
|
def validate_effective_leverage(self,
|
||||||
|
decision: Dict[str, Any],
|
||||||
|
margin: float,
|
||||||
|
actual_position_value: float) -> tuple[bool, str, float]:
|
||||||
|
if margin <= 0:
|
||||||
|
return False, "保证金无效", 0.0
|
||||||
|
|
||||||
|
effective_leverage = actual_position_value / margin if margin > 0 else 0.0
|
||||||
|
min_effective_leverage = self.get_min_effective_leverage(decision)
|
||||||
|
|
||||||
|
if effective_leverage + 1e-9 < min_effective_leverage:
|
||||||
|
signal_type = decision.get('timeframe') or decision.get('type') or 'medium_term'
|
||||||
|
return (
|
||||||
|
False,
|
||||||
|
f"{signal_type} 实际有效杠杆 {effective_leverage:.2f}x < 最小要求 {min_effective_leverage:.1f}x",
|
||||||
|
effective_leverage,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True, "", effective_leverage
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_fee_rate(self) -> float:
|
def get_fee_rate(self) -> float:
|
||||||
"""
|
"""
|
||||||
@ -775,4 +805,3 @@ class BaseExecutor(ABC):
|
|||||||
color = "green" if success else "red"
|
color = "green" if success else "red"
|
||||||
|
|
||||||
await self.feishu.send_card(title, content, color)
|
await self.feishu.send_card(title, content, color)
|
||||||
|
|
||||||
|
|||||||
@ -40,6 +40,12 @@ class BitgetExecutor(BaseExecutor):
|
|||||||
|
|
||||||
# 计算合约张数,必须与实际执行杠杆保持一致
|
# 计算合约张数,必须与实际执行杠杆保持一致
|
||||||
contracts = self._calculate_contracts(symbol, adjusted_margin, entry_price, leverage)
|
contracts = self._calculate_contracts(symbol, adjusted_margin, entry_price, leverage)
|
||||||
|
actual_position_value = contracts * self.bitget.get_contract_size(symbol) * entry_price
|
||||||
|
leverage_ok, leverage_reason, effective_leverage = self.validate_effective_leverage(
|
||||||
|
decision,
|
||||||
|
adjusted_margin,
|
||||||
|
actual_position_value,
|
||||||
|
)
|
||||||
|
|
||||||
if contracts < 1:
|
if contracts < 1:
|
||||||
return {
|
return {
|
||||||
@ -49,6 +55,12 @@ class BitgetExecutor(BaseExecutor):
|
|||||||
f'(保证金=${adjusted_margin:.2f}, 杠杆={leverage}x)'
|
f'(保证金=${adjusted_margin:.2f}, 杠杆={leverage}x)'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if not leverage_ok:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': leverage_reason,
|
||||||
|
'effective_leverage': effective_leverage,
|
||||||
|
}
|
||||||
|
|
||||||
# 设置杠杆
|
# 设置杠杆
|
||||||
self.bitget.update_leverage(symbol, leverage)
|
self.bitget.update_leverage(symbol, leverage)
|
||||||
@ -128,21 +140,7 @@ class BitgetExecutor(BaseExecutor):
|
|||||||
|
|
||||||
logger.info(f" ✅ 开仓成功: {symbol} {contracts}张 @ ${order_type}")
|
logger.info(f" ✅ 开仓成功: {symbol} {contracts}张 @ ${order_type}")
|
||||||
|
|
||||||
# 发送飞书通知
|
# 开仓成功通知由 crypto_agent 统一发送,避免与执行摘要重复
|
||||||
await self.send_execution_notification(
|
|
||||||
operation='OPEN',
|
|
||||||
symbol=symbol,
|
|
||||||
result=result,
|
|
||||||
details={
|
|
||||||
'size': contracts,
|
|
||||||
'price': entry_price,
|
|
||||||
'margin': adjusted_margin,
|
|
||||||
'leverage': leverage,
|
|
||||||
'stop_loss': stop_loss,
|
|
||||||
'take_profit': take_profit,
|
|
||||||
'order_type': order_type
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@ -46,12 +46,24 @@ class HyperliquidExecutor(BaseExecutor):
|
|||||||
|
|
||||||
# 计算仓位大小
|
# 计算仓位大小
|
||||||
position_size = self._calculate_position_size(symbol, adjusted_margin, entry_price, leverage)
|
position_size = self._calculate_position_size(symbol, adjusted_margin, entry_price, leverage)
|
||||||
|
actual_position_value = position_size * entry_price
|
||||||
|
leverage_ok, leverage_reason, effective_leverage = self.validate_effective_leverage(
|
||||||
|
decision,
|
||||||
|
adjusted_margin,
|
||||||
|
actual_position_value,
|
||||||
|
)
|
||||||
|
|
||||||
if position_size <= 0:
|
if position_size <= 0:
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'仓位计算失败: {position_size}'
|
'error': f'仓位计算失败: {position_size}'
|
||||||
}
|
}
|
||||||
|
if not leverage_ok:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': leverage_reason,
|
||||||
|
'effective_leverage': effective_leverage,
|
||||||
|
}
|
||||||
|
|
||||||
# 设置杠杆
|
# 设置杠杆
|
||||||
self.hyperliquid.update_leverage(symbol, leverage)
|
self.hyperliquid.update_leverage(symbol, leverage)
|
||||||
@ -103,38 +115,44 @@ class HyperliquidExecutor(BaseExecutor):
|
|||||||
if tp_set and sl_set:
|
if tp_set and sl_set:
|
||||||
logger.info(f" ✅ 止盈止损已设置: TP={take_profit}, SL={stop_loss}")
|
logger.info(f" ✅ 止盈止损已设置: TP={take_profit}, SL={stop_loss}")
|
||||||
elif tp_set or sl_set:
|
elif tp_set or sl_set:
|
||||||
# 部分成功:记录缺失侧
|
# 部分成功:记录缺失侧,交给 agent 后续补设
|
||||||
set_text = "TP" if tp_set else "SL"
|
set_text = "TP" if tp_set else "SL"
|
||||||
fail_text = "TP" if not tp_set else "SL"
|
fail_text = "TP" if not tp_set else "SL"
|
||||||
|
result['pending_tp_sl'] = {
|
||||||
|
'tp_price': take_profit if not tp_set else None,
|
||||||
|
'sl_price': stop_loss if not sl_set else None,
|
||||||
|
}
|
||||||
|
result['position_size'] = position_size
|
||||||
logger.warning(f" ⚠️ 止盈止损部分成功: {set_text}已设, {fail_text}失败")
|
logger.warning(f" ⚠️ 止盈止损部分成功: {set_text}已设, {fail_text}失败")
|
||||||
result['tp_sl_warning'] = f"{fail_text}设置失败: {tp_sl_result.get('errors', [])}"
|
result['tp_sl_warning'] = f"{fail_text}设置失败: {tp_sl_result.get('errors', [])}"
|
||||||
else:
|
else:
|
||||||
errors = tp_sl_result.get('errors', [])
|
errors = tp_sl_result.get('errors', [])
|
||||||
|
result['pending_tp_sl'] = {
|
||||||
|
'tp_price': take_profit,
|
||||||
|
'sl_price': stop_loss,
|
||||||
|
}
|
||||||
|
result['position_size'] = position_size
|
||||||
logger.warning(f" ⚠️ 止盈止损设置失败: {errors}")
|
logger.warning(f" ⚠️ 止盈止损设置失败: {errors}")
|
||||||
result['tp_sl_warning'] = f"TP/SL设置失败: {'; '.join(errors)}"
|
result['tp_sl_warning'] = f"TP/SL设置失败: {'; '.join(errors)}"
|
||||||
except Exception as tp_sl_err:
|
except Exception as tp_sl_err:
|
||||||
logger.error(f" ⚠️ 止盈止损设置异常: {tp_sl_err}")
|
logger.error(f" ⚠️ 止盈止损设置异常: {tp_sl_err}")
|
||||||
|
result['pending_tp_sl'] = {
|
||||||
|
'tp_price': take_profit,
|
||||||
|
'sl_price': stop_loss,
|
||||||
|
}
|
||||||
|
result['position_size'] = position_size
|
||||||
result['tp_sl_warning'] = str(tp_sl_err)
|
result['tp_sl_warning'] = str(tp_sl_err)
|
||||||
else:
|
else:
|
||||||
# 限价单未成交,暂时跳过(等成交后再设)
|
# 限价单未成交:记录下来,等成交后自动补设
|
||||||
logger.info(f" 📌 限价单待成交,TP/SL 将在成交后设置: TP={take_profit}, SL={stop_loss}")
|
result['pending_tp_sl'] = {
|
||||||
result['tp_sl_warning'] = "限价单未成交,TP/SL 待成交后设置"
|
'tp_price': take_profit,
|
||||||
|
'sl_price': stop_loss,
|
||||||
|
}
|
||||||
|
result['position_size'] = position_size
|
||||||
|
logger.info(f" 📌 限价单待成交,TP/SL 将在成交后自动设置: TP={take_profit}, SL={stop_loss}")
|
||||||
|
result['tp_sl_warning'] = "限价单未成交,TP/SL 已加入待补设列表"
|
||||||
|
|
||||||
# 发送飞书通知(在止盈止损之后,通知失败不影响交易结果)
|
# 开仓成功通知由 crypto_agent 统一发送,避免与执行摘要重复
|
||||||
await self.send_execution_notification(
|
|
||||||
operation='OPEN',
|
|
||||||
symbol=symbol,
|
|
||||||
result=result,
|
|
||||||
details={
|
|
||||||
'size': position_size,
|
|
||||||
'price': entry_price,
|
|
||||||
'margin': adjusted_margin,
|
|
||||||
'leverage': leverage,
|
|
||||||
'stop_loss': stop_loss,
|
|
||||||
'take_profit': take_profit,
|
|
||||||
'order_type': order_type
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -228,10 +246,15 @@ class HyperliquidExecutor(BaseExecutor):
|
|||||||
position_size: float) -> Dict[str, Any]:
|
position_size: float) -> Dict[str, Any]:
|
||||||
"""设置止盈止损"""
|
"""设置止盈止损"""
|
||||||
try:
|
try:
|
||||||
# Hyperliquid 的 TP/SL 设置方式可能需要查文档
|
positions = self.hyperliquid.get_open_positions()
|
||||||
# 这里假设有类似的方法
|
pos = next((p for p in positions if p.get('coin') == symbol.replace('USDT', '')), None)
|
||||||
|
|
||||||
|
if not pos:
|
||||||
|
return {'success': False, 'message': f'找不到 {symbol} 的持仓'}
|
||||||
|
|
||||||
result = self.hyperliquid.set_tp_sl(
|
result = self.hyperliquid.set_tp_sl(
|
||||||
symbol=symbol.replace('USDT', ''),
|
symbol=symbol.replace('USDT', ''),
|
||||||
|
is_long=pos['size'] > 0,
|
||||||
size=position_size,
|
size=position_size,
|
||||||
tp_price=take_profit,
|
tp_price=take_profit,
|
||||||
sl_price=stop_loss
|
sl_price=stop_loss
|
||||||
|
|||||||
@ -33,6 +33,18 @@ class PaperTradingExecutor(BaseExecutor):
|
|||||||
|
|
||||||
# 调整保证金(模拟盘无手续费)
|
# 调整保证金(模拟盘无手续费)
|
||||||
adjusted_margin = margin
|
adjusted_margin = margin
|
||||||
|
actual_position_value = adjusted_margin * self.paper_trading.leverage
|
||||||
|
leverage_ok, leverage_reason, effective_leverage = self.validate_effective_leverage(
|
||||||
|
decision,
|
||||||
|
adjusted_margin,
|
||||||
|
actual_position_value,
|
||||||
|
)
|
||||||
|
if not leverage_ok:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'error': leverage_reason,
|
||||||
|
'effective_leverage': effective_leverage,
|
||||||
|
}
|
||||||
|
|
||||||
# 根据 confidence 推算信号等级
|
# 根据 confidence 推算信号等级
|
||||||
confidence = decision.get('confidence', 0)
|
confidence = decision.get('confidence', 0)
|
||||||
|
|||||||
@ -294,6 +294,7 @@ _feishu_crypto_service: Optional[FeishuService] = None
|
|||||||
_feishu_stock_service: Optional[FeishuService] = None
|
_feishu_stock_service: Optional[FeishuService] = None
|
||||||
_feishu_news_service: Optional[FeishuService] = None
|
_feishu_news_service: Optional[FeishuService] = None
|
||||||
_feishu_paper_trading_service: Optional[FeishuService] = None
|
_feishu_paper_trading_service: Optional[FeishuService] = None
|
||||||
|
_feishu_error_service: Optional[FeishuService] = None
|
||||||
|
|
||||||
|
|
||||||
def get_feishu_service() -> FeishuService:
|
def get_feishu_service() -> FeishuService:
|
||||||
@ -331,3 +332,11 @@ def get_feishu_paper_trading_service() -> FeishuService:
|
|||||||
if _feishu_paper_trading_service is None:
|
if _feishu_paper_trading_service is None:
|
||||||
_feishu_paper_trading_service = FeishuService(service_type="paper_trading")
|
_feishu_paper_trading_service = FeishuService(service_type="paper_trading")
|
||||||
return _feishu_paper_trading_service
|
return _feishu_paper_trading_service
|
||||||
|
|
||||||
|
|
||||||
|
def get_feishu_error_service() -> FeishuService:
|
||||||
|
"""获取系统异常飞书服务实例"""
|
||||||
|
global _feishu_error_service
|
||||||
|
if _feishu_error_service is None:
|
||||||
|
_feishu_error_service = FeishuService(service_type="error")
|
||||||
|
return _feishu_error_service
|
||||||
|
|||||||
@ -115,6 +115,7 @@ def calculate_margin_and_position_value(
|
|||||||
target_margin_pct: float,
|
target_margin_pct: float,
|
||||||
max_margin_pct: float,
|
max_margin_pct: float,
|
||||||
min_margin: float = 0.0,
|
min_margin: float = 0.0,
|
||||||
|
min_effective_leverage: float = 1.0,
|
||||||
reserve_ratio: float = 0.05,
|
reserve_ratio: float = 0.05,
|
||||||
) -> Tuple[float, float, str]:
|
) -> Tuple[float, float, str]:
|
||||||
if balance <= 0:
|
if balance <= 0:
|
||||||
@ -123,6 +124,13 @@ def calculate_margin_and_position_value(
|
|||||||
return 0.0, 0.0, "可用保证金不足"
|
return 0.0, 0.0, "可用保证金不足"
|
||||||
if order_leverage <= 0:
|
if order_leverage <= 0:
|
||||||
return 0.0, 0.0, "订单杠杆无效"
|
return 0.0, 0.0, "订单杠杆无效"
|
||||||
|
if min_effective_leverage <= 0:
|
||||||
|
min_effective_leverage = 1.0
|
||||||
|
if order_leverage < min_effective_leverage:
|
||||||
|
return 0.0, 0.0, (
|
||||||
|
f"订单杠杆 {order_leverage:.1f}x 低于最小有效杠杆 "
|
||||||
|
f"{min_effective_leverage:.1f}x"
|
||||||
|
)
|
||||||
if target_margin_pct <= 0:
|
if target_margin_pct <= 0:
|
||||||
return 0.0, 0.0, "目标保证金比例无效"
|
return 0.0, 0.0, "目标保证金比例无效"
|
||||||
|
|
||||||
|
|||||||
@ -1093,6 +1093,105 @@
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.runtime-summary-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-card {
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255,255,255,0.03);
|
||||||
|
border: 1px solid rgba(255,255,255,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-title {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-main {
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--text);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.runtime-summary-row strong {
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-item {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: rgba(255, 184, 77, 0.08);
|
||||||
|
border: 1px solid rgba(255, 184, 77, 0.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-item-head {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-item-title {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-item-meta {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
font-family: "IBM Plex Mono", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-platforms {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-platform {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocked-platform strong {
|
||||||
|
color: var(--text);
|
||||||
|
min-width: 92px;
|
||||||
|
}
|
||||||
|
|
||||||
.analysis-log-item {
|
.analysis-log-item {
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
@ -1505,6 +1604,18 @@
|
|||||||
<div class="heartbeat-card"><span class="label">当前进度</span><span class="value">-</span></div>
|
<div class="heartbeat-card"><span class="label">当前进度</span><span class="value">-</span></div>
|
||||||
<div class="heartbeat-card"><span class="label">下一次运行</span><span class="value">-</span></div>
|
<div class="heartbeat-card"><span class="label">下一次运行</span><span class="value">-</span></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="runtime-summary-grid">
|
||||||
|
<div class="runtime-summary-card" id="runtimeSummaryCard">
|
||||||
|
<div class="runtime-summary-title">运行摘要</div>
|
||||||
|
<div class="runtime-summary-main">正在整理分析状态...</div>
|
||||||
|
</div>
|
||||||
|
<div class="runtime-summary-card">
|
||||||
|
<div class="runtime-summary-title">最近阻塞原因</div>
|
||||||
|
<div class="blocked-list" id="blockedSummaryList">
|
||||||
|
<div class="loading">正在读取未落单汇总...</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="analysis-log-list" id="analysisLogList">
|
<div class="analysis-log-list" id="analysisLogList">
|
||||||
<div class="loading">正在读取分析日志...</div>
|
<div class="loading">正在读取分析日志...</div>
|
||||||
</div>
|
</div>
|
||||||
@ -2150,7 +2261,9 @@
|
|||||||
function renderAnalysisHeartbeat(analysisMonitor, analysisEvents) {
|
function renderAnalysisHeartbeat(analysisMonitor, analysisEvents) {
|
||||||
const heartbeat = document.getElementById('analysisHeartbeat');
|
const heartbeat = document.getElementById('analysisHeartbeat');
|
||||||
const logList = document.getElementById('analysisLogList');
|
const logList = document.getElementById('analysisLogList');
|
||||||
|
const summaryCard = document.getElementById('runtimeSummaryCard');
|
||||||
const monitor = analysisMonitor || {};
|
const monitor = analysisMonitor || {};
|
||||||
|
const notifications = cachedConsoleData?.crypto_agent?.analysis_notifications || {};
|
||||||
const cycleStatus = monitor.last_cycle_status || 'idle';
|
const cycleStatus = monitor.last_cycle_status || 'idle';
|
||||||
const progressText = monitor.current_cycle_total
|
const progressText = monitor.current_cycle_total
|
||||||
? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total} ${monitor.current_cycle_symbol || ''}`
|
? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total} ${monitor.current_cycle_symbol || ''}`
|
||||||
@ -2175,6 +2288,19 @@
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const heartbeatSentAt = notifications.last_heartbeat_notified_at;
|
||||||
|
const lastSignalAt = notifications.last_signal_at;
|
||||||
|
const lastSignalSymbol = notifications.last_signal_symbol || '-';
|
||||||
|
summaryCard.innerHTML = `
|
||||||
|
<div class="runtime-summary-title">运行摘要</div>
|
||||||
|
<div class="runtime-summary-main">${monitor.last_analysis_status || 'idle'} / ${monitor.last_analysis_symbol || '-'}</div>
|
||||||
|
<div class="runtime-summary-meta">
|
||||||
|
<div class="runtime-summary-row"><span>最近分析说明</span><strong>${monitor.last_analysis_detail || '-'}</strong></div>
|
||||||
|
<div class="runtime-summary-row"><span>最近信号</span><strong>${lastSignalAt ? `${lastSignalSymbol} / ${relativeTime(lastSignalAt)}` : '近 60 分钟无信号'}</strong></div>
|
||||||
|
<div class="runtime-summary-row"><span>上次心跳通知</span><strong>${heartbeatSentAt ? `${relativeTime(heartbeatSentAt)} / ${formatTime(heartbeatSentAt)}` : '尚未发送'}</strong></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
if (!analysisEvents || analysisEvents.length === 0) {
|
if (!analysisEvents || analysisEvents.length === 0) {
|
||||||
logList.innerHTML = compactEmpty('最近还没有分析日志', '等待下一轮分析或新的运行事件写入。');
|
logList.innerHTML = compactEmpty('最近还没有分析日志', '等待下一轮分析或新的运行事件写入。');
|
||||||
return;
|
return;
|
||||||
@ -2191,6 +2317,39 @@
|
|||||||
`).join('');
|
`).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBlockedSummaries(events = cachedExecutionEvents) {
|
||||||
|
const container = document.getElementById('blockedSummaryList');
|
||||||
|
const blockedEvents = (Array.isArray(events) ? events : [])
|
||||||
|
.filter((event) => event.event_type === 'execution_blocked_summary')
|
||||||
|
.slice(0, 3);
|
||||||
|
|
||||||
|
if (!blockedEvents.length) {
|
||||||
|
container.innerHTML = compactEmpty('最近没有未落单阻塞', '出现执行阻塞时,这里会按平台归类展示原因。');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.innerHTML = blockedEvents.map((event) => {
|
||||||
|
const blockedPlatforms = event.blocked_platforms || [];
|
||||||
|
return `
|
||||||
|
<div class="blocked-item">
|
||||||
|
<div class="blocked-item-head">
|
||||||
|
<div class="blocked-item-title">${event.symbol || '-'} · ${event.signal_action_text || '-'} / ${event.signal_timeframe_text || '-'}</div>
|
||||||
|
<div class="blocked-item-meta">${relativeTime(event.timestamp)} / ${formatTime(event.timestamp)}</div>
|
||||||
|
</div>
|
||||||
|
<div class="analysis-log-detail">建议价 ${formatMoney(event.entry_price)} / 现价 ${formatMoney(event.current_price)} / 信心 ${formatPercent(event.confidence || 0, 1)}</div>
|
||||||
|
<div class="blocked-platforms">
|
||||||
|
${blockedPlatforms.map((item) => `
|
||||||
|
<div class="blocked-platform">
|
||||||
|
<strong>${item.platform}</strong>
|
||||||
|
<span>${item.tag} | ${item.detail}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeDecision(decision) {
|
function summarizeDecision(decision) {
|
||||||
if (!decision) return { label: '-', detail: '无数据', tone: 'hold' };
|
if (!decision) return { label: '-', detail: '无数据', tone: 'hold' };
|
||||||
const decisionType = decision.decision || decision.action || 'HOLD';
|
const decisionType = decision.decision || decision.action || 'HOLD';
|
||||||
@ -2307,8 +2466,19 @@
|
|||||||
<strong>${event.symbol || '-'}</strong>
|
<strong>${event.symbol || '-'}</strong>
|
||||||
${event.decision ? `<span class="event-inline-badge">${event.decision}</span>` : ''}
|
${event.decision ? `<span class="event-inline-badge">${event.decision}</span>` : ''}
|
||||||
${event.action ? `<span class="event-inline-badge">${event.action}</span>` : ''}
|
${event.action ? `<span class="event-inline-badge">${event.action}</span>` : ''}
|
||||||
|
${event.signal_timeframe_text ? `<span class="event-inline-badge">${event.signal_timeframe_text}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
<span style="color: var(--muted);">${event.reason || '无说明'}</span>
|
<span style="color: var(--muted);">${event.reason || '无说明'}</span>
|
||||||
|
${event.event_type === 'execution_blocked_summary' && Array.isArray(event.blocked_platforms) && event.blocked_platforms.length > 0 ? `
|
||||||
|
<div class="blocked-platforms" style="margin-top: 10px;">
|
||||||
|
${event.blocked_platforms.map((item) => `
|
||||||
|
<div class="blocked-platform">
|
||||||
|
<strong>${item.platform}</strong>
|
||||||
|
<span>${item.tag} | ${item.detail}</span>
|
||||||
|
</div>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : ''}
|
||||||
${(event.reason || '').length > 90 ? `
|
${(event.reason || '').length > 90 ? `
|
||||||
<details class="event-details">
|
<details class="event-details">
|
||||||
<summary>查看完整详情</summary>
|
<summary>查看完整详情</summary>
|
||||||
@ -2495,6 +2665,7 @@
|
|||||||
renderDecisionPreview(data.crypto_agent?.last_execution_preview || {});
|
renderDecisionPreview(data.crypto_agent?.last_execution_preview || {});
|
||||||
renderHalts(data.crypto_agent?.platform_halts || {});
|
renderHalts(data.crypto_agent?.platform_halts || {});
|
||||||
renderExecutionEvents(data.execution_events || []);
|
renderExecutionEvents(data.execution_events || []);
|
||||||
|
renderBlockedSummaries(data.execution_events || []);
|
||||||
renderAttentionItems(data.management?.attention_items || []);
|
renderAttentionItems(data.management?.attention_items || []);
|
||||||
renderUnifiedPositions(data.management?.positions || []);
|
renderUnifiedPositions(data.management?.positions || []);
|
||||||
renderUnifiedOrders(data.management?.orders || []);
|
renderUnifiedOrders(data.management?.orders || []);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user