/** * 中控币种档案:列表筛选、交易时间线、永久 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 elTrades = document.getElementById("archive-trades"); const TF_MS = { "5m": 5 * 60_000, "15m": 15 * 60_000, "1h": 60 * 60_000, "4h": 4 * 60 * 60_000, }; 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; 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 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 anchorMsForTrade(tr) { if (!tr) return null; const mode = (elViewMode && elViewMode.value) || "hold"; if (mode === "entry") { return tr.opened_at_ms || null; } return tr.closed_at_ms || tr.opened_at_ms || null; } 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" }, timeScale: { borderColor: isDark ? "#2a3348" : "#d0d7e2", timeVisible: true }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal }, }); 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(); const params = new URLSearchParams({ exchange_key: selected.exchange_key, symbol: selected.symbol, timeframe: timeframe, mode: (elViewMode && elViewMode.value) || "hold", 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 || []; 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", }; }) ); if (candles.length > 10) { chart.timeScale().setVisibleLogicalRange({ from: candles.length - 120, to: candles.length + 5 }); } setStatus("K 线 " + candles.length + " 根 · " + timeframe); } function renderTrades() { if (!elTrades) return; if (!trades.length) { elTrades.innerHTML = '该币种暂无已平仓记录。
'; return; } elTrades.innerHTML = '| 平仓 | 方向 | 结果 | 盈亏 | 标签 | 备注 | " + "
|---|---|---|---|---|---|
| " + (t.closed_at || "—") + " | " + "" + (t.direction || "—") + " | " + "" + (t.result || "—") + " | " + '' + fmtPnl(t.pnl_amount) + " | " + '" + ' | ' + " |