diff --git a/app/static/css/app.css b/app/static/css/app.css index b740c64..10c7b1b 100644 --- a/app/static/css/app.css +++ b/app/static/css/app.css @@ -130,8 +130,26 @@ th { color: var(--muted); font-weight: 600; } padding: 10px; min-height: 240px; } -.chart-box h4 { margin: 0 0 8px; font-size: 13px; color: var(--muted); } -.chart-canvas { width: 100%; height: 240px; display: block; } +.chart-box h4 { + margin: 0 0 8px; + font-size: 13px; + color: var(--muted); + display: flex; + align-items: baseline; + gap: 8px; + flex-wrap: wrap; +} +.chart-zoom-hint { + margin-left: auto; + font-size: 11px; + font-weight: 400; +} +.chart-canvas { + width: 100%; + height: 240px; + display: block; + cursor: zoom-in; +} .empty-chart { height: 240px; display: flex; @@ -140,3 +158,52 @@ th { color: var(--muted); font-weight: 600; } color: var(--muted); font-size: 13px; } + +.chart-lightbox { + position: fixed; + inset: 0; + z-index: 1000; + background: rgba(0, 0, 0, 0.72); + display: flex; + align-items: center; + justify-content: center; + padding: 24px; +} +.chart-lightbox[hidden] { display: none !important; } +.chart-lightbox-panel { + width: min(1100px, 96vw); + background: #12121c; + border: 1px solid var(--border); + border-radius: 14px; + padding: 14px 16px 16px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.45); +} +.chart-lightbox-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 4px; +} +.chart-lightbox-title { + font-size: 15px; + font-weight: 700; +} +.chart-lightbox-close { + border: 0; + background: transparent; + color: var(--muted); + font-size: 28px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} +.chart-lightbox-close:hover { color: #fff; } +.chart-lightbox-hint { margin: 0 0 10px; font-size: 12px; } +.chart-lightbox-canvas { + width: 100%; + height: min(62vh, 520px); + display: block; + border-radius: 10px; + background: #0e0e18; +} diff --git a/app/static/js/charts.js b/app/static/js/charts.js index d92fb4d..f69b606 100644 --- a/app/static/js/charts.js +++ b/app/static/js/charts.js @@ -1,6 +1,7 @@ -/** 复盘四周期:按「包含成交时刻」的 K 线落点,避免偏到下一根 */ +/** 复盘四周期:进出场落在包含时刻的 K 线;点击可放大 */ (function () { var TF_MAX = { M1: 500, M5: 200, M15: 100, H1: 50 }; + var TF_ZOOM = { M1: 90, M5: 48, M15: 32, H1: 20 }; var TF_SEC = { M1: 60, M5: 300, M15: 900, H1: 3600 }; function num(v) { @@ -9,7 +10,7 @@ return Number.isFinite(n) ? n : null; } - /** 找到覆盖 ts 的那根 K 线:barOpen <= ts < barOpen+period(最后一根含右边界) */ + /** 找到覆盖 ts 的那根 K 线:barOpen <= ts < barOpen+period */ function barIndexAt(bars, ts, tf) { if (ts == null || !bars.length) return -1; var period = TF_SEC[tf] || 60; @@ -23,15 +24,19 @@ return i; } } - // 落在首根之前 / 末根之后:夹到边界 if (t < Number(bars[0].ts)) return 0; return bars.length - 1; } - function cropBars(bars, markers, tf) { + /** 始终以成交为中心裁剪到 maxN,避免短单淹没在全天 K 线里 */ + function cropBars(bars, markers, tf, maxN) { if (!bars.length) return bars; - var maxN = TF_MAX[tf] || 100; - if (bars.length <= maxN) return bars; + maxN = maxN || TF_MAX[tf] || 100; + markers = markers || {}; + + if (markers.openTs == null) { + return bars.length > maxN ? bars.slice(-maxN) : bars; + } var a = barIndexAt(bars, markers.openTs, tf); var b = @@ -51,7 +56,6 @@ function fmtBjt(ts) { if (ts == null) return ""; var d = new Date(Number(ts) * 1000); - // 用 UTC+8 显示,与复盘页北京时间一致 var utc = d.getTime() + d.getTimezoneOffset() * 60000; var bjt = new Date(utc + 8 * 3600000); var hh = String(bjt.getHours()).padStart(2, "0"); @@ -59,14 +63,23 @@ return hh + ":" + mm; } - function drawCandleChart(canvas, bars, markers, tf) { + function sideLabel(side) { + var s = (side || "").toLowerCase(); + if (s === "buy") return "买入(多)"; + if (s === "sell") return "卖出(空)"; + return side || ""; + } + + function drawCandleChart(canvas, bars, markers, tf, opts) { if (!canvas || !bars || !bars.length) return; - bars = cropBars(bars, markers || {}, tf); + opts = opts || {}; + var maxN = opts.maxN || TF_MAX[tf] || 100; + bars = cropBars(bars, markers || {}, tf, maxN); markers = markers || {}; var dpr = window.devicePixelRatio || 1; - var cssW = canvas.clientWidth || 400; - var cssH = canvas.clientHeight || 220; + var cssW = canvas.clientWidth || opts.width || 400; + var cssH = canvas.clientHeight || opts.height || 220; canvas.width = cssW * dpr; canvas.height = cssH * dpr; var ctx = canvas.getContext("2d"); @@ -75,7 +88,7 @@ var padL = 4; var padR = 4; var padT = 4; - var padB = 16; // 底部时间轴 + var padB = 16; var min = Infinity; var max = -Infinity; @@ -112,7 +125,7 @@ return padL + i * slot + slot * 0.5; }; - var bodyW = Math.max(1, Math.min(6, slot * 0.6)); + var bodyW = Math.max(1, Math.min(opts.zoom ? 10 : 6, slot * 0.6)); bars.forEach(function (b, i) { var x = xOf(i); var up = b.close >= b.open; @@ -130,7 +143,6 @@ ctx.fillRect(x - bodyW / 2, top, bodyW, bh); }); - // 底部左右时间(北京时间),方便对照成交时刻 ctx.fillStyle = "#888899"; ctx.font = "10px Microsoft YaHei UI, sans-serif"; ctx.textBaseline = "top"; @@ -154,7 +166,7 @@ var x = xOf(i); var y = yOf(price); ctx.beginPath(); - ctx.arc(x, y, 4.5, 0, Math.PI * 2); + ctx.arc(x, y, opts.zoom ? 6 : 4.5, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); ctx.lineWidth = 1.5; @@ -176,7 +188,7 @@ y = pe.y - 8; } ctx.beginPath(); - ctx.arc(x, y, 4.5, 0, Math.PI * 2); + ctx.arc(x, y, opts.zoom ? 6 : 4.5, 0, Math.PI * 2); ctx.fillStyle = exitColor; ctx.fill(); ctx.lineWidth = 1.5; @@ -202,15 +214,18 @@ ctx.fillText("出", px.x, px.y - 6); } - var meta = canvas.parentElement && canvas.parentElement.querySelector(".chart-meta"); + var meta = + canvas.parentElement && canvas.parentElement.querySelector(".chart-meta"); if (meta) { + var sideBit = markers.side ? " · " + sideLabel(markers.side) : ""; meta.textContent = bars.length + " 根 · " + fmtBjt(bars[0].ts) + "–" + fmtBjt(bars[n - 1].ts) + - " 北京时间"; + " 北京时间" + + sideBit; } } @@ -224,18 +239,119 @@ }; } + function paintCanvas(el, opts) { + var bars = JSON.parse(el.dataset.bars || "[]"); + var tf = el.dataset.tf || "M1"; + drawCandleChart(el, bars, readMarkers(el), tf, opts); + } + function paintAll() { document.querySelectorAll("canvas.chart-canvas").forEach(function (el) { try { - var bars = JSON.parse(el.dataset.bars || "[]"); - var tf = el.dataset.tf || "M1"; - drawCandleChart(el, bars, readMarkers(el), tf); + paintCanvas(el, {}); } catch (e) { console.warn(e); } }); } + function ensureLightbox() { + var box = document.getElementById("chart-lightbox"); + if (box) return box; + box = document.createElement("div"); + box.id = "chart-lightbox"; + box.className = "chart-lightbox"; + box.hidden = true; + box.innerHTML = + '
围绕进出场收紧显示 · Esc / 点击遮罩关闭
' + + '' + + "| 方向 | {{ side_cn }} |
|---|---|
| 账户 | {{ trade.account_name or '—' }} |
| 机器 | {{ trade.machine_name or '—' }} |
| 手数 | {{ trade.volume }} |