This commit is contained in:
aaron 2026-04-22 11:19:44 +08:00
parent b1e6215ae2
commit 19780cf210
2 changed files with 294 additions and 2 deletions

View File

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

View File

@ -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 || []);