This commit is contained in:
aaron 2026-04-27 10:32:14 +08:00
parent e98b1c3c9c
commit 75c7fd4f0c
3 changed files with 563 additions and 401 deletions

View File

@ -11,6 +11,8 @@ from app.crypto_agent.crypto_agent import get_crypto_agent
from app.services.signal_database_service import get_signal_db_service
from app.services.paper_trading_service import get_paper_trading_service
from app.services.bitget_live_trading_service import get_all_bitget_live_services, get_bitget_live_service
from app.services.price_monitor_service import get_price_monitor_service
from app.services.runtime_status_service import get_runtime_status
router = APIRouter()
@ -429,6 +431,11 @@ async def get_console_snapshot():
if (_parse_signal_timestamp(signal.get("created_at")) or datetime.min) >= recent_cutoff
)
price_monitor = get_price_monitor_service()
configured_symbols = [symbol.strip().upper() for symbol in (get_settings().crypto_symbols or "").split(",") if symbol.strip()]
for symbol in configured_symbols:
price_monitor.subscribe_symbol(symbol)
paper_position_items = [
_normalize_platform_position("paper", pos)
for pos in paper_service.get_open_positions()[:12]
@ -517,6 +524,16 @@ async def get_console_snapshot():
"recent_30m_count": recent_signal_count,
},
"platforms": platforms_payload,
"monitoring": {
"price_monitor": {
"running": price_monitor.is_running(),
"mode": "websocket" if getattr(price_monitor, "_use_websocket", False) else "polling",
"subscribed_symbols": price_monitor.get_subscribed_symbols(),
"latest_prices": price_monitor.get_all_prices(),
"checked_at": now.isoformat(),
},
"execution_loop": get_runtime_status("price_monitor_loop"),
},
"management": {
"positions": unified_positions[:18],
"orders": unified_orders[:24],

View File

@ -5,7 +5,7 @@ import asyncio
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, RedirectResponse
from contextlib import asynccontextmanager
from app.config import get_settings
from app.utils.logger import logger
@ -485,7 +485,7 @@ if os.path.exists(frontend_path):
@app.get("/")
async def root():
"""根路径,返回主应用页面"""
index_path = os.path.join(frontend_path, "trading.html")
index_path = os.path.join(frontend_path, "console.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return {"message": "页面不存在"}
@ -493,7 +493,7 @@ async def root():
@app.get("/app")
async def app_page():
"""主应用页面"""
index_path = os.path.join(frontend_path, "trading.html")
index_path = os.path.join(frontend_path, "console.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return {"message": "页面不存在"}
@ -505,19 +505,13 @@ async def health_check():
@app.get("/trading")
async def trading_page():
"""交易页面"""
page_path = os.path.join(frontend_path, "trading.html")
if os.path.exists(page_path):
return FileResponse(page_path)
return {"message": "页面不存在"}
"""交易页面兼容入口,统一跳转到总控台"""
return RedirectResponse(url="/console", status_code=307)
@app.get("/bitget-trading")
async def bitget_trading_page():
"""Bitget 交易页面兼容入口,统一跳转到当前 trading 页面"""
page_path = os.path.join(frontend_path, "trading.html")
if os.path.exists(page_path):
return FileResponse(page_path)
return {"message": "页面不存在"}
"""Bitget 交易页面兼容入口,统一跳转到总控台"""
return RedirectResponse(url="/console", status_code=307)
@app.get("/signals")
async def signals_page():

View File

@ -58,6 +58,167 @@
padding: 24px 0 40px;
}
.console-hub {
display: grid;
grid-template-columns: 240px minmax(0, 1fr);
gap: 18px;
align-items: start;
}
.console-sidebar {
position: sticky;
top: 18px;
padding: 16px;
border-radius: 22px;
background: var(--panel);
border: 1px solid var(--line);
box-shadow: var(--shadow);
backdrop-filter: blur(18px);
}
.console-sidebar::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border-radius: 22px;
background: linear-gradient(180deg, rgba(255,255,255,0.04), transparent 24%, transparent 76%, rgba(126, 200, 255, 0.04));
}
.sidebar-label {
color: var(--muted);
font-size: 11px;
margin-bottom: 12px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.sidebar-nav {
display: grid;
gap: 10px;
}
.sidebar-link {
appearance: none;
width: 100%;
text-align: left;
cursor: pointer;
padding: 14px 16px;
border-radius: 16px;
border: 1px solid rgba(255,255,255,0.06);
background: rgba(255,255,255,0.03);
color: var(--text);
transition: border-color 0.2s ease, background 0.2s ease, transform 0.2s ease;
}
.sidebar-link:hover {
transform: translateY(-1px);
border-color: rgba(126, 200, 255, 0.18);
}
.sidebar-link.active {
border-color: rgba(126, 200, 255, 0.28);
background: rgba(126, 200, 255, 0.10);
}
.sidebar-link-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
font-size: 14px;
font-weight: 600;
margin-bottom: 6px;
}
.sidebar-link-sub {
color: var(--muted);
font-size: 12px;
line-height: 1.55;
}
.sidebar-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 20px;
padding: 0 8px;
border-radius: 999px;
background: rgba(255,255,255,0.08);
color: var(--cold);
font-size: 10px;
font-family: "IBM Plex Mono", monospace;
}
.hub-content {
display: grid;
gap: 18px;
}
.hub-pane {
display: none;
gap: 18px;
}
.hub-pane.active {
display: grid;
}
.pane-grid {
display: grid;
gap: 18px;
}
.pane-grid.two-col {
grid-template-columns: minmax(0, 1.08fr) minmax(320px, 0.92fr);
}
.pane-grid.equal {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.compact-panel-body {
display: grid;
gap: 14px;
}
.monitor-strip {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.monitor-card {
padding: 14px 16px;
border-radius: 16px;
background: rgba(255,255,255,0.03);
border: 1px solid rgba(255,255,255,0.06);
}
.monitor-card .title {
color: var(--muted);
font-size: 11px;
margin-bottom: 8px;
font-family: "IBM Plex Mono", monospace;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.monitor-card .main {
font-family: "IBM Plex Mono", monospace;
font-size: 16px;
color: var(--text);
margin-bottom: 6px;
}
.monitor-card .meta {
color: var(--muted);
font-size: 12px;
line-height: 1.6;
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.4fr) minmax(360px, 0.9fr);
@ -2211,6 +2372,7 @@
}
@media (max-width: 1240px) {
.console-hub,
.hero,
.layout,
.priority-layout,
@ -2219,11 +2381,23 @@
grid-template-columns: 1fr;
}
.console-sidebar {
position: static;
}
.sidebar-nav {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.platform-grid,
.signal-grid,
.ops-grid,
.coord-grid,
.position-card-grid {
.position-card-grid,
.pane-grid.two-col,
.pane-grid.equal,
.monitor-strip,
.runtime-summary-grid {
grid-template-columns: 1fr;
}
@ -2251,6 +2425,10 @@
padding-top: 12px;
}
.sidebar-nav {
grid-template-columns: 1fr;
}
.hero-main,
.hero-side,
.panel,
@ -2294,7 +2472,7 @@
<div class="eyebrow">System Console</div>
<h1 class="hero-title">交易系统 <strong>总控台</strong></h1>
<p class="hero-subtitle">
统一观察信号流、执行层、三端账户风险和平台熔断状态。
统一观察信号流、执行层、账户风险和平台熔断状态。
这个页面的目标不是展示“历史”,而是让你在一屏内判断系统现在是不是健康、哪里堵住了、哪里需要人工接管。
</p>
<div class="hero-metrics" id="heroMetrics">
@ -2327,8 +2505,34 @@
<div id="feedback"></div>
<section class="layout">
<div class="left-stack">
<section class="console-hub">
<aside class="console-sidebar">
<div class="sidebar-label">Navigation</div>
<div class="sidebar-nav" id="sidebarNav">
<button class="sidebar-link active" data-hub-target="hubOverview">
<div class="sidebar-link-title">总览 <span class="sidebar-badge" id="sidebarBadgeOverview">-</span></div>
<div class="sidebar-link-sub">系统健康、优先级、执行概览</div>
</button>
<button class="sidebar-link" data-hub-target="hubExecution">
<div class="sidebar-link-title">执行资产 <span class="sidebar-badge" id="sidebarBadgeExecution">-</span></div>
<div class="sidebar-link-sub">平台、持仓、挂单、自动交易控制</div>
</button>
<button class="sidebar-link" data-hub-target="hubSignals">
<div class="sidebar-link-title">信号决策 <span class="sidebar-badge" id="sidebarBadgeSignals">-</span></div>
<div class="sidebar-link-sub">生命周期、预览、信号缓存</div>
</button>
<button class="sidebar-link" data-hub-target="hubRuntime">
<div class="sidebar-link-title">运行监控 <span class="sidebar-badge" id="sidebarBadgeRuntime">-</span></div>
<div class="sidebar-link-sub">心跳、调度、guardian、事件触发</div>
</button>
<button class="sidebar-link" data-hub-target="hubLogs">
<div class="sidebar-link-title">日志事件 <span class="sidebar-badge" id="sidebarBadgeLogs">-</span></div>
<div class="sidebar-link-sub">执行事件、分析日志、阻塞归因</div>
</button>
</div>
</aside>
<div class="hub-content">
<section class="panel">
<div class="panel-header">
<div>
@ -2336,6 +2540,9 @@
<h2 class="panel-title" style="margin-top: 12px;">系统健康总览</h2>
<div class="panel-sub">先判断系统活着没有,再判断风险和堵点</div>
</div>
<div class="panel-actions">
<button class="ghost-btn" id="toggleSensitiveBtn">敏感数据: 隐藏</button>
</div>
</div>
<div class="health-ribbon" id="healthRibbon">
<div class="health-card"><div class="kicker">分析状态</div><div class="headline">-</div></div>
@ -2351,136 +2558,195 @@
<div class="alert-strip" id="alertStrip"></div>
</section>
<section class="hub-pane active" id="hubOverview">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Command Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">指挥台视图</h2>
<div class="panel-sub">先看系统机会与风险,再下钻到运行细节和日志</div>
</div>
<div class="panel-actions">
<button class="ghost-btn" id="toggleSensitiveBtn">敏感数据: 隐藏</button>
<div class="workspace-tabs" data-tab-group="command">
<button class="workspace-tab active" data-tab="command" data-target="commandOverview">总览</button>
<button class="workspace-tab" data-tab="command" data-target="commandRisk">风险</button>
<button class="workspace-tab" data-tab="command" data-target="commandRuntime">运行</button>
<button class="workspace-tab" data-tab="command" data-target="commandLogs">日志</button>
</div>
</div>
</div>
<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>
<h2 class="panel-title" style="margin-top: 12px;">当前最重要的问题</h2>
<div class="panel-sub">先回答你现在最该关注什么,再决定是否继续下钻</div>
</div>
</div>
<div class="compact-panel-body">
<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>
<h3 class="block-title">最近决策预览</h3>
<div class="block-sub">优先看系统准备做什么,而不是先看历史事件</div>
<div class="pane-grid two-col">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Execution Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">平台执行概览</h2>
<div class="panel-sub">统一看平台权益、仓位、挂单、停机与自动交易状态</div>
</div>
<div class="signal-grid" id="decisionPreview">
<div class="loading">正在读取决策预览...</div>
</div>
</section>
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Platform Layer</div>
<h3 class="block-title">平台执行概览</h3>
<div class="block-sub">资金、杠杆、持仓、挂单、回撤阈值</div>
</div>
<div class="platform-grid" id="platformGrid">
<div class="loading">正在加载平台状态...</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Attention Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">待处理清单</h2>
<div class="panel-sub">保留给人工干预和快速核对的辅助视图</div>
</div>
<div class="overview-side">
<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>
</div>
<div class="attention-list" id="attentionList">
<div class="loading">正在汇总待处理事项...</div>
</div>
</section>
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Market Layer</div>
<h3 class="block-title">最近信号流</h3>
<div class="block-sub">只保留紧凑摘要,不让历史列表抢占主视图</div>
</div>
</div>
</section>
<section class="hub-pane" id="hubExecution">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Asset Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">执行与资产</h2>
<div class="panel-sub">以后不再看 trading 页面,这里承接持仓、挂单和执行状态</div>
</div>
</div>
<div class="monitor-strip" id="executionMonitorStrip">
<div class="monitor-card">
<div class="title">价格链路</div>
<div class="main">等待刷新</div>
<div class="meta">状态加载中</div>
</div>
<div class="monitor-card">
<div class="title">模拟盘执行链路</div>
<div class="main">等待刷新</div>
<div class="meta">状态加载中</div>
</div>
</div>
</section>
<section class="panel workspace-panel unified-section">
<div class="workspace-head">
<div>
<div class="section-label">Position Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">统一持仓视图</h2>
<div class="panel-sub">跨平台持仓合并展示,优先看风险、盈亏和保护完整度</div>
</div>
</div>
<div class="table-toolbar" id="positionsToolbar"></div>
<div class="unified-table" id="positionsTable">
<div class="loading">正在整理跨平台持仓...</div>
</div>
</section>
<section class="panel workspace-panel unified-section">
<div class="workspace-head">
<div>
<div class="section-label">Order Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">统一挂单视图</h2>
<div class="panel-sub">入场单、保护单、资金占用和老化状态集中查看</div>
</div>
</div>
<div class="table-toolbar" id="ordersToolbar"></div>
<div class="unified-table" id="ordersTable">
<div class="loading">正在整理跨平台挂单...</div>
</div>
</section>
</div>
</section>
<section class="hub-pane" id="hubSignals">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Lifecycle Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">信号与决策</h2>
<div class="panel-sub">把最近信号、生命周期、预览和缓存收敛到一个入口</div>
</div>
</div>
<div class="lifecycle-list" id="signalLifecycle">
<div class="loading">正在整理信号生命周期...</div>
</div>
</section>
<div class="pane-grid two-col">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Decision Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">最近决策预览</h2>
<div class="panel-sub">看系统准备做什么,以及不同执行目标如何响应</div>
</div>
</div>
<div class="signal-grid" id="decisionPreview">
<div class="loading">正在读取决策预览...</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Signal Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">最近信号流</h2>
<div class="panel-sub">压缩展示最新机会,但保留关键信号细节</div>
</div>
</div>
<div class="stream-list workspace-stream scroll-panel" id="signalStream">
<div class="loading">正在读取信号...</div>
</div>
</section>
</div>
</div>
</div>
<div class="tab-pane" data-tab-pane="command" id="commandRisk">
<div class="focus-grid">
<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>
<div class="pane-grid equal">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Agent Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">Crypto Agent 状态</h2>
<div class="panel-sub">最近信号缓存、统计和辅助协同信息</div>
</div>
<div class="halt-list" id="haltList">
<div class="loading">正在读取平台停机状态...</div>
</div>
<div class="signal-grid" id="agentSignals">
<div class="loading">正在读取 Agent 状态...</div>
</div>
</section>
<section class="coord-block focus-panel">
<div class="block-head">
<div class="section-label">Blocked Layer</div>
<h3 class="block-title">最近阻塞原因</h3>
<div class="block-sub">未落单、风险过滤、执行限制统一归因</div>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Mirror Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">补充视角</h2>
<div class="panel-sub">保留对照区,避免主区过度堆叠</div>
</div>
</div>
<div class="signal-grid" id="coordinationMirror">
<div class="empty-box compact">
<strong>主视图已集中到本菜单</strong>
<div class="empty-detail">后续如需要补充“信号转执行”专题卡片,可以继续在这里加,不影响总览区。</div>
</div>
<div class="blocked-list" id="blockedSummaryList">
<div class="loading">正在读取未落单汇总...</div>
</div>
</section>
</div>
</div>
</section>
<div class="tab-pane" data-tab-pane="command" id="commandRuntime">
<div class="focus-grid">
<div class="overview-main">
<section class="coord-block focus-panel">
<div class="block-head">
<section class="hub-pane" id="hubRuntime">
<div class="pane-grid two-col">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Runtime Layer</div>
<h3 class="block-title">分析心跳</h3>
<div class="block-sub">没有信号时,也能确认系统仍在正常扫盘</div>
<h2 class="panel-title" style="margin-top: 12px;">分析心跳与调度</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>
@ -2499,28 +2765,48 @@
</div>
</div>
</section>
</div>
<div class="overview-side">
<section class="coord-block focus-panel">
<div class="block-head">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Guardian Layer</div>
<h3 class="block-title">执行监管目标</h3>
<div class="block-sub">监管 target、保护单补救、持仓管理能力</div>
<h2 class="panel-title" style="margin-top: 12px;">执行监管目标</h2>
<div class="panel-sub">监管 target、保护单补救和持仓管理能力集中展示</div>
</div>
</div>
<div class="analysis-log-list" id="guardianTargetList">
<div class="loading">正在读取执行监管目标...</div>
</div>
</section>
</div>
</div>
</div>
</section>
<div class="tab-pane" data-tab-pane="command" id="commandLogs">
<section class="hub-pane" id="hubLogs">
<div class="pane-grid">
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Risk Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">风险与阻塞</h2>
<div class="panel-sub">停机、熔断、执行阻塞统一在这里看,不再散落在各处</div>
</div>
</div>
<div class="pane-grid equal">
<div class="halt-list" id="haltList">
<div class="loading">正在读取平台停机状态...</div>
</div>
<div class="blocked-list" id="blockedSummaryList">
<div class="loading">正在读取未落单汇总...</div>
</div>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head log-head">
<div>
<div class="section-label">Log Layer</div>
<h3 class="block-title">运行与执行日志</h3>
<div class="block-sub">日志放到最后一级,按需查看,不再占用首屏决策空间</div>
<h2 class="panel-title" style="margin-top: 12px;">运行与执行日志</h2>
<div class="panel-sub">日志保留在独立菜单,按需查看</div>
</div>
<div class="workspace-tabs" data-tab-group="logs">
<button class="workspace-tab active" data-tab="logs" data-target="logsExecution">执行事件</button>
@ -2544,102 +2830,9 @@
<div class="loading">正在读取分析日志...</div>
</div>
</div>
</div>
</section>
</div>
</section>
<section class="panel workspace-panel">
<div class="workspace-head">
<div>
<div class="section-label">Drilldown Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">补充工作区</h2>
<div class="panel-sub">首屏用于判断,补充区用于核对账户、信号缓存和明细状态</div>
</div>
<div class="workspace-tabs" data-tab-group="workspace">
<button class="workspace-tab active" data-tab="workspace" data-target="workspaceCoordination">账户协同</button>
<button class="workspace-tab" data-tab="workspace" data-target="workspaceSignals">信号缓存</button>
</div>
</div>
<div class="tab-pane active" data-tab-pane="workspace" id="workspaceCoordination">
<div class="coord-grid">
<div class="coord-block">
<div class="block-head">
<div class="section-label">Agent Layer</div>
<h3 class="block-title">Crypto Agent 状态</h3>
<div class="block-sub">最近信号、平台停机、执行层状态</div>
</div>
<div class="signal-grid" id="agentSignals">
<div class="loading">正在读取 Agent 状态...</div>
</div>
</div>
<div class="coord-block">
<div class="block-head">
<div class="section-label">Decision Layer</div>
<h3 class="block-title">Agent / 账户协同</h3>
<div class="block-sub">保留辅助视角,用于对照信号缓存与系统工作状态</div>
</div>
<div class="signal-grid" id="coordinationMirror">
<div class="empty-box compact">
<strong>主视图已迁移到指挥台</strong>
<div class="empty-detail">最近决策预览已放到上方“总览”主标签,这里仅保留补充信息,避免重复占屏。</div>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane" data-tab-pane="workspace" id="workspaceSignals">
<div class="block-head">
<div class="section-label">Market Layer</div>
<h3 class="block-title">最近信号缓存</h3>
<div class="block-sub">用于核对 Agent 最近缓存的信号,不再重复展示完整主信号流</div>
</div>
<div class="signal-grid" id="signalMirror">
<div class="empty-box compact">
<strong>主信号流已迁移到指挥台</strong>
<div class="empty-detail">最近信号流已放到上方“总览”主标签,这里保留为信号缓存核对区。</div>
</div>
</div>
</div>
</section>
<section class="panel workspace-panel unified-section">
<div class="workspace-head">
<div>
<div class="section-label">Asset Layer</div>
<h2 class="panel-title" style="margin-top: 12px;">资产与挂单</h2>
<div class="panel-sub">统一持仓和统一挂单改为切换查看,减少表格同时占屏</div>
</div>
<div class="workspace-tabs" data-tab-group="asset">
<button class="workspace-tab active" data-tab="asset" data-target="assetPositions">持仓</button>
<button class="workspace-tab" data-tab="asset" data-target="assetOrders">挂单</button>
</div>
</div>
<div class="tab-pane active" data-tab-pane="asset" id="assetPositions">
<div class="block-head">
<div class="section-label">Position Layer</div>
<h3 class="block-title">统一持仓视图</h3>
<div class="block-sub">三端持仓合并,优先看风险与盈亏</div>
</div>
<div class="table-toolbar" id="positionsToolbar"></div>
<div class="unified-table" id="positionsTable">
<div class="loading">正在整理跨平台持仓...</div>
</div>
</div>
<div class="tab-pane" data-tab-pane="asset" id="assetOrders">
<div class="block-head">
<div class="section-label">Order Layer</div>
<h3 class="block-title">统一挂单视图</h3>
<div class="block-sub">入场单、保护单、资金占用一屏观察</div>
</div>
<div class="table-toolbar" id="ordersToolbar"></div>
<div class="unified-table" id="ordersTable">
<div class="loading">正在整理跨平台挂单...</div>
</div>
</div>
</section>
</div>
@ -2652,6 +2845,7 @@
let cachedConsoleData = null;
let revealSensitiveData = false;
const SENSITIVE_VISIBILITY_KEY = 'console_sensitive_visible_v2';
const HUB_PREFERENCE_KEY = 'console_hub_active_v1';
function formatNumber(value, digits = 2) {
const num = Number(value || 0);
@ -3011,6 +3205,42 @@
return revealSensitiveData ? formatMoney(value) : '$••••';
}
function initHubNavigation() {
document.querySelectorAll('[data-hub-target]').forEach((button) => {
button.addEventListener('click', () => {
setActiveHub(button.getAttribute('data-hub-target'));
});
});
let preferredHub = 'hubOverview';
try {
preferredHub = window.localStorage.getItem(HUB_PREFERENCE_KEY) || 'hubOverview';
} catch {
preferredHub = 'hubOverview';
}
setActiveHub(preferredHub);
}
function setActiveHub(target) {
document.querySelectorAll('[data-hub-target]').forEach((button) => {
button.classList.toggle('active', button.getAttribute('data-hub-target') === target);
});
document.querySelectorAll('.hub-pane').forEach((pane) => {
pane.classList.toggle('active', pane.id === target);
});
try {
window.localStorage.setItem(HUB_PREFERENCE_KEY, target);
} catch {
// ignore storage errors
}
}
function updateSidebarBadge(id, value) {
const el = document.getElementById(id);
if (!el) return;
el.textContent = value;
}
function initTabs() {
document.querySelectorAll('[data-tab-group]').forEach((groupEl) => {
const group = groupEl.getAttribute('data-tab-group');
@ -3022,12 +3252,6 @@
});
});
});
document.querySelectorAll('[data-ops-target]').forEach((button) => {
button.addEventListener('click', () => {
setActiveTab('ops', button.getAttribute('data-ops-target'));
});
});
}
function setActiveTab(group, target) {
@ -3037,11 +3261,6 @@
document.querySelectorAll(`[data-tab-pane="${group}"]`).forEach((pane) => {
pane.classList.toggle('active', pane.id === target);
});
if (group === 'ops') {
document.querySelectorAll('[data-ops-target]').forEach((card) => {
card.classList.toggle('active', card.getAttribute('data-ops-target') === target);
});
}
}
function getActiveTabTarget(group) {
@ -3055,60 +3274,33 @@
button.classList.toggle('muted', !hasData);
}
function renderOpsSummary(data) {
const container = document.getElementById('opsSummary');
function renderExecutionMonitor(data) {
const container = document.getElementById('executionMonitorStrip');
if (!container) return;
const platforms = data.platforms || {};
const cryptoAgent = data.crypto_agent || {};
const monitor = cryptoAgent.analysis_monitor || {};
const halts = cryptoAgent.platform_halts || {};
const executionControls = cryptoAgent.target_execution_controls || {};
const enabledPlatforms = ['paper', 'bitget'].filter((key) => platforms?.[key]?.enabled !== false);
const haltedCount = countHalted(halts);
const disabledCount = Object.values(executionControls).filter((item) => item && item.enabled === false).length;
const runtimeTone = toneClassForHealth(cryptoAgent.running ? monitor.last_cycle_status || monitor.last_analysis_status : 'stopped');
const riskTone = haltedCount > 0 ? 'danger' : ((disabledCount > 0) || (data.management?.attention_items || []).some((item) => item.severity === 'danger' || item.severity === 'warning') ? 'warn' : 'good');
const platformTone = haltedCount > 0 || disabledCount > 0 ? 'warn' : 'good';
const platformHeadline = `${enabledPlatforms.length} 平台`;
const platformDetail = `${sumPlatformPositions(platforms)} 持仓 / ${['paper', 'bitget'].map((key) => platforms?.[key]?.orders?.count || 0).reduce((a, b) => a + b, 0)} 挂单`;
const runtimeHeadline = monitor.last_heartbeat_at ? relativeTime(monitor.last_heartbeat_at) : '无心跳';
const runtimeDetail = `状态 ${String(monitor.last_cycle_status || monitor.last_analysis_status || 'idle').toUpperCase()} / ${monitor.current_cycle_total ? `${monitor.current_cycle_index || 0}/${monitor.current_cycle_total}` : '待机'}`;
const riskHeadline = haltedCount > 0 ? `${haltedCount} 停机` : (disabledCount > 0 ? `${disabledCount} 关闭` : '无停机');
const attentionItems = data.management?.attention_items || [];
const riskDetail = haltedCount > 0
? '已有平台触发停机或熔断,建议优先查看。'
: disabledCount > 0
? `有 ${disabledCount} 个执行目标被人工关闭自动交易。`
: attentionItems.length > 0
? `待处理 ${attentionItems.length} 项,建议检查风险与执行事件。`
: '当前没有明显风险阻塞。';
const executionLoop = data.monitoring?.execution_loop || {};
const priceMonitor = data.monitoring?.price_monitor || {};
const latestPriceCount = Object.keys(priceMonitor.latest_prices || {}).length;
container.innerHTML = `
<button class="ops-summary-card ${platformTone}" data-ops-target="opsPlatform">
<div class="ops-summary-kicker">平台概览</div>
<div class="ops-summary-headline ${platformTone}">${platformHeadline}</div>
<div class="ops-summary-detail">${platformDetail}</div>
</button>
<button class="ops-summary-card ${runtimeTone}" data-ops-target="opsRuntime">
<div class="ops-summary-kicker">心跳日志</div>
<div class="ops-summary-headline ${runtimeTone}">${runtimeHeadline}</div>
<div class="ops-summary-detail">${runtimeDetail}</div>
</button>
<button class="ops-summary-card ${riskTone}" data-ops-target="opsRisk">
<div class="ops-summary-kicker">停机熔断</div>
<div class="ops-summary-headline ${riskTone}">${riskHeadline}</div>
<div class="ops-summary-detail">${riskDetail}</div>
</button>
<div class="monitor-card">
<div class="title">价格链路</div>
<div class="main">${priceMonitor.running ? 'RUNNING' : 'STOPPED'} / ${(priceMonitor.mode || 'unknown').toUpperCase()}</div>
<div class="meta">
订阅标的 ${(priceMonitor.subscribed_symbols || []).length || 0} / 缓存价格 ${latestPriceCount}<br>
最近刷新 ${priceMonitor.checked_at ? formatTime(priceMonitor.checked_at) : '-'}
</div>
</div>
<div class="monitor-card">
<div class="title">模拟盘执行链路</div>
<div class="main">${executionLoop.running ? 'RUNNING' : 'STOPPED'}</div>
<div class="meta">
心跳 ${executionLoop.last_heartbeat_at ? `${relativeTime(executionLoop.last_heartbeat_at)} / ${formatTime(executionLoop.last_heartbeat_at)}` : '-'}<br>
活跃订单 ${executionLoop.active_orders ?? 0} / 标的 ${Array.isArray(executionLoop.last_symbols) && executionLoop.last_symbols.length ? executionLoop.last_symbols.join(', ') : '-'}<br>
错误 ${executionLoop.last_error || '无'}
</div>
</div>
`;
container.querySelectorAll('[data-ops-target]').forEach((button) => {
button.addEventListener('click', () => {
setActiveTab('ops', button.getAttribute('data-ops-target'));
});
});
setActiveTab('ops', getActiveTabTarget('ops') || 'opsPlatform');
}
function renderFocusSummary(data) {
@ -3228,39 +3420,9 @@
const executionEvents = data.execution_events || [];
const positions = data.management?.positions || [];
const orders = data.management?.orders || [];
const platformCount = ['paper', 'bitget']
.filter((key) => data.platforms?.[key]?.enabled !== false)
.length;
const lastSignals = Object.keys(data.crypto_agent?.last_signals || {}).length;
const previews = Object.keys(data.crypto_agent?.last_execution_preview || {}).length;
const statsTotal = Number(data.signals?.stats_7d?.total || 0);
const coordinationCount = lastSignals + previews + (statsTotal > 0 ? 1 : 0);
const haltedCount = countHalted(data.crypto_agent?.platform_halts || {});
const disabledCount = Object.values(data.crypto_agent?.target_execution_controls || {}).filter((item) => item && item.enabled === false).length;
const runtimeCount = (data.crypto_agent?.recent_analysis_events || []).length + (data.crypto_agent?.analysis_monitor?.last_heartbeat_at ? 1 : 0);
const blockedCount = (data.execution_events || []).filter((event) => event.event_type === 'execution_blocked_summary').length;
updateTabButton('command', 'commandOverview', '总览', coordinationCount + platformCount, coordinationCount + platformCount > 0);
updateTabButton('command', 'commandRisk', '风险', haltedCount + disabledCount + blockedCount, haltedCount + disabledCount + blockedCount > 0);
updateTabButton('command', 'commandRuntime', '运行', runtimeCount, runtimeCount > 0);
updateTabButton('command', 'commandLogs', '日志', executionEvents.length + runtimeCount, executionEvents.length + runtimeCount > 0);
updateTabButton('logs', 'logsExecution', '执行事件', executionEvents.length, executionEvents.length > 0);
updateTabButton('logs', 'logsAnalysis', '分析日志', runtimeCount, runtimeCount > 0);
updateTabButton('workspace', 'workspaceCoordination', '账户协同', coordinationCount, coordinationCount > 0);
updateTabButton('workspace', 'workspaceSignals', '信号缓存', recentSignals.length, recentSignals.length > 0);
updateTabButton('asset', 'assetPositions', '持仓', positions.length, positions.length > 0);
updateTabButton('asset', 'assetOrders', '挂单', orders.length, orders.length > 0);
const commandCurrent = getActiveTabTarget('command');
const commandChoices = [
{ target: 'commandRisk', hasData: haltedCount + disabledCount + blockedCount > 0 },
{ target: 'commandOverview', hasData: coordinationCount + platformCount > 0 },
{ target: 'commandRuntime', hasData: runtimeCount > 0 },
{ target: 'commandLogs', hasData: executionEvents.length + runtimeCount > 0 },
];
if (!commandChoices.find((item) => item.target === commandCurrent && item.hasData)) {
setActiveTab('command', commandChoices.find((item) => item.hasData)?.target || 'commandOverview');
}
const logsCurrent = getActiveTabTarget('logs');
const logsChoices = [
@ -3271,23 +3433,11 @@
setActiveTab('logs', logsChoices.find((item) => item.hasData)?.target || 'logsExecution');
}
const workspaceCurrent = getActiveTabTarget('workspace');
const workspaceChoices = [
{ target: 'workspaceCoordination', hasData: coordinationCount > 0 },
{ target: 'workspaceSignals', hasData: recentSignals.length > 0 },
];
if (!workspaceChoices.find((item) => item.target === workspaceCurrent && item.hasData)) {
setActiveTab('workspace', workspaceChoices.find((item) => item.hasData)?.target || 'workspaceCoordination');
}
const assetCurrent = getActiveTabTarget('asset');
const assetChoices = [
{ target: 'assetPositions', hasData: positions.length > 0 },
{ target: 'assetOrders', hasData: orders.length > 0 },
];
if (!assetChoices.find((item) => item.target === assetCurrent && item.hasData)) {
setActiveTab('asset', assetChoices.find((item) => item.hasData)?.target || 'assetPositions');
}
updateSidebarBadge('sidebarBadgeOverview', String((data.management?.attention_items || []).length));
updateSidebarBadge('sidebarBadgeExecution', `${positions.length}/${orders.length}`);
updateSidebarBadge('sidebarBadgeSignals', String(recentSignals.length));
updateSidebarBadge('sidebarBadgeRuntime', data.crypto_agent?.analysis_monitor?.last_heartbeat_at ? 'live' : 'idle');
updateSidebarBadge('sidebarBadgeLogs', String(executionEvents.length));
}
function toneClassForHealth(status) {
@ -4340,9 +4490,9 @@
renderHealthRibbon(data);
renderFocusSummary(data);
renderAlertStrip(data);
renderOpsSummary(data);
renderTopPriority(data);
renderOpsKpis(data);
renderExecutionMonitor(data);
renderSignalLifecycle(data);
renderPlatforms(data.platforms, data.crypto_agent?.platform_halts, data.crypto_agent?.target_execution_controls || {});
renderSignalStream(data.signals?.latest || []);
@ -4396,6 +4546,7 @@
loadSensitivePreference();
initTabs();
initHubNavigation();
applyAutoRefreshState();
loadConsole();
</script>