/** * 中控振幅统计:OKX ETH/BTC 时段点数振幅. */ (function () { const page = document.getElementById("page-amp-stats"); if (!page) return; let meta = null; let lastResult = null; let pageNo = 1; let bound = false; const el = (id) => document.getElementById(id); async function apiFetch(url, opts) { const r = await fetch(url, { credentials: "same-origin", ...(opts || {}) }); const ct = (r.headers.get("content-type") || "").toLowerCase(); if (ct.includes("application/json")) { const data = await r.json(); if (!r.ok) throw new Error((data && (data.detail || data.msg)) || r.statusText || "请求失败"); return data; } if (!r.ok) throw new Error(r.statusText || "请求失败"); return r; } function esc(s) { return String(s ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function setStatus(msg) { const s = el("amp-status"); if (s) s.textContent = msg || ""; } function setView(view) { const isHist = view === "history"; el("amp-panel-stats")?.classList.toggle("hidden", isHist); el("amp-panel-history")?.classList.toggle("hidden", !isHist); page.querySelectorAll(".amp-view-tab").forEach((btn) => { const on = btn.getAttribute("data-view") === view; btn.classList.toggle("is-active", on); btn.setAttribute("aria-selected", on ? "true" : "false"); }); if (isHist) void loadHistory(); } function syncCustomDays() { const period = el("amp-period")?.value || "2m"; const wrap = el("amp-custom-wrap"); if (wrap) wrap.classList.toggle("hidden", period !== "custom"); } function fillMetaControls() { const hourSel = el("amp-start-hour"); if (hourSel && !hourSel.options.length) { for (let h = 0; h < 24; h++) { const opt = document.createElement("option"); opt.value = String(h); opt.textContent = String(h).padStart(2, "0") + ":00"; if (h === 16) opt.selected = true; hourSel.appendChild(opt); } } } function renderSummary(summary, result) { const box = el("amp-summary"); if (!box) return; const s = summary || {}; if (!s.sample_count) { box.innerHTML = '

暂无汇总

'; return; } box.innerHTML = `
` + `
样本${esc(s.sample_count)}
` + `
最大振幅${esc(s.max_amplitude)} (${esc(s.max_amplitude_day)})
` + `
振幅均值${esc(s.avg_amplitude)}
` + `
振幅中位${esc(s.median_amplitude)}
` + `
开→高最大/均${esc(s.max_up_points)} / ${esc(s.avg_up_points)}
` + `
开→低最大/均${esc(s.max_down_points)} / ${esc(s.avg_down_points)}
` + `
涨/跌窗占比${esc(s.up_day_ratio)} / ${esc(s.down_day_ratio)}
` + `
价源${esc(result && result.price_source)}
` + `
`; } function renderTable(pagePayload) { const body = el("amp-table-body"); const pager = el("amp-pager"); if (!body) return; const rows = (pagePayload && pagePayload.rows) || []; if (!rows.length) { body.innerHTML = '暂无数据'; } else { body.innerHTML = rows .map( (r) => `` + `${esc(r.settlement_day)}` + `${esc(r.window_start)}` + `${esc(r.open)}` + `${esc(r.high)}` + `${esc(r.low)}` + `${esc(r.close)}` + `${esc(r.up_points)}` + `${esc(r.down_points)}` + `${esc(r.amplitude)}` + `${esc(r.change)}` + `` ) .join(""); } if (pager && pagePayload) { pager.innerHTML = `` + `第 ${esc(pagePayload.page)} / ${esc(pagePayload.total_pages)} 页 · 共 ${esc(pagePayload.total)} 天` + ``; el("amp-page-prev")?.addEventListener("click", () => { if (pageNo > 1) { pageNo -= 1; void compute(false); } }); el("amp-page-next")?.addEventListener("click", () => { if (pagePayload.page < pagePayload.total_pages) { pageNo += 1; void compute(false); } }); } } async function compute(resetPage) { if (resetPage) pageNo = 1; const symbol = el("amp-symbol")?.value || "eth"; const startHour = Number(el("amp-start-hour")?.value || 16); const period = el("amp-period")?.value || "2m"; const customDays = Number(el("amp-custom-days")?.value || 60); setStatus("计算中…(首次拉取 OKX K 线可能需数十秒)"); try { const data = await apiFetch("/api/amp-stats/compute", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ symbol, start_hour: startHour, period, custom_days: period === "custom" ? customDays : null, page: pageNo, page_size: 20, }), }); lastResult = data.result || null; renderSummary(lastResult && lastResult.summary, lastResult); renderTable(data.page); const miss = (lastResult && lastResult.missing_count) || 0; setStatus( miss ? `完成 · 样本 ${(lastResult.summary || {}).sample_count || 0} · 缺 ${miss} 天` : `完成 · 样本 ${(lastResult.summary || {}).sample_count || 0}` ); } catch (e) { setStatus(String(e && e.message ? e.message : e)); } } async function saveHistory() { if (!lastResult) { setStatus("请先计算"); return; } try { await apiFetch("/api/amp-stats/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ result: lastResult }), }); setStatus("已保存到历史"); } catch (e) { setStatus(String(e && e.message ? e.message : e)); } } function downloadCurrent() { if (!lastResult) { setStatus("请先计算"); return; } const symbol = el("amp-symbol")?.value || "eth"; const startHour = Number(el("amp-start-hour")?.value || 16); const period = el("amp-period")?.value || "2m"; const customDays = Number(el("amp-custom-days")?.value || 60); const q = new URLSearchParams({ symbol, start_hour: String(startHour), period, }); if (period === "custom") q.set("custom_days", String(customDays)); window.location.href = "/api/amp-stats/export?" + q.toString(); } async function loadHistory() { const box = el("amp-history-list"); if (!box) return; box.innerHTML = '

加载中…

'; try { const data = await apiFetch("/api/amp-stats/history?limit=50"); const items = data.items || []; if (!items.length) { box.innerHTML = '

暂无历史

'; return; } box.innerHTML = items .map( (it) => `
` + `
` + `${esc(it.symbol_label || it.symbol)} · ${esc(String(it.start_hour).padStart(2, "0"))}:00→16:00 · ${esc(it.period)}` + `
${esc(it.created_at)} · 样本 ${esc(it.sample_count)} · 最大振幅 ${esc(it.max_amplitude)} (${esc(it.max_amplitude_day)})
` + `
` + `
` + `` + `` + `` + `
` ) .join(""); box.querySelectorAll(".amp-hist-card").forEach((card) => { const id = card.getAttribute("data-id"); card.querySelector(".amp-hist-view")?.addEventListener("click", () => void openHistory(id)); card.querySelector(".amp-hist-dl")?.addEventListener("click", () => { window.location.href = "/api/amp-stats/export?history_id=" + encodeURIComponent(id); }); card.querySelector(".amp-hist-del")?.addEventListener("click", async () => { if (!confirm("删除该历史记录?")) return; await apiFetch("/api/amp-stats/history/" + encodeURIComponent(id), { method: "DELETE" }); void loadHistory(); }); }); } catch (e) { box.innerHTML = `

${esc(String(e && e.message ? e.message : e))}

`; } } async function openHistory(id) { try { const data = await apiFetch("/api/amp-stats/history/" + encodeURIComponent(id)); lastResult = data.item || null; setView("stats"); if (lastResult) { if (el("amp-symbol")) el("amp-symbol").value = lastResult.symbol || "eth"; if (el("amp-start-hour")) el("amp-start-hour").value = String(lastResult.start_hour ?? 16); renderSummary(lastResult.summary, lastResult); const pagePayload = { page: 1, page_size: 20, total: (lastResult.rows || []).length, total_pages: Math.max(1, Math.ceil((lastResult.rows || []).length / 20)), rows: (lastResult.rows || []).slice(0, 20), }; pageNo = 1; renderTable(pagePayload); setStatus("已载入历史 " + id); } } catch (e) { setStatus(String(e && e.message ? e.message : e)); } } function bind() { if (bound) return; bound = true; fillMetaControls(); page.querySelectorAll(".amp-view-tab").forEach((btn) => { btn.addEventListener("click", () => setView(btn.getAttribute("data-view"))); }); el("amp-period")?.addEventListener("change", syncCustomDays); el("amp-btn-compute")?.addEventListener("click", () => void compute(true)); el("amp-btn-save")?.addEventListener("click", () => void saveHistory()); el("amp-btn-download")?.addEventListener("click", downloadCurrent); syncCustomDays(); } window.hubAmpStatsPage = { init() { bind(); setView("stats"); setStatus(""); }, }; })();