172 lines
7.3 KiB
JavaScript
172 lines
7.3 KiB
JavaScript
// 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").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;
|
|
|
|
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"));
|
|
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").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;
|
|
|
|
document.getElementById("f-vol").textContent = d.volatility;
|
|
document.getElementById("f-vol").style.color =
|
|
d.volatility === "LOW_VOL" ? "#58a6ff" : d.volatility === "NORMAL_VOL" ? "#e6edf3" :
|
|
d.volatility === "HIGH_VOL" ? "#d29922" : "#f85149";
|
|
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 = `<tr><td colspan="6">${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");
|
|
|
|
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.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>
|
|
</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);
|
|
}
|
|
}
|
|
|
|
// Init
|
|
loadState();
|
|
loadHistory();
|
|
loadExpectancy();
|