// dashboard.js — ChanMacro dashboard 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; } document.getElementById("db-status").textContent = "✓ " + d.date; document.getElementById("update-time").textContent = "更新于 " + new Date().toLocaleTimeString(); // Hero const regime = d.regime; document.getElementById("hero-regime").textContent = regime === "TREND" ? "趋势" : regime === "RANGE" ? "震荡" : "恐慌"; document.getElementById("hero-regime").className = "hero-regime regime-" + regime; document.getElementById("hero-badge").textContent = regime; document.getElementById("hero-badge").className = "badge-regime badge-" + regime; 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-sub").textContent = d.price_structure.label; document.getElementById("f-price-narr").textContent = d.price_structure.narrative; const b = d.breadth; document.getElementById("f-breadth").textContent = b.score.toFixed(0); document.getElementById("f-breadth").className = "factor-value " + (BUCKET_CLASS[b.bucket] || ""); 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; document.getElementById("f-oi").textContent = d.oi_state; document.getElementById("f-oi-sub").textContent = `分数: ${d.oi_score.toFixed(0)}`; document.getElementById("f-oi-narr").textContent = d.oi_narrative; document.getElementById("f-vol").textContent = d.volatility; document.getElementById("f-vol-sub").textContent = `分数: ${d.price_structure.score.toFixed(0)}`; } catch (e) { document.getElementById("db-status").textContent = "连接失败"; } } async function loadHistory() { try { 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"); if (regimeChart) regimeChart.destroy(); const ctx1 = document.getElementById("chart-regime").getContext("2d"); regimeChart = new Chart(ctx1, { type: "bar", data: { labels: dates, datasets: [{ label: "置信度", 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)}%` } } }, scales: { x: { ticks: { color: "#8b949e", maxTicksLimit: 15, maxRotation: 45 } }, y: { max: 100, ticks: { color: "#8b949e" } } } } }); // Breadth chart if (breadthChart) breadthChart.destroy(); const ctx2 = document.getElementById("chart-breadth").getContext("2d"); breadthChart = new Chart(ctx2, { 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 }, { label: "下跌", data: d.breadth.map(x => x.decline), borderColor: "#f85149", backgroundColor: "rgba(248,81,73,0.1)", 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 }, ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { labels: { color: "#8b949e", usePointStyle: true, boxWidth: 8 } } }, scales: { x: { ticks: { color: "#8b949e", maxTicksLimit: 15, maxRotation: 45 } }, y: { max: 50, ticks: { color: "#8b949e" } } } } }); } catch (e) { console.error("History load failed:", e); } } async function loadExpectancy() { const signal = document.getElementById("exp-signal").value; try { const r = await fetch(`/api/expectancy?signal=${signal}`); const d = await r.json(); if (d.error) { document.getElementById("exp-layers").innerHTML = `${d.error}`; 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"); let html = ""; for (const l of d.layers) { html += ` ${l.name} ${l.samples} ${l.effective_samples.toFixed(0)} ${l.raw_winrate ? (l.raw_winrate * 100).toFixed(1) + "%" : "—"} ${(l.posterior_winrate * 100).toFixed(1)}% ${l.avg_return ? (l.avg_return > 0 ? "+" : "") + l.avg_return.toFixed(1) + "%" : "—"} `; } document.getElementById("exp-layers").innerHTML = html; let summary = `最终估计: ${(d.final_estimate * 100).toFixed(1)}%`; if (d.avg_return_7d) summary += ` · 平均收益: ${d.avg_return_7d > 0 ? "+" : ""}${d.avg_return_7d.toFixed(1)}%`; if (d.profit_factor) summary += ` · 盈亏比: ${d.profit_factor}`; document.getElementById("exp-summary").innerHTML = summary; } catch (e) { console.error("Expectancy load failed:", e); } } // Init loadState(); loadHistory(); loadExpectancy();