1
This commit is contained in:
parent
b1e6215ae2
commit
19780cf210
@ -200,6 +200,23 @@ class CryptoAgent:
|
|||||||
self._platform_halts: Dict[str, Dict[str, Any]] = {}
|
self._platform_halts: Dict[str, Dict[str, Any]] = {}
|
||||||
self._load_platform_halts()
|
self._load_platform_halts()
|
||||||
self._execution_events: deque[Dict[str, Any]] = deque(maxlen=120)
|
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 追踪:挂单成交后自动补设止盈止损
|
# 挂单 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}
|
||||||
@ -262,6 +279,30 @@ class CryptoAgent:
|
|||||||
def get_recent_execution_events(self, limit: int = 30) -> List[Dict[str, Any]]:
|
def get_recent_execution_events(self, limit: int = 30) -> List[Dict[str, Any]]:
|
||||||
return list(self._execution_events)[:limit]
|
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):
|
def _on_price_update(self, symbol: str, price: float):
|
||||||
"""处理实时价格更新(用于模拟交易)"""
|
"""处理实时价格更新(用于模拟交易)"""
|
||||||
if not self.paper_trading:
|
if not self.paper_trading:
|
||||||
@ -532,10 +573,26 @@ class CryptoAgent:
|
|||||||
wait_seconds = self._get_seconds_until_next_5min()
|
wait_seconds = self._get_seconds_until_next_5min()
|
||||||
if wait_seconds > 0:
|
if wait_seconds > 0:
|
||||||
next_run = datetime.now() + timedelta(seconds=wait_seconds)
|
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')}")
|
logger.info(f"⏳ 等待 {wait_seconds} 秒,下次运行: {next_run.strftime('%H:%M:%S')}")
|
||||||
await asyncio.sleep(wait_seconds)
|
await asyncio.sleep(wait_seconds)
|
||||||
|
|
||||||
run_time = datetime.now()
|
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("\n" + "=" * 60)
|
||||||
logger.info(f"⏰ 定时任务执行 [{run_time.strftime('%Y-%m-%d %H:%M:%S')}]")
|
logger.info(f"⏰ 定时任务执行 [{run_time.strftime('%Y-%m-%d %H:%M:%S')}]")
|
||||||
logger.info("=" * 60)
|
logger.info("=" * 60)
|
||||||
@ -567,9 +624,20 @@ class CryptoAgent:
|
|||||||
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 并补救
|
||||||
|
|
||||||
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)
|
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("\n" + "─" * 60)
|
||||||
logger.info(f"✅ 本轮分析完成,共分析 {len(self.symbols)} 个交易对")
|
logger.info(f"✅ 本轮分析完成,共分析 {len(self.symbols)} 个交易对")
|
||||||
logger.info("─" * 60 + "\n")
|
logger.info("─" * 60 + "\n")
|
||||||
@ -577,6 +645,14 @@ class CryptoAgent:
|
|||||||
await asyncio.sleep(2)
|
await asyncio.sleep(2)
|
||||||
|
|
||||||
except Exception as e:
|
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}")
|
logger.error(f"❌ 分析循环出错: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@ -695,6 +771,16 @@ class CryptoAgent:
|
|||||||
# 更新活动时间
|
# 更新活动时间
|
||||||
monitor = get_system_monitor()
|
monitor = get_system_monitor()
|
||||||
monitor.update_activity("crypto_agent")
|
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"\n{'─' * 50}")
|
||||||
logger.info(f"📊 {symbol} 分析开始")
|
logger.info(f"📊 {symbol} 分析开始")
|
||||||
@ -704,6 +790,15 @@ class CryptoAgent:
|
|||||||
data = self.exchange.get_multi_timeframe_data(symbol)
|
data = self.exchange.get_multi_timeframe_data(symbol)
|
||||||
|
|
||||||
if not self._validate_data(data):
|
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} 数据不完整,跳过分析")
|
logger.warning(f"⚠️ {symbol} 数据不完整,跳过分析")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -715,6 +810,16 @@ class CryptoAgent:
|
|||||||
# 1.5. 波动率检查(节省 LLM 调用)
|
# 1.5. 波动率检查(节省 LLM 调用)
|
||||||
should_analyze, volatility_reason, volatility = self._check_volatility(symbol, data)
|
should_analyze, volatility_reason, volatility = self._check_volatility(symbol, data)
|
||||||
if not should_analyze:
|
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 分析")
|
logger.info(f"⏸️ {volatility_reason},跳过本次 LLM 分析")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -746,6 +851,16 @@ class CryptoAgent:
|
|||||||
trade_signals = [s for s in signals if s.get('action') in ['buy', 'sell']]
|
trade_signals = [s for s in signals if s.get('action') in ['buy', 'sell']]
|
||||||
|
|
||||||
if not trade_signals:
|
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⏸️ 结论: 无交易信号(仅有观望建议),继续观望")
|
logger.info(f"\n⏸️ 结论: 无交易信号(仅有观望建议),继续观望")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -754,6 +869,16 @@ class CryptoAgent:
|
|||||||
valid_signals = [s for s in trade_signals if s.get('confidence', 0) >= threshold]
|
valid_signals = [s for s in trade_signals if s.get('confidence', 0) >= threshold]
|
||||||
|
|
||||||
if not valid_signals:
|
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}%),继续观望")
|
logger.info(f"\n⏸️ 结论: 无交易信号达到置信度阈值 ({threshold}%),继续观望")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -858,8 +983,27 @@ class CryptoAgent:
|
|||||||
# 第三阶段:执行交易动作(各平台独立)
|
# 第三阶段:执行交易动作(各平台独立)
|
||||||
# ============================================================
|
# ============================================================
|
||||||
await self._execute_decisions(paper_decision, hl_decision, bg_decision, market_signal, current_price)
|
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:
|
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}")
|
logger.error(f"❌ 分析 {symbol} 出错: {e}")
|
||||||
import traceback
|
import traceback
|
||||||
logger.error(traceback.format_exc())
|
logger.error(traceback.format_exc())
|
||||||
@ -4220,6 +4364,7 @@ class CryptoAgent:
|
|||||||
'symbols': self.symbols,
|
'symbols': self.symbols,
|
||||||
'mode': 'LLM 驱动',
|
'mode': 'LLM 驱动',
|
||||||
'platform_halts': self.get_platform_halt_status(),
|
'platform_halts': self.get_platform_halt_status(),
|
||||||
|
'analysis_monitor': self._analysis_monitor,
|
||||||
'last_signals': {
|
'last_signals': {
|
||||||
symbol: {
|
symbol: {
|
||||||
'type': sig.get('type'),
|
'type': sig.get('type'),
|
||||||
@ -4230,6 +4375,7 @@ class CryptoAgent:
|
|||||||
for symbol, sig in self.last_signals.items()
|
for symbol, sig in self.last_signals.items()
|
||||||
},
|
},
|
||||||
'last_execution_preview': self.last_execution_preview,
|
'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):
|
async def _notify_expired_orders_cancelled(self, cancelled_orders: List):
|
||||||
|
|||||||
@ -723,6 +723,85 @@
|
|||||||
font-family: "IBM Plex Mono", monospace;
|
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 {
|
.ops-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
@ -920,7 +999,8 @@
|
|||||||
|
|
||||||
.hero-metrics,
|
.hero-metrics,
|
||||||
.platform-stats,
|
.platform-stats,
|
||||||
.signal-stats {
|
.signal-stats,
|
||||||
|
.heartbeat-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1023,6 +1103,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<div>
|
||||||
|
<h2 class="panel-title">分析心跳</h2>
|
||||||
|
<div class="panel-sub">没有信号时,用它判断系统是否仍在正常扫盘</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="heartbeat-grid" id="analysisHeartbeat">
|
||||||
|
<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 class="analysis-log-list" id="analysisLogList">
|
||||||
|
<div class="loading">正在读取分析日志...</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<div>
|
<div>
|
||||||
@ -1359,6 +1457,50 @@
|
|||||||
container.innerHTML = cards.join('');
|
container.innerHTML = cards.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderAnalysisHeartbeat(analysisMonitor, analysisEvents) {
|
||||||
|
const heartbeat = document.getElementById('analysisHeartbeat');
|
||||||
|
const logList = document.getElementById('analysisLogList');
|
||||||
|
const monitor = analysisMonitor || {};
|
||||||
|
const cycleStatus = monitor.last_cycle_status || 'idle';
|
||||||
|
const progressText = monitor.current_cycle_total
|
||||||
|
? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total} ${monitor.current_cycle_symbol || ''}`
|
||||||
|
: '-';
|
||||||
|
|
||||||
|
heartbeat.innerHTML = `
|
||||||
|
<div class="heartbeat-card">
|
||||||
|
<span class="label">最近心跳</span>
|
||||||
|
<span class="value">${monitor.last_heartbeat_at ? `${relativeTime(monitor.last_heartbeat_at)} / ${formatTime(monitor.last_heartbeat_at)}` : '-'}</span>
|
||||||
|
</div>
|
||||||
|
<div class="heartbeat-card">
|
||||||
|
<span class="label">最近轮次</span>
|
||||||
|
<span class="value">${cycleStatus}${monitor.last_cycle_completed_at ? ` / ${relativeTime(monitor.last_cycle_completed_at)}` : ''}</span>
|
||||||
|
</div>
|
||||||
|
<div class="heartbeat-card">
|
||||||
|
<span class="label">当前进度</span>
|
||||||
|
<span class="value">${progressText}</span>
|
||||||
|
</div>
|
||||||
|
<div class="heartbeat-card">
|
||||||
|
<span class="label">下一次运行</span>
|
||||||
|
<span class="value">${monitor.next_scheduled_run_at ? formatTime(monitor.next_scheduled_run_at) : '-'}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (!analysisEvents || analysisEvents.length === 0) {
|
||||||
|
logList.innerHTML = '<div class="empty-box">最近还没有分析日志</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logList.innerHTML = analysisEvents.map((event) => `
|
||||||
|
<div class="analysis-log-item ${event.status || 'info'}">
|
||||||
|
<div class="analysis-log-head">
|
||||||
|
<strong>${event.symbol || 'SYSTEM'} · ${event.event_type || '-'}</strong>
|
||||||
|
<span class="analysis-log-meta">${relativeTime(event.timestamp)} / ${formatTime(event.timestamp)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="analysis-log-detail">${event.detail || '-'}</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';
|
||||||
@ -1651,6 +1793,10 @@
|
|||||||
renderPlatforms(data.platforms, data.crypto_agent?.platform_halts);
|
renderPlatforms(data.platforms, data.crypto_agent?.platform_halts);
|
||||||
renderSignalStream(data.signals?.latest || []);
|
renderSignalStream(data.signals?.latest || []);
|
||||||
renderAgentSignals(data.crypto_agent, data.signals);
|
renderAgentSignals(data.crypto_agent, data.signals);
|
||||||
|
renderAnalysisHeartbeat(
|
||||||
|
data.crypto_agent?.analysis_monitor || {},
|
||||||
|
data.crypto_agent?.recent_analysis_events || []
|
||||||
|
);
|
||||||
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 || []);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user