1
This commit is contained in:
parent
2779330ee5
commit
784c6a8fff
@ -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 @@
|
||||
<div class="tab-pane active" data-tab-pane="command" id="commandOverview">
|
||||
<div class="overview-grid">
|
||||
<div class="overview-main">
|
||||
<section class="coord-block focus-panel">
|
||||
<div class="block-head">
|
||||
<div class="section-label">Priority Layer</div>
|
||||
<h3 class="block-title">当前最重要的问题</h3>
|
||||
<div class="block-sub">先回答现在最需要你看的是什么,以及你该做什么</div>
|
||||
</div>
|
||||
<div class="top-priority-grid" id="topPriority">
|
||||
<div class="loading">正在整理当前优先级...</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="coord-block focus-panel">
|
||||
<div class="block-head">
|
||||
<div class="section-label">Ops Layer</div>
|
||||
<h3 class="block-title">系统运营指标</h3>
|
||||
<div class="block-sub">看系统过去一段时间是否真的有分析、信号和执行产出</div>
|
||||
</div>
|
||||
<div class="ops-kpi-grid" id="opsKpis">
|
||||
<div class="loading">正在汇总运营指标...</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="coord-block focus-panel">
|
||||
<div class="block-head">
|
||||
<div class="section-label">Lifecycle Layer</div>
|
||||
<h3 class="block-title">信号生命周期</h3>
|
||||
<div class="block-sub">把信号、执行结果、挂单和持仓串起来,不再分散查看</div>
|
||||
</div>
|
||||
<div class="lifecycle-list" id="signalLifecycle">
|
||||
<div class="loading">正在整理信号生命周期...</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="coord-block focus-panel">
|
||||
<div class="block-head">
|
||||
<div class="section-label">Decision Layer</div>
|
||||
@ -2121,8 +2427,8 @@
|
||||
<section class="coord-block focus-panel">
|
||||
<div class="block-head">
|
||||
<div class="section-label">Risk Layer</div>
|
||||
<h3 class="block-title">管理待处理事项</h3>
|
||||
<div class="block-sub">把需要你判断和干预的事情放到最前面</div>
|
||||
<h3 class="block-title">待处理清单</h3>
|
||||
<div class="block-sub">首屏的辅助核对区,完整优先级已提升到左侧顶部</div>
|
||||
</div>
|
||||
<div class="attention-list" id="attentionList">
|
||||
<div class="loading">正在汇总待处理事项...</div>
|
||||
@ -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 = `
|
||||
<article class="top-priority-card ${primary.tone}">
|
||||
<div class="top-priority-kicker">top priority</div>
|
||||
<div class="top-priority-title">${primary.title}</div>
|
||||
<div class="top-priority-detail">${primary.detail}</div>
|
||||
<div class="top-priority-action"><strong style="color: var(--text);">建议动作:</strong> ${primary.action}</div>
|
||||
<div class="top-priority-meta">
|
||||
${(primary.badges || []).map((badge) => `<span class="event-inline-badge">${badge}</span>`).join('')}
|
||||
</div>
|
||||
</article>
|
||||
<div class="priority-mini-list">
|
||||
${rest.length ? rest.map((item) => `
|
||||
<div class="priority-mini-item ${item.tone}">
|
||||
<div class="priority-mini-head">
|
||||
<div class="priority-mini-title">${item.title}</div>
|
||||
<span class="event-inline-badge">${item.tone}</span>
|
||||
</div>
|
||||
<div class="priority-mini-detail">${item.detail}</div>
|
||||
</div>
|
||||
`).join('') : compactEmpty('没有其他更高优先级事项', '当前首要问题已经覆盖了最需要处理的点。')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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 = `
|
||||
<article class="ops-kpi-card ${snapshot.recentSignal24h === 0 ? 'warn' : ''}">
|
||||
<div class="ops-kpi-label">24h 分析产出</div>
|
||||
<div class="ops-kpi-value">${snapshot.recentSignal24h}</div>
|
||||
<div class="ops-kpi-detail">最近 24 小时写入的信号数。若长期为 0,需要先看运行心跳。</div>
|
||||
</article>
|
||||
<article class="ops-kpi-card ${snapshot.blocked24h > snapshot.success24h ? 'warn' : ''}">
|
||||
<div class="ops-kpi-label">24h 执行成功率</div>
|
||||
<div class="ops-kpi-value">${formatPercent(snapshot.conversionRate, 1)}</div>
|
||||
<div class="ops-kpi-detail">成功 ${snapshot.success24h} / 信号 ${snapshot.recentSignal24h || 0},阻塞 ${snapshot.blocked24h}。</div>
|
||||
</article>
|
||||
<article class="ops-kpi-card ${snapshot.protectionCoverage < 100 ? 'warn' : ''}">
|
||||
<div class="ops-kpi-label">持仓保护完整率</div>
|
||||
<div class="ops-kpi-value">${formatPercent(snapshot.protectionCoverage, 0)}</div>
|
||||
<div class="ops-kpi-detail">持仓 ${snapshot.positions.length} / 已完整 TP+SL ${snapshot.positions.filter((item) => item.take_profit && item.stop_loss).length}。</div>
|
||||
</article>
|
||||
<article class="ops-kpi-card ${(halts > 0 || disabled > 0) ? 'danger' : (snapshot.staleEntryOrders > 0 ? 'warn' : '')}">
|
||||
<div class="ops-kpi-label">停机与陈旧挂单</div>
|
||||
<div class="ops-kpi-value">${halts + disabled + snapshot.staleEntryOrders}</div>
|
||||
<div class="ops-kpi-detail">停机 ${halts} / 关闭 ${disabled} / 超 30m 未成交入场单 ${snapshot.staleEntryOrders}。</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
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 `
|
||||
<article class="lifecycle-card ${tone}">
|
||||
<div class="lifecycle-head">
|
||||
<div>
|
||||
<div class="lifecycle-title">${signal.symbol || '-'}</div>
|
||||
<div class="lifecycle-meta">
|
||||
${relativeTime(signal.created_at)} / ${formatTime(signal.created_at)}<br>
|
||||
${signal.signal_type || '-'} / ${signal.timeframe || signal.type || '-'} / ${signal.grade || '-'} / ${formatPercent(signal.confidence || 0, 1)}
|
||||
</div>
|
||||
</div>
|
||||
<span class="badge ${tone}">${signal.action === 'buy' ? '做多' : signal.action === 'sell' ? '做空' : '观望'}</span>
|
||||
</div>
|
||||
<div class="lifecycle-summary">
|
||||
入场 ${formatMoney(signal.entry_price)} / 现价 ${formatMoney(signal.current_price)} / TP ${formatMoney(signal.take_profit)} / SL ${formatMoney(signal.stop_loss)}<br>
|
||||
${summary}
|
||||
</div>
|
||||
<div class="lifecycle-lanes">
|
||||
${[paperLane, bitgetLane].map((lane) => `
|
||||
<div class="lifecycle-lane ${lane.tone}">
|
||||
<div class="lifecycle-lane-head">
|
||||
<div class="lifecycle-lane-title">${lane.laneName}</div>
|
||||
<div class="lifecycle-lane-state">${lane.state}</div>
|
||||
</div>
|
||||
<div class="lifecycle-lane-detail">
|
||||
${lane.detailRows.map((row) => `<div>${row}</div>`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user