web: professional trading-terminal redesign — dark theme, chart grid, progress bars

This commit is contained in:
jackyu66git
2026-06-24 18:28:40 +08:00
parent f391020f78
commit 7813e319b4
2 changed files with 208 additions and 200 deletions
+82 -93
View File
@@ -1,60 +1,75 @@
// dashboard.js — ChanMacro dashboard
// dashboard.js — ChanMacro
const C = { TREND: "#3fb950", RANGE: "#d29922", PANIC: "#f85149" };
let regimeChart = null, breadthChart = null;
const REGIME_COLORS = { TREND: "#3fb950", RANGE: "#d29922", PANIC: "#f85149" };
const BUCKET_CLASS = { EXTREME: "bucket-EXTREME", STRONG: "bucket-STRONG",
NORMAL: "bucket-NORMAL", WEAK: "bucket-WEAK", PANIC: "bucket-PANIC" };
async function loadState() {
try {
const r = await fetch("/api/state");
const d = await r.json();
if (d.error) { document.getElementById("db-status").textContent = d.error; return; }
if (d.error) { document.getElementById("update-time").textContent = d.error; return; }
document.getElementById("db-status").textContent = "✓ " + d.date;
document.getElementById("update-time").textContent = "更新于 " + new Date().toLocaleTimeString();
document.getElementById("update-time").textContent = d.date;
// Hero
const regime = d.regime;
document.getElementById("hero-regime").textContent = regime === "TREND" ? "趋势" : regime === "RANGE" ? "震荡" : "恐慌";
document.getElementById("hero-regime").className = "hero-regime regime-" + regime;
const names = { TREND: "TREND", RANGE: "RANGE", PANIC: "PANIC" };
document.getElementById("hero-regime").textContent = names[regime] || regime;
document.getElementById("hero-regime").className = "regime-name " + regime.toLowerCase();
document.getElementById("hero-badge").textContent = regime;
document.getElementById("hero-badge").className = "badge-regime badge-" + regime;
document.getElementById("hero-badge").className = "regime-badge " + regime.toLowerCase();
document.getElementById("hero-conf").textContent = (d.regime_confidence * 100).toFixed(0) + "%";
document.getElementById("hero-maturity").textContent = d.regime_maturity.toFixed(0) + "/100";
// Factors
document.getElementById("f-price").textContent = d.price_structure.score.toFixed(0);
document.getElementById("f-price").style.color =
document.getElementById("hero-maturity").textContent = d.regime_maturity.toFixed(0);
document.getElementById("hero-ps").textContent = d.price_structure.score.toFixed(0);
document.getElementById("hero-ps").style.color =
d.price_structure.score >= 60 ? "#3fb950" : d.price_structure.score >= 40 ? "#d29922" : "#f85149";
document.getElementById("f-price-sub").textContent = d.price_structure.label;
document.getElementById("f-price-narr").textContent = d.price_structure.narrative;
document.getElementById("hero-br").textContent = d.breadth.score.toFixed(0);
document.getElementById("hero-br").style.color =
d.breadth.bucket === "EXTREME" || d.breadth.bucket === "STRONG" ? "#3fb950" :
d.breadth.bucket === "WEAK" || d.breadth.bucket === "PANIC" ? "#f85149" : "#d29922";
const b = d.breadth;
document.getElementById("f-breadth").textContent = b.score.toFixed(0);
document.getElementById("f-breadth").setAttribute("style",
"color: " + (b.bucket === "EXTREME" || b.bucket === "STRONG" ? "#3fb950" :
b.bucket === "WEAK" || b.bucket === "PANIC" ? "#f85149" :
b.bucket === "NORMAL" ? "#d29922" : "#e6edf3"));
// Factor cards
const ps = d.price_structure;
document.getElementById("f-price").textContent = ps.score.toFixed(0);
document.getElementById("f-price").style.color =
ps.score >= 60 ? "#3fb950" : ps.score >= 40 ? "#d29922" : "#f85149";
document.getElementById("f-price-sub").textContent =
`趋势 ${ps.trend.toFixed(0)} · 波动 ${ps.vol_comp.toFixed(0)} · 动量 ${ps.momentum.toFixed(0)}`;
document.getElementById("bar-price").style.width = ps.score + "%";
document.getElementById("bar-price").style.background =
ps.score >= 60 ? "#3fb950" : ps.score >= 40 ? "#d29922" : "#f85149";
const br = d.breadth;
document.getElementById("f-breadth").textContent = br.score.toFixed(0);
document.getElementById("f-breadth").style.color =
br.bucket === "EXTREME" || br.bucket === "STRONG" ? "#3fb950" :
br.bucket === "WEAK" || br.bucket === "PANIC" ? "#f85149" : "#d29922";
document.getElementById("f-breadth-sub").textContent =
`${b.bucket} · T20=${b.top20.toFixed(0)} T50=${b.top50.toFixed(0)} div=${b.divergence > 0 ? "+" : ""}${b.divergence.toFixed(0)}`;
document.getElementById("f-breadth-narr").textContent = b.narrative;
`${br.bucket} · T20=${br.top20.toFixed(0)} T50=${br.top50.toFixed(0)}`;
document.getElementById("bar-breadth").style.width = br.score + "%";
document.getElementById("bar-breadth").style.background =
br.bucket === "EXTREME" || br.bucket === "STRONG" ? "#3fb950" :
br.bucket === "WEAK" || br.bucket === "PANIC" ? "#f85149" : "#d29922";
document.getElementById("f-oi").textContent = d.oi_state;
document.getElementById("f-oi").textContent = d.oi_state.toUpperCase().replace(" ", "\n");
document.getElementById("f-oi").style.color =
d.oi_state === "New Longs" ? "#3fb950" : d.oi_state === "New Shorts" ? "#f85149" :
d.oi_state === "Short Covering" ? "#d29922" : d.oi_state === "Long Exit" ? "#f85149" : "#e6edf3";
document.getElementById("f-oi-sub").textContent = `分数: ${d.oi_score.toFixed(0)}`;
document.getElementById("f-oi-narr").textContent = d.oi_narrative;
d.oi_state === "New Longs" ? "#3fb950" : d.oi_state.includes("Short") || d.oi_state === "Long Exit" ? "#f85149" : "#8b949e";
document.getElementById("f-oi-sub").textContent = d.oi_narrative;
document.getElementById("f-vol").textContent = d.volatility;
const vm = { LOW_VOL: "低波动", NORMAL_VOL: "正常", HIGH_VOL: "高波动", EXPLOSIVE_VOL: "极端" };
document.getElementById("f-vol").textContent = vm[d.volatility] || d.volatility;
document.getElementById("f-vol").style.color =
d.volatility === "LOW_VOL" ? "#58a6ff" : d.volatility === "NORMAL_VOL" ? "#e6edf3" :
d.volatility === "LOW_VOL" ? "#58a6ff" : d.volatility === "NORMAL_VOL" ? "#8b949e" :
d.volatility === "HIGH_VOL" ? "#d29922" : "#f85149";
document.getElementById("f-vol-sub").textContent = `分数: ${d.price_structure.score.toFixed(0)}`;
document.getElementById("f-vol-sub").textContent = d.volatility;
document.getElementById("bar-vol").style.width =
(d.volatility === "EXPLOSIVE_VOL" ? 95 : d.volatility === "HIGH_VOL" ? 70 :
d.volatility === "NORMAL_VOL" ? 40 : 20) + "%";
document.getElementById("bar-vol").style.background =
d.volatility === "EXPLOSIVE_VOL" ? "#f85149" : d.volatility === "HIGH_VOL" ? "#d29922" :
d.volatility === "NORMAL_VOL" ? "#8b949e" : "#58a6ff";
} catch (e) {
document.getElementById("db-status").textContent = "连接失败";
document.getElementById("update-time").textContent = "连接失败";
}
}
@@ -63,72 +78,50 @@ async function loadHistory() {
const r = await fetch("/api/history?days=60");
const d = await r.json();
// Regime chart
const dates = d.regimes.map(x => x.date);
const regimes = d.regimes.map(x => x.regime);
const colors = regimes.map(r => REGIME_COLORS[r] || "#8b949e");
const colors = d.regimes.map(x => C[x.regime] || "#5c6675");
if (regimeChart) regimeChart.destroy();
const ctx1 = document.getElementById("chart-regime").getContext("2d");
regimeChart = new Chart(ctx1, {
regimeChart = new Chart(document.getElementById("chart-regime").getContext("2d"), {
type: "bar",
data: {
labels: dates,
datasets: [{
label: "置信度",
data: d.regimes.map(x => x.confidence * 100),
backgroundColor: colors,
borderWidth: 0,
borderRadius: 2,
}]
},
data: { labels: dates, datasets: [{ data: d.regimes.map(x => x.confidence * 100),
backgroundColor: colors, borderWidth: 0, borderRadius: 2 }] },
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: ctx => `${d.regimes[ctx.dataIndex].regime} · ${ctx.raw.toFixed(0)}%`
}
}
},
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: "#8b949e", maxTicksLimit: 15, maxRotation: 45 } },
y: { max: 100, ticks: { color: "#8b949e" } }
x: { ticks: { color: "#5c6675", maxTicksLimit: 15, maxRotation: 45, font: { size: 10 } },
grid: { color: "#151a23" } },
y: { max: 100, ticks: { color: "#5c6675", font: { size: 10 } }, grid: { color: "#151a23" } }
}
}
});
// Breadth chart
if (breadthChart) breadthChart.destroy();
const ctx2 = document.getElementById("chart-breadth").getContext("2d");
breadthChart = new Chart(ctx2, {
breadthChart = new Chart(document.getElementById("chart-breadth").getContext("2d"), {
type: "line",
data: {
labels: d.breadth.map(x => x.date),
datasets: [
{ label: "上涨", data: d.breadth.map(x => x.advance), borderColor: "#3fb950",
backgroundColor: "rgba(63,185,80,0.1)", fill: true, tension: 0.3, pointRadius: 0 },
backgroundColor: "rgba(63,185,80,0.08)", fill: true, tension: 0.3, pointRadius: 0 },
{ label: "下跌", data: d.breadth.map(x => x.decline), borderColor: "#f85149",
backgroundColor: "rgba(248,81,73,0.1)", fill: true, tension: 0.3, pointRadius: 0 },
backgroundColor: "rgba(248,81,73,0.06)", fill: true, tension: 0.3, pointRadius: 0 },
{ label: ">EMA20", data: d.breadth.map(x => x.above_ema20), borderColor: "#58a6ff",
borderDash: [4, 2], tension: 0.3, pointRadius: 0 },
borderDash: [3, 3], tension: 0.3, pointRadius: 0 },
]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { labels: { color: "#8b949e", usePointStyle: true, boxWidth: 8 } } },
responsive: true, maintainAspectRatio: false,
plugins: { legend: { labels: { color: "#5c6675", usePointStyle: true, boxWidth: 6, font: { size: 10 } } } },
scales: {
x: { ticks: { color: "#8b949e", maxTicksLimit: 15, maxRotation: 45 } },
y: { max: 50, ticks: { color: "#8b949e" } }
x: { ticks: { color: "#5c6675", maxTicksLimit: 15, maxRotation: 45, font: { size: 10 } },
grid: { color: "#151a23" } },
y: { ticks: { color: "#5c6675", font: { size: 10 } }, grid: { color: "#151a23" } }
}
}
});
} catch (e) {
console.error("History load failed:", e);
}
} catch (e) { console.error(e); }
}
async function loadExpectancy() {
@@ -136,36 +129,32 @@ async function loadExpectancy() {
try {
const r = await fetch(`/api/expectancy?signal=${signal}`);
const d = await r.json();
if (d.error) { document.getElementById("exp-layers").innerHTML = `<tr><td colspan="6">${d.error}</td></tr>`; return; }
if (d.error) { document.getElementById("exp-layers").innerHTML =
`<tr><td colspan="6" style="color:#f85149">${d.error}</td></tr>`; return; }
document.getElementById("exp-sufficiency").textContent = d.sufficiency;
document.getElementById("exp-sufficiency").className =
"badge " + (d.sufficiency === "HIGH" ? "bg-success" : d.sufficiency === "MEDIUM" ? "bg-warning" :
d.sufficiency === "LOW" ? "bg-danger" : "bg-secondary");
const el = document.getElementById("exp-sufficiency");
el.textContent = d.sufficiency;
el.className = "suff suff-" + d.sufficiency;
let html = "";
for (const l of d.layers) {
html += `<tr>
<td>${l.name}</td>
<td>${l.samples}</td>
<td>${l.effective_samples.toFixed(0)}</td>
<td>${l.name}</td><td>${l.samples}</td><td>${l.effective_samples.toFixed(0)}</td>
<td>${l.raw_winrate ? (l.raw_winrate * 100).toFixed(1) + "%" : "—"}</td>
<td><strong>${(l.posterior_winrate * 100).toFixed(1)}%</strong></td>
<td>${l.avg_return ? (l.avg_return > 0 ? "+" : "") + l.avg_return.toFixed(1) + "%" : "—"}</td>
<td style="color:${l.avg_return > 0 ? '#3fb950' : l.avg_return < 0 ? '#f85149' : '#8b949e'}">${l.avg_return ? (l.avg_return > 0 ? "+" : "") + l.avg_return.toFixed(2) + "%" : "—"}</td>
</tr>`;
}
document.getElementById("exp-layers").innerHTML = html;
let summary = `最终估计: <strong>${(d.final_estimate * 100).toFixed(1)}%</strong>`;
if (d.avg_return_7d) summary += ` · 平均收益: <strong>${d.avg_return_7d > 0 ? "+" : ""}${d.avg_return_7d.toFixed(1)}%</strong>`;
if (d.profit_factor) summary += ` · 盈亏比: <strong>${d.profit_factor}</strong>`;
document.getElementById("exp-summary").innerHTML = summary;
} catch (e) {
console.error("Expectancy load failed:", e);
}
let s = `后验胜率 <strong style="color:#58a6ff">${(d.final_estimate * 100).toFixed(1)}%</strong>`;
if (d.avg_return_7d) s += ` · 平均收益 <strong>${d.avg_return_7d > 0 ? "+" : ""}${d.avg_return_7d.toFixed(2)}%</strong>`;
if (d.profit_factor) s += ` · 盈亏比 <strong>${d.profit_factor}</strong>`;
if (d.max_adverse) s += ` · MAE <strong>${d.max_adverse.toFixed(1)}%</strong>`;
document.getElementById("exp-summary").innerHTML = s;
} catch (e) { console.error(e); }
}
// Init
loadState();
loadHistory();
loadExpectancy();
+126 -107
View File
@@ -4,138 +4,157 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ChanMacro — 市场状态</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<style>
:root { --bg: #0d1117; --card: #161b22; --border: #30363d; --text: #e6edf3; --muted: #8b949e;
--green: #3fb950; --red: #f85149; --orange: #d29922; --blue: #58a6ff; }
body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
.card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; }
.hero-regime { font-size: 3rem; font-weight: 700; }
.hero-conf { font-size: 1.2rem; color: var(--muted); }
.factor-value { font-size: 2.2rem; font-weight: 700; color: var(--text); }
.factor-label { color: var(--muted); font-size: 0.85rem; }
.regime-TREND { color: var(--green); }
.regime-RANGE { color: var(--orange); }
.regime-PANIC { color: var(--red); }
.bucket-EXTREME, .bucket-STRONG { color: var(--green); }
.bucket-NORMAL { color: var(--orange); }
.bucket-WEAK, .bucket-PANIC { color: var(--red); }
.badge-regime { font-size: 0.85rem; padding: 4px 12px; border-radius: 20px; }
.badge-TREND { background: #1a3a1a; color: var(--green); }
.badge-RANGE { background: #3a2a0a; color: var(--orange); }
.badge-PANIC { background: #3a0a0a; color: var(--red); }
.narrative { color: var(--muted); font-size: 0.9rem; }
canvas { max-height: 300px; }
.text-muted { color: var(--muted) !important; }
.table { color: var(--text); }
.table-dark { --bs-table-color: var(--text); --bs-table-bg: var(--card); }
.form-select { background-color: var(--card); color: var(--text); border-color: var(--border); }
.btn-primary { background-color: var(--blue); border-color: var(--blue); }
strong { color: var(--text); }
small { color: var(--muted); }
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #0a0e14; color: #c9d1d9; font-family: -apple-system, BlinkMacSystemFont, "SF Mono", monospace; }
.app { max-width: 1200px; margin: 0 auto; padding: 20px 24px; }
/* Header */
.header { display: flex; justify-content: space-between; align-items: flex-end; padding: 20px 0 28px;
border-bottom: 1px solid #1c2333; margin-bottom: 24px; }
.header h1 { font-size: 22px; font-weight: 600; letter-spacing: 1px; }
.header h1 span { color: #58a6ff; }
.header .time { color: #5c6675; font-size: 13px; }
.dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: #3fb950;
margin-right: 6px; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
/* Regime Hero */
.hero { display: flex; gap: 16px; margin-bottom: 24px; }
.hero-card { flex: 1; background: #11161e; border: 1px solid #1c2333; border-radius: 8px; padding: 20px 24px; }
.hero-card.main { flex: 2; display: flex; align-items: center; gap: 28px; }
.regime-badge { display: inline-block; padding: 5px 16px; border-radius: 4px; font-size: 13px;
font-weight: 600; letter-spacing: 2px; }
.regime-badge.trend { background: rgba(63,185,80,0.12); color: #3fb950; border: 1px solid rgba(63,185,80,0.3); }
.regime-badge.range { background: rgba(210,153,34,0.12); color: #d29922; border: 1px solid rgba(210,153,34,0.3); }
.regime-badge.panic { background: rgba(248,81,73,0.12); color: #f85149; border: 1px solid rgba(248,81,73,0.3); }
.regime-name { font-size: 42px; font-weight: 700; letter-spacing: 2px; }
.regime-name.trend { color: #3fb950; }
.regime-name.range { color: #d29922; }
.regime-name.panic { color: #f85149; }
.hero-stat { text-align: center; }
.hero-stat .val { font-size: 28px; font-weight: 600; color: #e6edf3; }
.hero-stat .lbl { font-size: 11px; color: #5c6675; letter-spacing: 1px; margin-top: 4px; }
/* Factor Grid */
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 24px; }
.fcard { background: #11161e; border: 1px solid #1c2333; border-radius: 8px; padding: 18px 20px; }
.fcard .title { font-size: 11px; color: #5c6675; letter-spacing: 1.5px; margin-bottom: 10px; }
.fcard .score { font-size: 38px; font-weight: 700; margin-bottom: 4px; }
.fcard .sub { font-size: 12px; color: #5c6675; }
.fcard .bar-wrap { height: 3px; background: #1c2333; border-radius: 2px; margin-top: 12px; }
.fcard .bar { height: 100%; border-radius: 2px; transition: width 0.6s; }
/* Charts */
.charts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 24px; }
.chart-box { background: #11161e; border: 1px solid #1c2333; border-radius: 8px; padding: 18px 20px; }
.chart-box h3 { font-size: 12px; color: #5c6675; letter-spacing: 1.5px; margin-bottom: 14px; }
.chart-box canvas { max-height: 260px; }
/* Expectancy */
.exp { background: #11161e; border: 1px solid #1c2333; border-radius: 8px; padding: 18px 20px; }
.exp h3 { font-size: 12px; color: #5c6675; letter-spacing: 1.5px; margin-bottom: 14px; }
.exp-row { display: flex; gap: 12px; align-items: center; margin-bottom: 14px; }
.exp select { background: #0a0e14; color: #c9d1d9; border: 1px solid #1c2333; padding: 6px 12px;
border-radius: 4px; font-size: 13px; }
.exp button { background: #1c3a5c; color: #58a6ff; border: 1px solid #2d4f7c; padding: 6px 18px;
border-radius: 4px; cursor: pointer; font-size: 13px; }
.exp button:hover { background: #254d7a; }
.exp .suff { font-size: 11px; padding: 3px 10px; border-radius: 3px; }
.suff-HIGH { background: rgba(63,185,80,0.12); color: #3fb950; }
.suff-MEDIUM { background: rgba(210,153,34,0.12); color: #d29922; }
.suff-LOW { background: rgba(248,81,73,0.12); color: #f85149; }
.suff-INSUFFICIENT { background: rgba(92,102,117,0.12); color: #5c6675; }
table { width: 100%; border-collapse: collapse; font-size: 13px; }
th { text-align: left; color: #5c6675; font-weight: 500; padding: 8px 10px; border-bottom: 1px solid #1c2333; }
td { padding: 7px 10px; border-bottom: 1px solid #0e1219; color: #8b949e; }
td strong { color: #e6edf3; }
.exp-summary { margin-top: 14px; font-size: 13px; color: #8b949e; padding: 10px 14px;
background: #0d1117; border-radius: 6px; border-left: 3px solid #58a6ff; }
.exp-summary strong { color: #e6edf3; }
</style>
</head>
<body>
<div class="container-fluid py-3 px-4">
<div class="app">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="header">
<div>
<h4 class="mb-0">ChanMacro <span class="text-muted fs-6">市场状态</span></h4>
<small class="text-muted" id="update-time"></small>
</div>
<div>
<span class="badge bg-secondary" id="db-status">加载中...</span>
<h1><span>Chan</span>Macro</h1>
</div>
<div class="time"><span class="dot"></span> <span id="update-time">加载中...</span></div>
</div>
<!-- Hero: Regime -->
<div class="card p-4 mb-3 text-center">
<div class="hero-conf mb-1">当前制度</div>
<div class="hero-regime" id="hero-regime"></div>
<div>
<span class="badge-regime" id="hero-badge"></span>
<span class="ms-2" style="color:#8b949e">置信度 <strong id="hero-conf" style="color:#e6edf3"></strong></span>
<span class="ms-2" style="color:#8b949e">成熟度 <strong id="hero-maturity" style="color:#e6edf3"></strong></span>
<!-- Regime Hero -->
<div class="hero">
<div class="hero-card main">
<div>
<div class="regime-badge" id="hero-badge"></div>
<div class="regime-name" id="hero-regime"></div>
</div>
<div style="display:flex; gap:32px; margin-left:auto;">
<div class="hero-stat"><div class="val" id="hero-conf"></div><div class="lbl">置信度</div></div>
<div class="hero-stat"><div class="val" id="hero-maturity"></div><div class="lbl">成熟度</div></div>
</div>
</div>
<div class="hero-card" style="flex:1">
<div class="hero-stat"><div class="val" id="hero-ps"></div><div class="lbl">价格结构</div></div>
</div>
<div class="hero-card" style="flex:1">
<div class="hero-stat"><div class="val" id="hero-br"></div><div class="lbl">市场广度</div></div>
</div>
</div>
<!-- 4 Factor Cards -->
<div class="row g-3 mb-3">
<div class="col-md-3">
<div class="card p-3 h-100">
<div class="factor-label">价格结构</div>
<div class="factor-value" id="f-price"></div>
<div class="text-muted small" id="f-price-sub"></div>
<div class="narrative mt-1" id="f-price-narr"></div>
</div>
<div class="grid">
<div class="fcard">
<div class="title">价格结构 PRICE STRUCTURE</div>
<div class="score" id="f-price"></div>
<div class="sub" id="f-price-sub"></div>
<div class="bar-wrap"><div class="bar" id="bar-price"></div></div>
</div>
<div class="col-md-3">
<div class="card p-3 h-100">
<div class="factor-label">市场广度</div>
<div class="factor-value" id="f-breadth"></div>
<div class="text-muted small" id="f-breadth-sub"></div>
<div class="narrative mt-1" id="f-breadth-narr"></div>
</div>
<div class="fcard">
<div class="title">市场广度 BREADTH</div>
<div class="score" id="f-breadth"></div>
<div class="sub" id="f-breadth-sub"></div>
<div class="bar-wrap"><div class="bar" id="bar-breadth"></div></div>
</div>
<div class="col-md-3">
<div class="card p-3 h-100">
<div class="factor-label">OI 状态</div>
<div class="factor-value fs-4" id="f-oi"></div>
<div class="text-muted small" id="f-oi-sub"></div>
<div class="narrative mt-1" id="f-oi-narr"></div>
</div>
<div class="fcard">
<div class="title">持仓状态 OI MATRIX</div>
<div class="score" id="f-oi" style="font-size:24px"></div>
<div class="sub" id="f-oi-sub"></div>
</div>
<div class="col-md-3">
<div class="card p-3 h-100">
<div class="factor-label">波动率</div>
<div class="factor-value" id="f-vol"></div>
<div class="text-muted small" id="f-vol-sub"></div>
</div>
<div class="fcard">
<div class="title">波动率 VOLATILITY</div>
<div class="score" id="f-vol"></div>
<div class="sub" id="f-vol-sub"></div>
<div class="bar-wrap"><div class="bar" id="bar-vol"></div></div>
</div>
</div>
<!-- Charts Row -->
<div class="row g-3 mb-3">
<div class="col-md-6">
<div class="card p-3">
<h6 class="mb-3">制度历史</h6>
<canvas id="chart-regime"></canvas>
</div>
</div>
<div class="col-md-6">
<div class="card p-3">
<h6 class="mb-3">市场广度</h6>
<canvas id="chart-breadth"></canvas>
</div>
</div>
<!-- Charts -->
<div class="charts">
<div class="chart-box"><h3>制度历史 REGIME HISTORY</h3><canvas id="chart-regime"></canvas></div>
<div class="chart-box"><h3>市场广度 BREADTH</h3><canvas id="chart-breadth"></canvas></div>
</div>
<!-- Expectancy -->
<div class="card p-3">
<h6 class="mb-3">信号期望查询</h6>
<div class="row g-2 align-items-end">
<div class="col-auto">
<select class="form-select form-select-sm" id="exp-signal">
<option value="B3">B3 (三买)</option><option value="B2">B2 (二买)</option><option value="B1">B1 (一买)</option>
<option value="S3">S3 (三卖)</option><option value="S2">S2 (二卖)</option><option value="S1">S1 (一卖)</option>
</select>
</div>
<div class="col-auto">
<button class="btn btn-sm btn-primary" onclick="loadExpectancy()">查询</button>
</div>
<div class="col-auto">
<span class="badge bg-secondary" id="exp-sufficiency"></span>
</div>
<div class="exp">
<h3>信号期望 SIGNAL EXPECTANCY</h3>
<div class="exp-row">
<select id="exp-signal">
<option value="B3">B3 · 三买</option><option value="B2">B2 · 二买</option><option value="B1">B1 · 一买</option>
<option value="S3">S3 · 三卖</option><option value="S2">S2 · 二卖</option><option value="S1">S1 · 一卖</option>
</select>
<button onclick="loadExpectancy()">查询</button>
<span class="suff" id="exp-sufficiency"></span>
</div>
<div class="table-responsive mt-2">
<table class="table table-sm table-dark mb-0" style="--bs-table-bg:#161b22">
<thead><tr><th>层级</th><th>样本</th><th>有效样本</th><th>原始胜率</th><th>后验胜率</th><th>平均收益</th></tr></thead>
<tbody id="exp-layers"></tbody>
</table>
</div>
<div class="mt-2 text-muted small" id="exp-summary"></div>
<table>
<thead><tr><th>层级</th><th>样本</th><th>有效样本</th><th>原始胜率</th><th>后验胜率</th><th>平均收益</th></tr></thead>
<tbody id="exp-layers"></tbody>
</table>
<div class="exp-summary" id="exp-summary"></div>
</div>
</div>