diff --git a/frontend/console.html b/frontend/console.html index 78684cd..49e93b4 100644 --- a/frontend/console.html +++ b/frontend/console.html @@ -980,6 +980,277 @@ gap: 12px; } + .top-priority-grid { + display: grid; + grid-template-columns: minmax(0, 1.08fr) minmax(280px, 0.92fr); + gap: 14px; + } + + .top-priority-card { + padding: 18px; + border-radius: 18px; + border: 1px solid rgba(255,255,255,0.08); + background: + linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02)), + rgba(255,255,255,0.02); + } + + .top-priority-card.danger { + border-color: rgba(255, 111, 97, 0.24); + background: + linear-gradient(180deg, rgba(255, 111, 97, 0.16), rgba(255, 111, 97, 0.06)), + rgba(255, 111, 97, 0.05); + } + + .top-priority-card.warn { + border-color: rgba(255, 184, 77, 0.24); + background: + linear-gradient(180deg, rgba(255, 184, 77, 0.14), rgba(255, 184, 77, 0.05)), + rgba(255, 184, 77, 0.04); + } + + .top-priority-card.good { + border-color: rgba(48, 209, 88, 0.22); + background: + linear-gradient(180deg, rgba(48, 209, 88, 0.12), rgba(48, 209, 88, 0.04)), + rgba(48, 209, 88, 0.04); + } + + .top-priority-kicker { + color: var(--muted); + font-size: 11px; + margin-bottom: 8px; + font-family: "IBM Plex Mono", monospace; + text-transform: uppercase; + letter-spacing: 0.08em; + } + + .top-priority-title { + font-size: 20px; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--text); + margin-bottom: 10px; + } + + .top-priority-detail, + .top-priority-action { + font-size: 13px; + line-height: 1.65; + color: var(--muted); + } + + .top-priority-action { + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid rgba(255,255,255,0.08); + } + + .top-priority-meta { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 14px; + } + + .priority-mini-list { + display: grid; + gap: 10px; + } + + .priority-mini-item { + padding: 14px; + border-radius: 14px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); + } + + .priority-mini-item.danger { + border-color: rgba(255, 111, 97, 0.18); + background: rgba(255, 111, 97, 0.08); + } + + .priority-mini-item.warn { + border-color: rgba(255, 184, 77, 0.18); + background: rgba(255, 184, 77, 0.08); + } + + .priority-mini-item.good { + border-color: rgba(48, 209, 88, 0.16); + background: rgba(48, 209, 88, 0.07); + } + + .priority-mini-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-bottom: 6px; + } + + .priority-mini-title { + font-size: 13px; + font-weight: 600; + color: var(--text); + } + + .priority-mini-detail { + color: var(--muted); + font-size: 12px; + line-height: 1.55; + } + + .ops-kpi-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; + } + + .ops-kpi-card { + padding: 14px 16px; + border-radius: 16px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); + } + + .ops-kpi-card.warn { + border-color: rgba(255, 184, 77, 0.2); + background: rgba(255, 184, 77, 0.08); + } + + .ops-kpi-card.danger { + border-color: rgba(255, 111, 97, 0.2); + background: rgba(255, 111, 97, 0.08); + } + + .ops-kpi-label { + color: var(--muted); + font-size: 11px; + margin-bottom: 8px; + font-family: "IBM Plex Mono", monospace; + text-transform: uppercase; + letter-spacing: 0.08em; + } + + .ops-kpi-value { + font-family: "IBM Plex Mono", monospace; + font-size: 20px; + color: var(--text); + margin-bottom: 6px; + } + + .ops-kpi-detail { + color: var(--muted); + font-size: 12px; + line-height: 1.55; + } + + .lifecycle-list { + display: grid; + gap: 12px; + } + + .lifecycle-card { + padding: 16px; + border-radius: 16px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); + } + + .lifecycle-card.buy { + border-color: rgba(48, 209, 88, 0.18); + } + + .lifecycle-card.sell { + border-color: rgba(255, 111, 97, 0.18); + } + + .lifecycle-head { + display: flex; + justify-content: space-between; + gap: 12px; + align-items: flex-start; + margin-bottom: 12px; + } + + .lifecycle-title { + font-size: 16px; + font-weight: 700; + color: var(--text); + margin-bottom: 4px; + } + + .lifecycle-meta { + color: var(--muted); + font-size: 11px; + line-height: 1.55; + font-family: "IBM Plex Mono", monospace; + } + + .lifecycle-summary { + color: var(--muted); + font-size: 12px; + line-height: 1.6; + margin-bottom: 12px; + } + + .lifecycle-lanes { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 10px; + } + + .lifecycle-lane { + padding: 12px 14px; + border-radius: 14px; + background: rgba(255,255,255,0.03); + border: 1px solid rgba(255,255,255,0.06); + } + + .lifecycle-lane.good { + border-color: rgba(48, 209, 88, 0.18); + background: rgba(48, 209, 88, 0.08); + } + + .lifecycle-lane.warn { + border-color: rgba(255, 184, 77, 0.18); + background: rgba(255, 184, 77, 0.08); + } + + .lifecycle-lane.danger { + border-color: rgba(255, 111, 97, 0.18); + background: rgba(255, 111, 97, 0.08); + } + + .lifecycle-lane-head { + display: flex; + justify-content: space-between; + gap: 10px; + margin-bottom: 8px; + align-items: center; + } + + .lifecycle-lane-title { + font-size: 13px; + font-weight: 600; + color: var(--text); + } + + .lifecycle-lane-state { + font-size: 11px; + color: var(--muted); + font-family: "IBM Plex Mono", monospace; + text-transform: uppercase; + } + + .lifecycle-lane-detail { + display: grid; + gap: 6px; + color: var(--muted); + font-size: 12px; + line-height: 1.55; + } + .signal-feed-card { padding: 14px 16px; border-radius: 16px; @@ -1959,7 +2230,9 @@ .platform-overview, .platform-stats, .signal-feed-stats, - .position-card-gridline { + .position-card-gridline, + .ops-kpi-grid, + .lifecycle-lanes { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -1996,7 +2269,10 @@ .heartbeat-grid, .health-ribbon, .ops-summary, - .focus-summary { + .focus-summary, + .top-priority-grid, + .ops-kpi-grid, + .lifecycle-lanes { grid-template-columns: 1fr; } @@ -2096,6 +2372,36 @@
+
+
+ +

当前最重要的问题

+
先回答现在最需要你看的是什么,以及你该做什么
+
+
+
正在整理当前优先级...
+
+
+
+
+ +

系统运营指标

+
看系统过去一段时间是否真的有分析、信号和执行产出
+
+
+
正在汇总运营指标...
+
+
+
+
+ +

信号生命周期

+
把信号、执行结果、挂单和持仓串起来,不再分散查看
+
+
+
正在整理信号生命周期...
+
+
@@ -2121,8 +2427,8 @@
-

管理待处理事项

-
把需要你判断和干预的事情放到最前面
+

待处理清单

+
首屏的辅助核对区,完整优先级已提升到左侧顶部
正在汇总待处理事项...
@@ -2425,6 +2731,247 @@ `; } + function normalizeSeverity(severity) { + const value = String(severity || '').toLowerCase(); + if (['danger', 'error', 'critical'].includes(value)) return 'danger'; + if (['warning', 'warn', 'hold'].includes(value)) return 'warn'; + return 'good'; + } + + function countRecentEvents(events, hours = 24, matcher = null) { + const cutoff = Date.now() - hours * 3600 * 1000; + return (events || []).filter((event) => { + const rawTime = event?.timestamp || event?.created_at || event?.opened_at; + const time = rawTime ? new Date(rawTime).getTime() : 0; + if (!time || Number.isNaN(time) || time < cutoff) return false; + return typeof matcher === 'function' ? matcher(event) : true; + }).length; + } + + function inferOpsSnapshot(data) { + const signals = data.signals?.latest || []; + const events = data.execution_events || []; + const positions = data.management?.positions || []; + const orders = data.management?.orders || []; + const blocked24h = countRecentEvents(events, 24, (event) => event.event_type === 'execution_blocked_summary'); + const success24h = countRecentEvents(events, 24, (event) => event.status === 'success'); + const warn24h = countRecentEvents(events, 24, (event) => ['warning', 'error'].includes(event.status)); + const recentSignal24h = countRecentEvents(signals, 24, () => true); + const entryOrders = orders.filter((order) => order.category === 'entry'); + const protectionOrders = orders.filter((order) => order.category === 'tp_sl'); + const completeProtectionCount = positions.filter((position) => position.take_profit && position.stop_loss).length; + const protectionCoverage = positions.length > 0 ? (completeProtectionCount / positions.length) * 100 : 100; + const conversionRate = recentSignal24h > 0 ? (success24h / recentSignal24h) * 100 : 0; + const staleEntryOrders = entryOrders.filter((order) => { + const created = order.created_at ? new Date(order.created_at).getTime() : 0; + return created && !Number.isNaN(created) && (Date.now() - created) > (30 * 60 * 1000); + }).length; + + return { + recentSignal24h, + success24h, + warn24h, + blocked24h, + entryOrders, + protectionOrders, + protectionCoverage, + conversionRate, + staleEntryOrders, + positions, + }; + } + + function deriveTopPriorities(data) { + const cryptoAgent = data.crypto_agent || {}; + const monitor = cryptoAgent.analysis_monitor || {}; + const attentionItems = data.management?.attention_items || []; + const events = data.execution_events || []; + const positions = data.management?.positions || []; + const executionControls = cryptoAgent.target_execution_controls || {}; + const haltedEntries = Object.entries(cryptoAgent.platform_halts || {}).filter(([, item]) => item?.halted); + const disabledTargets = Object.entries(executionControls).filter(([, item]) => item?.enabled === false); + const top = []; + + if (!cryptoAgent.running || !monitor.last_heartbeat_at) { + top.push({ + tone: 'danger', + title: !cryptoAgent.running ? 'Crypto Agent 已停止' : '分析心跳缺失', + detail: !cryptoAgent.running + ? '信号层当前未运行,后续不会继续产出分析、信号或执行决策。' + : '当前没有最近分析心跳,说明调度、循环或主任务可能卡住。', + action: '优先核对进程状态、定时调度和主循环日志,确认系统是否仍在正常扫盘。', + badges: [ + !cryptoAgent.running ? 'STOPPED' : 'NO HEARTBEAT', + monitor.last_heartbeat_at ? `上次 ${relativeTime(monitor.last_heartbeat_at)}` : '无时间戳', + ], + }); + } + + if (haltedEntries.length > 0) { + const [targetKey, halt] = haltedEntries[0]; + top.push({ + tone: 'danger', + title: `${targetKey} 已停机`, + detail: halt?.reason || '平台已触发回撤熔断或风险停机。', + action: '检查触发原因、核对持仓是否已处理,然后决定是恢复自动执行还是保持停机。', + badges: [ + `${haltedEntries.length} 个停机目标`, + halt?.halted_at ? formatTime(halt.halted_at) : '等待处理', + ], + }); + } + + const latestBlocked = events.find((event) => event.event_type === 'execution_blocked_summary'); + if (latestBlocked) { + top.push({ + tone: latestBlocked.status === 'error' ? 'danger' : 'warn', + title: `${latestBlocked.symbol || '-'} 执行被阻塞`, + detail: latestBlocked.reason || '最近有信号未能落到执行层。', + action: '切到风险标签核对平台级归因,确认是价格未到、风控拦截、余额问题还是执行关闭。', + badges: [ + latestBlocked.platform || 'execution', + latestBlocked.signal_timeframe_text || latestBlocked.setup_type || 'blocked', + ], + }); + } + + const unprotected = positions.filter((item) => !item.take_profit || !item.stop_loss); + if (unprotected.length > 0) { + const sample = unprotected[0]; + top.push({ + tone: 'warn', + title: `${sample.symbol || '-'} 风控保护不完整`, + detail: `当前有 ${unprotected.length} 个持仓缺少止盈或止损,保护单链路可能存在异常。`, + action: '优先检查执行监管器和交易所保护单状态,确认 TP/SL 是否漏挂、失败或已失效。', + badges: [ + `${unprotected.length} 个异常持仓`, + `${sample.platform || '-'} / ${sample.account_id || '-'}`, + ], + }); + } + + if (disabledTargets.length > 0) { + const [targetKey] = disabledTargets[0]; + top.push({ + tone: 'warn', + title: `${targetKey} 自动交易已关闭`, + detail: `当前共有 ${disabledTargets.length} 个执行目标处于手动关闭状态。`, + action: '确认这是有意停用还是遗留配置。如果应继续执行,需要在控制台重新开启。', + badges: [`${disabledTargets.length} OFF`, 'manual control'], + }); + } + + attentionItems.slice(0, 2).forEach((item) => { + top.push({ + tone: normalizeSeverity(item.severity), + title: item.title || '待处理事项', + detail: item.detail || '请检查对应模块状态。', + action: '根据事项详情进入风险、运行或平台区域继续下钻。', + badges: [item.timestamp ? relativeTime(item.timestamp) : 'now'], + }); + }); + + if (!top.length) { + top.push({ + tone: 'good', + title: '当前没有紧急人工接管项', + detail: '系统没有明显的停机、熔断、执行阻塞或保护单异常。', + action: '重点关注新的信号生命周期和运营指标,确认系统仍有稳定分析与执行产出。', + badges: ['CLEAR'], + }); + } + + return top.slice(0, 4); + } + + function eventPlatformMatches(event, platform) { + const eventPlatform = String(event?.platform || '').toLowerCase(); + if (platform === 'paper') { + return eventPlatform.includes('paper'); + } + if (platform === 'bitget') { + return eventPlatform.includes('bitget'); + } + return true; + } + + function findMatchingEvent(signal, laneKeyword, events, platform) { + const signalTime = signal?.created_at ? new Date(signal.created_at).getTime() : 0; + const symbol = signal?.symbol; + const candidates = (events || []).filter((event) => { + if (event.symbol !== symbol) return false; + if (!eventPlatformMatches(event, platform)) return false; + if (laneKeyword && event.signal_timeframe_text && !String(event.signal_timeframe_text).includes(laneKeyword)) return false; + const eventTime = event?.timestamp ? new Date(event.timestamp).getTime() : 0; + if (!signalTime || !eventTime || Number.isNaN(signalTime) || Number.isNaN(eventTime)) return true; + return Math.abs(eventTime - signalTime) <= 12 * 3600 * 1000; + }); + return candidates.sort((a, b) => new Date(b.timestamp || 0) - new Date(a.timestamp || 0))[0] || null; + } + + function findMatchingPosition(signal, platform, positions) { + return (positions || []).find((position) => { + if (position.symbol !== signal.symbol) return false; + return String(position.platform || '').toLowerCase() === String(platform || '').toLowerCase(); + }) || null; + } + + function findMatchingOrder(signal, platform, orders) { + return (orders || []).find((order) => { + if (order.symbol !== signal.symbol) return false; + if (String(order.platform || '').toLowerCase() !== String(platform || '').toLowerCase()) return false; + return order.category === 'entry'; + }) || null; + } + + function buildLifecycleLane(signal, laneName, platform, previewDecision, events, positions, orders) { + const position = findMatchingPosition(signal, platform, positions); + const order = findMatchingOrder(signal, platform, orders); + const event = findMatchingEvent(signal, signal.timeframe || signal.type || '', events, platform); + let tone = 'warn'; + let state = 'pending'; + const detailRows = []; + + if (previewDecision?.decision) { + detailRows.push(`决策: ${previewDecision.decision}`); + } + + if (position) { + tone = (!position.take_profit || !position.stop_loss) ? 'warn' : 'good'; + state = 'position'; + detailRows.push(`已持仓 ${position.side || '-'} / ${formatNumber(position.size || 0, 4)} / ${formatNumber(position.leverage || 0, 1)}x`); + detailRows.push(`入场 ${formatMoney(position.entry_price)} / 现价 ${formatMoney(position.mark_price)}`); + detailRows.push(`TP ${position.take_profit ? formatMoney(position.take_profit) : '-'} / SL ${position.stop_loss ? formatMoney(position.stop_loss) : '-'}`); + } else if (order) { + tone = 'warn'; + state = 'entry order'; + detailRows.push(`挂单 ${order.order_type || '-'} / ${formatMoney(order.price)} / ${formatNumber(order.size || 0, 4)}`); + detailRows.push(`创建 ${order.created_at ? `${relativeTime(order.created_at)} / ${formatTime(order.created_at)}` : '-'}`); + } else if (event) { + tone = event.status === 'success' ? 'good' : (event.status === 'error' ? 'danger' : 'warn'); + state = event.status || event.event_type || 'event'; + detailRows.push(event.reason || '已有执行事件,但未找到持仓或挂单。'); + if (event.action || event.event_type) { + detailRows.push(`动作 ${event.action || '-'} / 事件 ${event.event_type || '-'}`); + } + } else if (previewDecision?.decision) { + tone = ['OPEN', 'ADD', 'CLOSE', 'CANCEL_PENDING'].includes(previewDecision.decision) ? 'warn' : 'good'; + state = previewDecision.decision.toLowerCase(); + detailRows.push(previewDecision.reason || previewDecision.reasoning || '已有决策预览,但尚未找到执行落地迹象。'); + } else { + tone = 'good'; + state = 'idle'; + detailRows.push('当前没有匹配到执行结果、挂单或持仓。'); + } + + return { + laneName, + tone, + state, + detailRows, + }; + } + function loadSensitivePreference() { try { revealSensitiveData = window.localStorage.getItem(SENSITIVE_VISIBILITY_KEY) === '1'; @@ -2455,6 +3002,8 @@ cachedConsoleData.crypto_agent?.platform_halts, cachedConsoleData.crypto_agent?.target_execution_controls || {} ); + renderOpsKpis(cachedConsoleData); + renderSignalLifecycle(cachedConsoleData); } } @@ -2995,6 +3544,129 @@ }); } + function renderTopPriority(data) { + const container = document.getElementById('topPriority'); + if (!container) return; + + const priorities = deriveTopPriorities(data); + const primary = priorities[0]; + const rest = priorities.slice(1, 4); + + container.innerHTML = ` +
+
top priority
+
${primary.title}
+
${primary.detail}
+
建议动作: ${primary.action}
+
+ ${(primary.badges || []).map((badge) => `${badge}`).join('')} +
+
+
+ ${rest.length ? rest.map((item) => ` +
+
+
${item.title}
+ ${item.tone} +
+
${item.detail}
+
+ `).join('') : compactEmpty('没有其他更高优先级事项', '当前首要问题已经覆盖了最需要处理的点。')} +
+ `; + } + + function renderOpsKpis(data) { + const container = document.getElementById('opsKpis'); + if (!container) return; + + const snapshot = inferOpsSnapshot(data); + const halts = countHalted(data.crypto_agent?.platform_halts || {}); + const disabled = Object.values(data.crypto_agent?.target_execution_controls || {}).filter((item) => item?.enabled === false).length; + + container.innerHTML = ` +
+
24h 分析产出
+
${snapshot.recentSignal24h}
+
最近 24 小时写入的信号数。若长期为 0,需要先看运行心跳。
+
+
+
24h 执行成功率
+
${formatPercent(snapshot.conversionRate, 1)}
+
成功 ${snapshot.success24h} / 信号 ${snapshot.recentSignal24h || 0},阻塞 ${snapshot.blocked24h}。
+
+
+
持仓保护完整率
+
${formatPercent(snapshot.protectionCoverage, 0)}
+
持仓 ${snapshot.positions.length} / 已完整 TP+SL ${snapshot.positions.filter((item) => item.take_profit && item.stop_loss).length}。
+
+
+
停机与陈旧挂单
+
${halts + disabled + snapshot.staleEntryOrders}
+
停机 ${halts} / 关闭 ${disabled} / 超 30m 未成交入场单 ${snapshot.staleEntryOrders}。
+
+ `; + } + + function renderSignalLifecycle(data) { + const container = document.getElementById('signalLifecycle'); + if (!container) return; + + const signals = data.signals?.latest || []; + const events = data.execution_events || []; + const positions = data.management?.positions || []; + const orders = data.management?.orders || []; + const previewMap = data.crypto_agent?.last_execution_preview || {}; + + if (!signals.length) { + container.innerHTML = compactEmpty('暂无可追踪的信号生命周期', '当前没有最近信号,先关注运行心跳和分析状态。'); + return; + } + + const cards = signals.slice(0, 5).map((signal) => { + const preview = previewMap?.[signal.symbol] || {}; + const paperLane = buildLifecycleLane(signal, '模拟盘', 'paper', preview.paper, events, positions, orders); + const bitgetPreview = preview.bitget_accounts?.default || preview.bitget; + const bitgetLane = buildLifecycleLane(signal, 'Bitget', 'bitget', bitgetPreview, events, positions, orders); + const tone = statusClassFromAction(signal.action); + const summary = signal.reasoning || signal.reason || signal.summary || '暂无额外说明'; + + return ` +
+
+
+
${signal.symbol || '-'}
+
+ ${relativeTime(signal.created_at)} / ${formatTime(signal.created_at)}
+ ${signal.signal_type || '-'} / ${signal.timeframe || signal.type || '-'} / ${signal.grade || '-'} / ${formatPercent(signal.confidence || 0, 1)} +
+
+ ${signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '观望'} +
+
+ 入场 ${formatMoney(signal.entry_price)} / 现价 ${formatMoney(signal.current_price)} / TP ${formatMoney(signal.take_profit)} / SL ${formatMoney(signal.stop_loss)}
+ ${summary} +
+
+ ${[paperLane, bitgetLane].map((lane) => ` +
+
+
${lane.laneName}
+
${lane.state}
+
+
+ ${lane.detailRows.map((row) => `
${row}
`).join('')} +
+
+ `).join('')} +
+
+ `; + }); + + container.innerHTML = cards.join(''); + } + function renderSignalStream(signals) { const container = document.getElementById('signalStream'); if (!signals || signals.length === 0) { @@ -3669,6 +4341,9 @@ renderFocusSummary(data); renderAlertStrip(data); renderOpsSummary(data); + renderTopPriority(data); + renderOpsKpis(data); + renderSignalLifecycle(data); renderPlatforms(data.platforms, data.crypto_agent?.platform_halts, data.crypto_agent?.target_execution_controls || {}); renderSignalStream(data.signals?.latest || []); renderAgentSignals(data.crypto_agent, data.signals);