diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py
index 2751aa8..7e818d0 100644
--- a/backend/app/crypto_agent/crypto_agent.py
+++ b/backend/app/crypto_agent/crypto_agent.py
@@ -200,6 +200,23 @@ class CryptoAgent:
self._platform_halts: Dict[str, Dict[str, Any]] = {}
self._load_platform_halts()
self._execution_events: deque[Dict[str, Any]] = deque(maxlen=120)
+ self._analysis_events: deque[Dict[str, Any]] = deque(maxlen=240)
+ self._analysis_monitor: Dict[str, Any] = {
+ "last_heartbeat_at": None,
+ "last_cycle_started_at": None,
+ "last_cycle_completed_at": None,
+ "last_cycle_status": "idle",
+ "last_cycle_error": "",
+ "current_cycle_symbol": None,
+ "current_cycle_index": 0,
+ "current_cycle_total": 0,
+ "last_analysis_started_at": None,
+ "last_analysis_completed_at": None,
+ "last_analysis_symbol": None,
+ "last_analysis_status": "idle",
+ "last_analysis_detail": "",
+ "next_scheduled_run_at": None,
+ }
# 挂单 TP/SL 追踪:挂单成交后自动补设止盈止损
# key=order_id, value={symbol, is_long, size/contracts, tp_price, sl_price}
@@ -262,6 +279,30 @@ class CryptoAgent:
def get_recent_execution_events(self, limit: int = 30) -> List[Dict[str, Any]]:
return list(self._execution_events)[:limit]
+ def _touch_analysis_heartbeat(self):
+ self._analysis_monitor["last_heartbeat_at"] = datetime.now().isoformat()
+
+ def _record_analysis_event(self,
+ event_type: str,
+ symbol: str = "",
+ status: str = "info",
+ detail: str = "",
+ extra: Optional[Dict[str, Any]] = None):
+ event = {
+ "timestamp": datetime.now().isoformat(),
+ "event_type": event_type,
+ "status": status,
+ "symbol": symbol,
+ "detail": detail,
+ }
+ if extra:
+ event.update(extra)
+ self._analysis_events.appendleft(event)
+ self._touch_analysis_heartbeat()
+
+ def get_recent_analysis_events(self, limit: int = 40) -> List[Dict[str, Any]]:
+ return list(self._analysis_events)[:limit]
+
def _on_price_update(self, symbol: str, price: float):
"""处理实时价格更新(用于模拟交易)"""
if not self.paper_trading:
@@ -532,10 +573,26 @@ class CryptoAgent:
wait_seconds = self._get_seconds_until_next_5min()
if wait_seconds > 0:
next_run = datetime.now() + timedelta(seconds=wait_seconds)
+ self._analysis_monitor["next_scheduled_run_at"] = next_run.isoformat()
+ self._analysis_monitor["last_cycle_status"] = "waiting"
+ self._touch_analysis_heartbeat()
logger.info(f"⏳ 等待 {wait_seconds} 秒,下次运行: {next_run.strftime('%H:%M:%S')}")
await asyncio.sleep(wait_seconds)
run_time = datetime.now()
+ self._analysis_monitor["last_cycle_started_at"] = run_time.isoformat()
+ self._analysis_monitor["last_cycle_status"] = "running"
+ self._analysis_monitor["last_cycle_error"] = ""
+ self._analysis_monitor["current_cycle_symbol"] = None
+ self._analysis_monitor["current_cycle_index"] = 0
+ self._analysis_monitor["current_cycle_total"] = len(self.symbols)
+ self._analysis_monitor["next_scheduled_run_at"] = None
+ self._record_analysis_event(
+ "cycle_started",
+ status="info",
+ detail=f"新一轮分析开始,计划扫描 {len(self.symbols)} 个交易对",
+ extra={"symbols": list(self.symbols)},
+ )
logger.info("\n" + "=" * 60)
logger.info(f"⏰ 定时任务执行 [{run_time.strftime('%Y-%m-%d %H:%M:%S')}]")
logger.info("=" * 60)
@@ -567,9 +624,20 @@ class CryptoAgent:
await self._check_and_set_pending_tp_sl_bitget()
await self._check_bitget_missing_tp_sl() # 兜底:检查缺少的 TP/SL 并补救
- for symbol in self.symbols:
+ for index, symbol in enumerate(self.symbols, start=1):
+ self._analysis_monitor["current_cycle_symbol"] = symbol
+ self._analysis_monitor["current_cycle_index"] = index
+ self._touch_analysis_heartbeat()
await self.analyze_symbol(symbol)
+ self._analysis_monitor["last_cycle_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_cycle_status"] = "completed"
+ self._analysis_monitor["current_cycle_symbol"] = None
+ self._record_analysis_event(
+ "cycle_completed",
+ status="success",
+ detail=f"本轮分析完成,共扫描 {len(self.symbols)} 个交易对",
+ )
logger.info("\n" + "─" * 60)
logger.info(f"✅ 本轮分析完成,共分析 {len(self.symbols)} 个交易对")
logger.info("─" * 60 + "\n")
@@ -577,6 +645,14 @@ class CryptoAgent:
await asyncio.sleep(2)
except Exception as e:
+ self._analysis_monitor["last_cycle_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_cycle_status"] = "error"
+ self._analysis_monitor["last_cycle_error"] = str(e)
+ self._record_analysis_event(
+ "cycle_error",
+ status="error",
+ detail=f"分析主循环异常: {str(e)}",
+ )
logger.error(f"❌ 分析循环出错: {e}")
import traceback
logger.error(traceback.format_exc())
@@ -695,6 +771,16 @@ class CryptoAgent:
# 更新活动时间
monitor = get_system_monitor()
monitor.update_activity("crypto_agent")
+ self._analysis_monitor["last_analysis_started_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_analysis_symbol"] = symbol
+ self._analysis_monitor["last_analysis_status"] = "running"
+ self._analysis_monitor["last_analysis_detail"] = "开始获取数据"
+ self._record_analysis_event(
+ "symbol_analysis_started",
+ symbol=symbol,
+ status="info",
+ detail="开始分析",
+ )
logger.info(f"\n{'─' * 50}")
logger.info(f"📊 {symbol} 分析开始")
@@ -704,6 +790,15 @@ class CryptoAgent:
data = self.exchange.get_multi_timeframe_data(symbol)
if not self._validate_data(data):
+ self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_analysis_status"] = "warning"
+ self._analysis_monitor["last_analysis_detail"] = "数据不完整,跳过分析"
+ self._record_analysis_event(
+ "symbol_analysis_skipped",
+ symbol=symbol,
+ status="warning",
+ detail="数据不完整,跳过分析",
+ )
logger.warning(f"⚠️ {symbol} 数据不完整,跳过分析")
return
@@ -715,6 +810,16 @@ class CryptoAgent:
# 1.5. 波动率检查(节省 LLM 调用)
should_analyze, volatility_reason, volatility = self._check_volatility(symbol, data)
if not should_analyze:
+ self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_analysis_status"] = "skipped"
+ self._analysis_monitor["last_analysis_detail"] = volatility_reason
+ self._record_analysis_event(
+ "symbol_analysis_skipped",
+ symbol=symbol,
+ status="hold",
+ detail=volatility_reason,
+ extra={"volatility_percent": volatility},
+ )
logger.info(f"⏸️ {volatility_reason},跳过本次 LLM 分析")
return
@@ -746,6 +851,16 @@ class CryptoAgent:
trade_signals = [s for s in signals if s.get('action') in ['buy', 'sell']]
if not trade_signals:
+ self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_analysis_status"] = "completed"
+ self._analysis_monitor["last_analysis_detail"] = "完成分析,无交易信号"
+ self._record_analysis_event(
+ "symbol_analysis_completed",
+ symbol=symbol,
+ status="success",
+ detail="完成分析,无交易信号",
+ extra={"trade_signals": 0, "valid_signals": 0},
+ )
logger.info(f"\n⏸️ 结论: 无交易信号(仅有观望建议),继续观望")
return
@@ -754,6 +869,16 @@ class CryptoAgent:
valid_signals = [s for s in trade_signals if s.get('confidence', 0) >= threshold]
if not valid_signals:
+ self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_analysis_status"] = "completed"
+ self._analysis_monitor["last_analysis_detail"] = f"完成分析,但无信号达到阈值 {threshold}%"
+ self._record_analysis_event(
+ "symbol_analysis_completed",
+ symbol=symbol,
+ status="success",
+ detail=f"完成分析,但无信号达到阈值 {threshold}%",
+ extra={"trade_signals": len(trade_signals), "valid_signals": 0},
+ )
logger.info(f"\n⏸️ 结论: 无交易信号达到置信度阈值 ({threshold}%),继续观望")
return
@@ -858,8 +983,27 @@ class CryptoAgent:
# 第三阶段:执行交易动作(各平台独立)
# ============================================================
await self._execute_decisions(paper_decision, hl_decision, bg_decision, market_signal, current_price)
+ self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_analysis_status"] = "completed"
+ self._analysis_monitor["last_analysis_detail"] = f"完成分析,产生 {len(valid_signals)} 个有效信号"
+ self._record_analysis_event(
+ "symbol_analysis_completed",
+ symbol=symbol,
+ status="success",
+ detail=f"完成分析,产生 {len(valid_signals)} 个有效信号",
+ extra={"trade_signals": len(trade_signals), "valid_signals": len(valid_signals)},
+ )
except Exception as e:
+ self._analysis_monitor["last_analysis_completed_at"] = datetime.now().isoformat()
+ self._analysis_monitor["last_analysis_status"] = "error"
+ self._analysis_monitor["last_analysis_detail"] = str(e)
+ self._record_analysis_event(
+ "symbol_analysis_error",
+ symbol=symbol,
+ status="error",
+ detail=str(e),
+ )
logger.error(f"❌ 分析 {symbol} 出错: {e}")
import traceback
logger.error(traceback.format_exc())
@@ -4220,6 +4364,7 @@ class CryptoAgent:
'symbols': self.symbols,
'mode': 'LLM 驱动',
'platform_halts': self.get_platform_halt_status(),
+ 'analysis_monitor': self._analysis_monitor,
'last_signals': {
symbol: {
'type': sig.get('type'),
@@ -4230,6 +4375,7 @@ class CryptoAgent:
for symbol, sig in self.last_signals.items()
},
'last_execution_preview': self.last_execution_preview,
+ 'recent_analysis_events': self.get_recent_analysis_events(limit=30),
}
async def _notify_expired_orders_cancelled(self, cancelled_orders: List):
diff --git a/frontend/console.html b/frontend/console.html
index 7f5f3e2..b9b44db 100644
--- a/frontend/console.html
+++ b/frontend/console.html
@@ -723,6 +723,85 @@
font-family: "IBM Plex Mono", monospace;
}
+ .heartbeat-grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+ margin-bottom: 14px;
+ }
+
+ .heartbeat-card {
+ padding: 12px;
+ border-radius: 12px;
+ background: rgba(255,255,255,0.03);
+ border: 1px solid rgba(255,255,255,0.05);
+ }
+
+ .heartbeat-card .label {
+ display: block;
+ color: var(--muted);
+ font-size: 11px;
+ margin-bottom: 6px;
+ }
+
+ .heartbeat-card .value {
+ font-size: 13px;
+ font-family: "IBM Plex Mono", monospace;
+ color: var(--text);
+ }
+
+ .analysis-log-list {
+ display: grid;
+ gap: 10px;
+ }
+
+ .analysis-log-item {
+ padding: 12px 14px;
+ border-radius: 14px;
+ background: rgba(255,255,255,0.03);
+ border: 1px solid rgba(255,255,255,0.05);
+ }
+
+ .analysis-log-item.error {
+ border-color: rgba(255, 111, 97, 0.2);
+ background: rgba(255, 111, 97, 0.08);
+ }
+
+ .analysis-log-item.warning,
+ .analysis-log-item.hold {
+ border-color: rgba(255, 184, 77, 0.2);
+ background: rgba(255, 184, 77, 0.08);
+ }
+
+ .analysis-log-item.success {
+ border-color: rgba(48, 209, 88, 0.18);
+ background: rgba(48, 209, 88, 0.08);
+ }
+
+ .analysis-log-head {
+ display: flex;
+ justify-content: space-between;
+ gap: 12px;
+ align-items: center;
+ margin-bottom: 6px;
+ }
+
+ .analysis-log-head strong {
+ font-size: 13px;
+ }
+
+ .analysis-log-meta {
+ color: var(--muted);
+ font-size: 11px;
+ font-family: "IBM Plex Mono", monospace;
+ }
+
+ .analysis-log-detail {
+ color: var(--muted);
+ font-size: 12px;
+ line-height: 1.6;
+ }
+
.ops-grid {
display: grid;
gap: 18px;
@@ -920,7 +999,8 @@
.hero-metrics,
.platform-stats,
- .signal-stats {
+ .signal-stats,
+ .heartbeat-grid {
grid-template-columns: 1fr;
}
@@ -1023,6 +1103,24 @@
+
+
+
+
最近心跳-
+
最近轮次-
+
当前进度-
+
下一次运行-
+
+
+
+