/** * 中控币种档案:列表筛选、交易时间线、永久 K 线(lightweight-charts)。 */ (function () { const page = document.getElementById("page-archive"); if (!page) return; const elExchange = document.getElementById("archive-exchange"); const elFilterProfit = document.getElementById("archive-filter-profit"); const elFilterLoss = document.getElementById("archive-filter-loss"); const elFilterSick = document.getElementById("archive-filter-sick"); const elFilterEmotion = document.getElementById("archive-filter-emotion"); const elBtnRefresh = document.getElementById("archive-btn-refresh"); const elBtnSync = document.getElementById("archive-btn-sync"); const elStatus = document.getElementById("archive-status"); const elList = document.getElementById("archive-list"); const elDetailPanel = document.getElementById("archive-detail-panel"); const elDetailTitle = document.getElementById("archive-detail-title"); const elDetailStats = document.getElementById("archive-detail-stats"); const elTfTabs = document.getElementById("archive-tf-tabs"); const elViewMode = document.getElementById("archive-view-mode"); const elJumpAt = document.getElementById("archive-jump-at"); const elBtnJump = document.getElementById("archive-btn-jump"); const elBtnReloadChart = document.getElementById("archive-btn-reload-chart"); const elChartHost = document.getElementById("archive-chart"); const elMarkAuto = document.getElementById("archive-mark-auto"); const elTrades = document.getElementById("archive-trades"); const ARCHIVE_MARK_AUTO_KEY = "hubArchiveMarkAuto"; const TF_MS = { "5m": 5 * 60_000, "15m": 15 * 60_000, "1h": 60 * 60_000, "4h": 4 * 60 * 60_000, }; const CHART_TZ_OFFSET_SEC = 8 * 60 * 60; let meta = null; let listRows = []; let selected = null; let trades = []; let selectedTradeId = null; let timeframe = "15m"; let chart = null; let candleSeries = null; let volumeSeries = null; let inited = false; let markAuto = true; let lastCandles = []; function loadMarkAutoPref() { try { const raw = localStorage.getItem(ARCHIVE_MARK_AUTO_KEY); if (raw === "0" || raw === "false") markAuto = false; else if (raw === "1" || raw === "true") markAuto = true; } catch (_) {} syncMarkAutoBtn(); } function syncMarkAutoBtn() { if (!elMarkAuto) return; elMarkAuto.classList.toggle("is-on", markAuto); elMarkAuto.setAttribute("aria-pressed", markAuto ? "true" : "false"); } function saveMarkAutoPref() { try { localStorage.setItem(ARCHIVE_MARK_AUTO_KEY, markAuto ? "1" : "0"); } catch (_) {} } function tradeHistoryBounds(tradeList) { let minOpen = null; let maxClose = null; (tradeList || []).forEach(function (tr) { const o = tradeOpenMs(tr); const c = tradeCloseMs(tr); if (o != null) minOpen = minOpen == null ? o : Math.min(minOpen, o); if (c != null) maxClose = maxClose == null ? c : Math.max(maxClose, c); }); return { minOpen: minOpen, maxClose: maxClose }; } function fmt(n, d) { if (n == null || n === "" || !Number.isFinite(Number(n))) return "—"; return Number(n).toFixed(d == null ? 2 : d); } function fmtPnl(v) { const n = Number(v); if (!Number.isFinite(n)) return "—"; const s = (n >= 0 ? "+" : "") + n.toFixed(2); return s; } function pad2(n) { return n < 10 ? "0" + n : String(n); } function utcSecToBjDate(utcSec) { return new Date((Number(utcSec) + CHART_TZ_OFFSET_SEC) * 1000); } function formatChartTimeBj(utcSec, withDate) { const d = utcSecToBjDate(utcSec); const h = pad2(d.getUTCHours()); const mi = pad2(d.getUTCMinutes()); if (!withDate) return h + ":" + mi; return ( d.getUTCFullYear() + "-" + pad2(d.getUTCMonth() + 1) + "-" + pad2(d.getUTCDate()) + " " + h + ":" + mi ); } function chartLocalizationBj() { return { locale: "zh-CN", dateFormat: "yyyy-MM-dd", timeFormatter: function (time) { if (typeof time === "number") return formatChartTimeBj(time, true); if (time && typeof time === "object" && time.year) { return time.year + "-" + pad2(time.month) + "-" + pad2(time.day); } return ""; }, tickMarkFormatter: function (time, tickMarkType) { if (typeof time !== "number") { if (time && typeof time === "object" && time.year) { return time.year + "-" + pad2(time.month) + "-" + pad2(time.day); } return ""; } const d = utcSecToBjDate(time); if (tickMarkType === 0) return String(d.getUTCFullYear()); if (tickMarkType === 1) return pad2(d.getUTCMonth() + 1); if (tickMarkType === 2) return pad2(d.getUTCDate()); return formatChartTimeBj(time, false); }, }; } function fmtDt(raw) { if (raw == null || raw === "") return "—"; return String(raw).replace("T", " ").slice(0, 16); } function fmtHoldMinutes(tr) { if (!tr) return "—"; const text = tr.hold_minutes_text; if (text) return text; const n = Number(tr.hold_minutes); if (!Number.isFinite(n) || n <= 0) return "0分钟"; const hours = Math.floor(n / 60); const mins = Math.floor(n % 60); if (hours) return hours + "小时" + mins + "分钟"; return mins + "分钟"; } const ENTRY_TYPE_LABELS = { trend_pullback: "趋势回调", roll: "顺势加仓", trend: "趋势回调", }; function fmtEntryType(tr) { if (!tr) return "—"; const raw = String( tr.entry_type || tr.entry_reason || tr.reviewed_entry_reason || "" ).trim(); if (raw) { return ENTRY_TYPE_LABELS[raw] || raw; } const mt = String(tr.monitor_type || "").trim(); if (mt && mt !== "下单监控") { return ENTRY_TYPE_LABELS[mt] || mt; } return "—"; } function reviewMark(tr) { return tr && tr.reviewed ? "复" : ""; } function pnlClass(v) { const n = Number(v); if (!Number.isFinite(n) || Math.abs(n) < 1e-6) return ""; return n > 0 ? "pos" : "neg"; } function setStatus(text) { if (elStatus) elStatus.textContent = text || ""; } async function apiFetch(url, opts) { const r = await fetch(url, opts); if (r.status === 401) { location.href = "/login?next=" + encodeURIComponent(location.pathname); throw new Error("未登录"); } return r; } function queryListParams() { const q = new URLSearchParams(); const ex = (elExchange && elExchange.value) || ""; if (ex) q.set("exchange_key", ex); if (elFilterProfit && elFilterProfit.checked) q.set("filter_profit", "1"); if (elFilterLoss && elFilterLoss.checked) q.set("filter_loss", "1"); if (elFilterSick && elFilterSick.checked) q.set("filter_sick", "1"); if (elFilterEmotion && elFilterEmotion.checked) q.set("filter_emotion", "1"); return q.toString(); } function renderExchangeOptions() { if (!elExchange || !meta) return; const cur = elExchange.value; elExchange.innerHTML = ''; (meta.exchanges || []).forEach(function (ex) { const opt = document.createElement("option"); opt.value = ex.key || ""; opt.textContent = (ex.name || ex.key || "") + " (" + (ex.key || "") + ")"; elExchange.appendChild(opt); }); if (cur) elExchange.value = cur; } function renderList() { if (!elList) return; if (!listRows.length) { elList.innerHTML = '

暂无档案数据。点击「同步交易与 K 线」从四所拉取。

'; return; } elList.innerHTML = listRows .map(function (row) { const active = selected && selected.exchange_key === row.exchange_key && selected.symbol === row.symbol ? " is-active" : ""; const seed = row.seed_complete ? "已建档" : "待种子"; return ( '" ); }) .join(""); elList.querySelectorAll(".archive-row").forEach(function (btn) { btn.addEventListener("click", function () { openDetail(btn.getAttribute("data-ex"), btn.getAttribute("data-sym")); }); }); } function pickAnchorTrade() { if (!trades.length) return null; if (selectedTradeId != null) { const hit = trades.find(function (t) { return String(t.trade_id || t.id) === String(selectedTradeId); }); if (hit) return hit; } return trades[0]; } function parseTimeMs(raw) { if (raw == null || raw === "") return null; if (typeof raw === "number" && Number.isFinite(raw)) { const v = Math.trunc(raw); return v > 1e12 ? v : v * 1000; } const s = String(raw).trim().replace("Z", "").replace("T", " "); if (!s) return null; const m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?: (\d{2}):(\d{2})(?::(\d{2}))?)?/); if (!m) return null; const ms = Date.UTC( Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4] || 0), Number(m[5] || 0), Number(m[6] || 0) ) - CHART_TZ_OFFSET_SEC * 1000; return Number.isFinite(ms) ? ms : null; } function tradeOpenMs(tr) { if (!tr) return null; return tr.opened_at_ms || parseTimeMs(tr.opened_at); } function tradeCloseMs(tr) { if (!tr) return null; return tr.closed_at_ms || parseTimeMs(tr.closed_at); } function anchorMsForTrade(tr) { if (!tr) return null; const mode = (elViewMode && elViewMode.value) || "hold"; if (mode === "entry") { return tradeOpenMs(tr); } return tradeCloseMs(tr) || tradeOpenMs(tr); } function msToBarTime(ms, tf) { const period = TF_MS[tf] || TF_MS["15m"]; const aligned = Math.floor(Number(ms) / period) * period; return Math.floor(aligned / 1000); } function snapToCandleTime(targetSec, candles) { if (!candles || !candles.length) return targetSec; let best = candles[0].time; let bestDiff = Math.abs(candles[0].time - targetSec); for (let i = 0; i < candles.length; i++) { const d = Math.abs(candles[i].time - targetSec); if (d < bestDiff) { bestDiff = d; best = candles[i].time; } } return best; } const OPEN_ARROW_LONG = "#22c55e"; const OPEN_ARROW_SHORT = "#ef4444"; const OPEN_ARROW_LONG_HI = "#4ade80"; const OPEN_ARROW_SHORT_HI = "#f87171"; function isLongDirection(dir) { const d = String(dir || "").trim().toLowerCase(); if (d === "short" || d === "空" || d === "sell" || d === "做空" || d === "shorts") { return false; } if (d === "long" || d === "多" || d === "buy" || d === "做多" || d === "longs") { return true; } return true; } function openArrowColor(long, highlight) { if (long) return highlight ? OPEN_ARROW_LONG_HI : OPEN_ARROW_LONG; return highlight ? OPEN_ARROW_SHORT_HI : OPEN_ARROW_SHORT; } function buildTradeMarkers(tr, candles, tf, opts) { if (!tr || !candles.length) return []; const options = opts || {}; const suffix = options.labelSuffix ? String(options.labelSuffix) : ""; const highlight = !!options.highlight; const long = isLongDirection(tr.direction); const openMs = tradeOpenMs(tr); const closeMs = tradeCloseMs(tr); const openColor = openArrowColor(long, highlight); let closeColor = highlight ? "#fbbf24" : "#f59e0b"; const pnl = Number(tr.pnl_amount); if (!highlight && Number.isFinite(pnl) && pnl < -0.0001) { closeColor = "#a855f7"; } const markers = []; if (openMs) { markers.push({ time: snapToCandleTime(msToBarTime(openMs, tf), candles), position: long ? "belowBar" : "aboveBar", color: openColor, shape: long ? "arrowUp" : "arrowDown", text: "开" + suffix, }); } if (closeMs) { markers.push({ time: snapToCandleTime(msToBarTime(closeMs, tf), candles), position: long ? "aboveBar" : "belowBar", color: closeColor, shape: long ? "arrowDown" : "arrowUp", text: "平" + suffix, }); } return markers; } function buildChartMarkers(candles, tf) { if (!candles.length) return []; const tr = pickAnchorTrade(); if (!markAuto || !trades.length) { return buildTradeMarkers(tr, candles, tf, { highlight: true }); } const sorted = trades.slice().sort(function (a, b) { return (tradeOpenMs(a) || 0) - (tradeOpenMs(b) || 0); }); const multi = sorted.length > 1; const out = []; sorted.forEach(function (row, idx) { const tid = String(row.trade_id || row.id); const parts = buildTradeMarkers(row, candles, tf, { labelSuffix: multi ? String(idx + 1) : "", highlight: tid === String(selectedTradeId), }); out.push.apply(out, parts); }); return out.sort(function (a, b) { return a.time > b.time ? 1 : a.time < b.time ? -1 : 0; }); } function applyChartMarkers() { if (!candleSeries || !candleSeries.setMarkers || !lastCandles.length) return; candleSeries.setMarkers(buildChartMarkers(lastCandles, timeframe)); } /** 初始只聚焦持仓段;完整历史已加载,可向左拖动/滚轮缩小查看建仓前全局。 */ function focusInitialTradeView(candles, tr, tf) { if (!chart || !candles.length || !tr) return; const mode = (elViewMode && elViewMode.value) || "hold"; const openSec = tradeOpenMs(tr) ? msToBarTime(tradeOpenMs(tr), tf) : null; const closeSec = tradeCloseMs(tr) ? msToBarTime(tradeCloseMs(tr), tf) : null; let openIdx = 0; let closeIdx = candles.length - 1; if (openSec != null) { for (let i = 0; i < candles.length; i++) { if (candles[i].time >= openSec) { openIdx = i; break; } } } if (closeSec != null) { for (let i = candles.length - 1; i >= 0; i--) { if (candles[i].time <= closeSec) { closeIdx = i; break; } } } const span = Math.max(24, closeIdx - openIdx + 20); let fromIdx; let toIdx; if (mode === "entry") { fromIdx = Math.max(0, openIdx - Math.floor(span * 0.35)); toIdx = Math.min(candles.length - 1, openIdx + Math.floor(span * 0.65)); } else { fromIdx = Math.max(0, openIdx - 10); toIdx = Math.min(candles.length - 1, closeIdx + 14); } if (toIdx <= fromIdx) { toIdx = Math.min(candles.length - 1, fromIdx + 80); } chart.timeScale().setVisibleLogicalRange({ from: fromIdx, to: toIdx + 4 }); } function destroyChart() { if (chart) { chart.remove(); chart = null; candleSeries = null; volumeSeries = null; } if (elChartHost) elChartHost.innerHTML = ""; } function ensureChart() { if (!elChartHost || !window.LightweightCharts) return; if (chart) return; const isDark = document.documentElement.getAttribute("data-theme") !== "light"; chart = LightweightCharts.createChart(elChartHost, { layout: { background: { color: isDark ? "#0b0e18" : "#f8f9fc" }, textColor: isDark ? "#9aa4b8" : "#4a5568", }, grid: { vertLines: { color: isDark ? "#1a2030" : "#e8ecf2" }, horzLines: { color: isDark ? "#1a2030" : "#e8ecf2" }, }, rightPriceScale: { borderColor: isDark ? "#2a3348" : "#d0d7e2", autoScale: true }, localization: chartLocalizationBj(), timeScale: { borderColor: isDark ? "#2a3348" : "#d0d7e2", timeVisible: true, secondsVisible: false, }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal }, handleScroll: { mouseWheel: true, pressedMouseMove: true, horzTouchDrag: true, vertTouchDrag: false, }, handleScale: { axisPressedMouseMove: true, mouseWheel: true, pinch: true, }, }); candleSeries = chart.addCandlestickSeries({ upColor: "#22c55e", downColor: "#ef4444", borderVisible: false, wickUpColor: "#22c55e", wickDownColor: "#ef4444", }); volumeSeries = chart.addHistogramSeries({ color: "#3b82f680", priceFormat: { type: "volume" }, priceScaleId: "", }); volumeSeries.priceScale().applyOptions({ scaleMargins: { top: 0.82, bottom: 0 }, }); new ResizeObserver(function () { if (chart && elChartHost) { chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight }); } }).observe(elChartHost); chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight }); } async function loadChart() { if (!selected) return; const tr = pickAnchorTrade(); const anchor = anchorMsForTrade(tr); const jump = (elJumpAt && elJumpAt.value || "").trim(); let openMs = null; let closeMs = null; if (markAuto && trades.length) { const bounds = tradeHistoryBounds(trades); openMs = bounds.minOpen; closeMs = bounds.maxClose; } else if (tr) { openMs = tradeOpenMs(tr); closeMs = tradeCloseMs(tr); } const params = new URLSearchParams({ exchange_key: selected.exchange_key, symbol: selected.symbol, timeframe: timeframe, mode: (elViewMode && elViewMode.value) || "hold", }); if (openMs && closeMs) { params.set("range", "history"); params.set("opened_ms", String(openMs)); params.set("closed_ms", String(closeMs)); } else { params.set("bars", "200"); if (jump) params.set("at", jump); else if (anchor) params.set("anchor_ms", String(anchor)); } setStatus("加载 K 线…"); const r = await apiFetch("/api/archive/ohlcv?" + params.toString()); const j = await r.json(); if (!r.ok) { setStatus(j.detail || "K 线加载失败"); return; } ensureChart(); const candles = j.candles || []; lastCandles = candles; candleSeries.setData( candles.map(function (c) { return { time: c.time, open: c.open, high: c.high, low: c.low, close: c.close }; }) ); volumeSeries.setData( candles.map(function (c) { return { time: c.time, value: c.volume || 0, color: c.close >= c.open ? "#22c55e55" : "#ef444455", }; }) ); applyChartMarkers(); if (tr && tradeOpenMs(tr) && tradeCloseMs(tr)) { focusInitialTradeView(candles, tr, timeframe); } else if (candles.length > 10) { chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 }); } const markHint = markAuto && trades.length > 1 ? " · 自动标注 " + trades.length + " 笔" : tr ? " · 已标注开/平" : ""; const histHint = openMs && closeMs ? " · 建档30天历史 · 可拖动/滚轮缩放查看建仓前走势" : ""; setStatus("K 线 " + candles.length + " 根 · " + timeframe + markHint + histHint); } function renderTrades() { if (!elTrades) return; if (!trades.length) { elTrades.innerHTML = '

该币种暂无已平仓记录。

'; return; } elTrades.innerHTML = '' + "" + "" + "" + trades .map(function (t) { const tid = t.trade_id || t.id; const active = String(tid) === String(selectedTradeId) ? " is-active" : ""; const tag = t.behavior_tag || ""; const rev = reviewMark(t); return ( '' + '" + (rev ? '' + rev + "" : "") + fmtEntryType(t) + "" + '" + '" + '" + "" + "" + '" + '" + '' + '' + "" ); }) .join("") + "
开仓类型开仓时间平仓时间持仓时长方向结果盈亏标签备注操作
" + (rev ? '' + rev + "" : "") + fmtDt(t.opened_at) + "" + (rev ? '' + rev + "" : "") + fmtDt(t.closed_at) + "" + (rev ? '' + rev + "" : "") + fmtHoldMinutes(t) + "" + (t.direction || "—") + "" + (t.result || "—") + "' + fmtPnl(t.pnl_amount) + "
"; elTrades.querySelectorAll(".archive-del-btn").forEach(function (btn) { btn.addEventListener("click", function (ev) { ev.stopPropagation(); void deleteTrade(btn.getAttribute("data-id")); }); }); elTrades.querySelectorAll(".archive-trade-row").forEach(function (row) { row.addEventListener("click", function (ev) { if ( ev.target.closest("select") || ev.target.closest("input") || ev.target.closest(".archive-del-btn") ) { return; } selectedTradeId = row.getAttribute("data-id"); renderTrades(); applyChartMarkers(); const trSel = pickAnchorTrade(); if (trSel && tradeOpenMs(trSel) && tradeCloseMs(trSel)) { focusInitialTradeView(lastCandles, trSel, timeframe); } }); }); elTrades.querySelectorAll(".archive-tag-select").forEach(function (sel) { sel.addEventListener("change", function () { saveOverlay(sel.getAttribute("data-id"), sel.value, null); }); }); elTrades.querySelectorAll(".archive-note-input").forEach(function (inp) { inp.addEventListener("change", function () { const row = inp.closest(".archive-trade-row"); const tagSel = row && row.querySelector(".archive-tag-select"); saveOverlay(inp.getAttribute("data-id"), tagSel ? tagSel.value : "", inp.value); }); }); } async function deleteTrade(tradeId) { if (!selected || tradeId == null) return; if (!window.confirm("从币种档案移除该笔交易?(不影响交易所实例里的复盘记录)")) return; const r = await apiFetch( "/api/archive/trade/" + selected.exchange_key + "/" + tradeId, { method: "DELETE" } ); if (!r.ok) { const j = await r.json().catch(function () { return {}; }); setStatus(j.detail || j.msg || "删除失败"); return; } if (String(selectedTradeId) === String(tradeId)) { selectedTradeId = null; } trades = trades.filter(function (t) { return String(t.trade_id || t.id) !== String(tradeId); }); renderTrades(); applyChartMarkers(); await loadList(); setStatus("已移除 1 笔档案记录"); } async function saveOverlay(tradeId, tag, note) { if (!selected) return; const body = { behavior_tag: tag || "", note: note != null ? note : undefined }; if (note == null) { const row = elTrades.querySelector('.archive-trade-row[data-id="' + tradeId + '"]'); const inp = row && row.querySelector(".archive-note-input"); body.note = inp ? inp.value : ""; } await apiFetch("/api/archive/trade/" + selected.exchange_key + "/" + tradeId, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const tr = trades.find(function (t) { return String(t.trade_id || t.id) === String(tradeId); }); if (tr) { tr.behavior_tag = body.behavior_tag; tr.note = body.note; } } async function openDetail(exchangeKey, symbol) { selected = { exchange_key: exchangeKey, symbol: symbol }; if (elDetailPanel) elDetailPanel.classList.remove("hidden"); const row = listRows.find(function (r) { return r.exchange_key === exchangeKey && r.symbol === symbol; }); if (elDetailTitle) { elDetailTitle.textContent = symbol + " · " + exchangeKey; } if (elDetailStats && row) { elDetailStats.textContent = row.trade_count + " 笔 · 胜 " + row.win_count + " / 负 " + row.loss_count + " · 合计 " + fmtPnl(row.total_pnl) + " U"; } renderList(); setStatus("加载交易明细…"); const r = await apiFetch( "/api/archive/detail?exchange_key=" + encodeURIComponent(exchangeKey) + "&symbol=" + encodeURIComponent(symbol) ); const j = await r.json(); trades = j.trades || []; selectedTradeId = trades.length ? String(trades[0].trade_id || trades[0].id) : null; renderTrades(); await loadChart(); } async function loadList() { setStatus("加载列表…"); const r = await apiFetch("/api/archive/list?" + queryListParams()); const j = await r.json(); listRows = j.rows || []; renderList(); setStatus("共 " + listRows.length + " 个币种档案 · " + new Date().toLocaleTimeString()); } async function loadMeta() { const r = await apiFetch("/api/archive/meta"); meta = await r.json(); timeframe = (meta && meta.default_timeframe) || "15m"; if (meta && meta.last_sync && elStatus && !elStatus.textContent) { setStatus(formatSyncSummary(meta.last_sync)); } renderExchangeOptions(); if (elTfTabs) { elTfTabs.querySelectorAll(".archive-tf-btn").forEach(function (btn) { btn.classList.toggle("is-active", btn.getAttribute("data-tf") === timeframe); }); } } function formatSyncSummary(j) { const results = j.results || []; const okN = results.filter(function (x) { return x.ok !== false; }).length; const parts = ["同步完成 · " + okN + "/" + (j.exchanges || 0) + " 所"]; results.forEach(function (row) { const label = row.exchange_key || row.name || "?"; if (row.ok === false) { parts.push(label + " 失败: " + (row.msg || "未知错误")); } else { let line = label + " " + (row.trade_count != null ? row.trade_count : row.trades || 0) + " 笔"; if (row.trades_removed > 0) { line += " 清" + row.trades_removed; } parts.push(line); } }); return parts.join(" · "); } async function syncAll() { setStatus("同步中(可能需数分钟)…"); elBtnSync && (elBtnSync.disabled = true); try { const r = await apiFetch("/api/archive/sync", { method: "POST" }); const j = await r.json(); setStatus(formatSyncSummary(j)); await loadList(); if (selected) await openDetail(selected.exchange_key, selected.symbol); } catch (e) { setStatus(String(e)); } finally { elBtnSync && (elBtnSync.disabled = false); } } function bindEvents() { if (elBtnRefresh) elBtnRefresh.addEventListener("click", loadList); if (elBtnSync) elBtnSync.addEventListener("click", syncAll); if (elExchange) elExchange.addEventListener("change", loadList); [elFilterProfit, elFilterLoss, elFilterSick, elFilterEmotion].forEach(function (el) { if (el) el.addEventListener("change", loadList); }); if (elTfTabs) { elTfTabs.addEventListener("click", function (ev) { const btn = ev.target.closest(".archive-tf-btn"); if (!btn) return; timeframe = btn.getAttribute("data-tf") || "15m"; elTfTabs.querySelectorAll(".archive-tf-btn").forEach(function (b) { b.classList.toggle("is-active", b === btn); }); loadChart(); }); } if (elViewMode) elViewMode.addEventListener("change", loadChart); if (elBtnReloadChart) elBtnReloadChart.addEventListener("click", loadChart); if (elMarkAuto) { elMarkAuto.addEventListener("click", function () { markAuto = !markAuto; syncMarkAutoBtn(); saveMarkAutoPref(); loadChart(); }); } if (elBtnJump) { elBtnJump.addEventListener("click", function () { loadChart(); }); } } async function init() { if (!document.getElementById("page-archive") || document.getElementById("page-archive").classList.contains("hidden")) { return; } if (!inited) { loadMarkAutoPref(); bindEvents(); inited = true; } await loadMeta(); await loadList(); } function destroy() { destroyChart(); } window.hubArchivePage = { init: init, destroy: destroy }; })();