/** 复盘四周期蜡烛图:围绕成交裁剪窗口,入场/出场轻量标注 */ (function () { var TF_MAX = { M1: 90, M5: 48, M15: 36, H1: 24 }; function num(v) { if (v == null || v === "") return null; var n = Number(v); return Number.isFinite(n) ? n : null; } function nearestBarIndex(bars, ts) { if (ts == null || !bars.length) return -1; var best = 0; var bestDiff = Math.abs(Number(bars[0].ts) - ts); for (var i = 1; i < bars.length; i++) { var d = Math.abs(Number(bars[i].ts) - ts); if (d < bestDiff) { bestDiff = d; best = i; } } return best; } /** 只保留成交前后一段,避免 M1 挤满、H1 太空 */ function cropBars(bars, markers, tf) { if (!bars.length) return bars; var maxN = TF_MAX[tf] || 60; if (bars.length <= maxN) return bars; var a = nearestBarIndex(bars, markers.openTs); var b = markers.closeTs != null ? nearestBarIndex(bars, markers.closeTs) : a; if (a < 0) a = Math.floor(bars.length / 2); if (b < 0) b = a; var lo = Math.min(a, b); var hi = Math.max(a, b); var span = hi - lo + 1; var pad = Math.max(8, Math.floor((maxN - span) / 2)); var start = Math.max(0, lo - pad); var end = Math.min(bars.length, start + maxN); start = Math.max(0, end - maxN); return bars.slice(start, end); } function drawCandleChart(canvas, bars, markers, tf) { if (!canvas || !bars || !bars.length) return; bars = cropBars(bars, markers || {}, tf); markers = markers || {}; var dpr = window.devicePixelRatio || 1; var cssW = canvas.clientWidth || 400; var cssH = canvas.clientHeight || 200; canvas.width = cssW * dpr; canvas.height = cssH * dpr; var ctx = canvas.getContext("2d"); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); var padL = 6; var padR = 56; // 右侧留给价签 var padT = 10; var padB = 6; var min = Infinity; var max = -Infinity; bars.forEach(function (b) { min = Math.min(min, b.low); max = Math.max(max, b.high); }); if (markers.openPrice != null) { min = Math.min(min, markers.openPrice); max = Math.max(max, markers.openPrice); } if (markers.closePrice != null) { min = Math.min(min, markers.closePrice); max = Math.max(max, markers.closePrice); } if (!(max > min)) max = min + 1; var padPrice = (max - min) * 0.08 || 0.8; min -= padPrice; max += padPrice; var w = cssW - padL - padR; var h = cssH - padT - padB; var n = bars.length; var slot = w / n; ctx.clearRect(0, 0, cssW, cssH); ctx.fillStyle = "#0e0e18"; ctx.fillRect(0, 0, cssW, cssH); var yOf = function (p) { return padT + ((max - p) / (max - min)) * h; }; var xOf = function (i) { return padL + i * slot + slot * 0.5; }; // K 线 var bodyW = Math.max(1.5, Math.min(8, slot * 0.55)); bars.forEach(function (b, i) { var x = xOf(i); var up = b.close >= b.open; ctx.strokeStyle = up ? "#0ecb81" : "#f6465d"; ctx.fillStyle = ctx.strokeStyle; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, yOf(b.high)); ctx.lineTo(x, yOf(b.low)); ctx.stroke(); var y1 = yOf(b.open); var y2 = yOf(b.close); var top = Math.min(y1, y2); var bh = Math.max(1, Math.abs(y2 - y1)); ctx.fillRect(x - bodyW / 2, top, bodyW, bh); }); var entryColor = "#4cc2ff"; var exitColor = "#f0b429"; var entryIdx = nearestBarIndex(bars, markers.openTs); var exitIdx = markers.closeTs != null ? nearestBarIndex(bars, markers.closeTs) : -1; var hasExit = markers.closeTs != null && markers.closePrice != null && exitIdx >= 0; var openP = markers.openPrice; var closeP = markers.closePrice; var samePrice = openP != null && closeP != null && Math.abs(openP - closeP) < 0.08; // 价格横线:价差很小时只画一条,避免两行字叠在一起 function drawHLine(price, color) { var y = yOf(price); ctx.save(); ctx.strokeStyle = color; ctx.globalAlpha = 0.7; ctx.lineWidth = 1; ctx.setLineDash([4, 3]); ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(padL + w, y); ctx.stroke(); ctx.restore(); return y; } function drawRightTag(y, text, color, dy) { dy = dy || 0; var ty = Math.max(padT + 10, Math.min(cssH - padB - 4, y + dy)); ctx.save(); ctx.font = "11px Microsoft YaHei UI, sans-serif"; ctx.fillStyle = color; ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillText(text, padL + w + 4, ty); ctx.restore(); } if (samePrice && openP != null) { var ySame = drawHLine(openP, entryColor); drawRightTag(ySame, "入/出 " + openP.toFixed(2), entryColor, 0); } else { if (openP != null) { var yIn = drawHLine(openP, entryColor); drawRightTag(yIn, "入 " + openP.toFixed(2), entryColor, 0); } if (hasExit && closeP != null) { var yOut = drawHLine(closeP, exitColor); var dy = 0; if (openP != null && Math.abs(yOf(openP) - yOut) < 14) { dy = yOut < yOf(openP) ? -12 : 12; } drawRightTag(yOut, "出 " + closeP.toFixed(2), exitColor, dy); } } // 点:无竖线、无三角、无紫连线 —— 只留圆点 + 短字 function drawDot(i, price, color, label, labelSide) { if (i < 0 || price == null) return; var x = xOf(i); var y = yOf(price); ctx.save(); ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); ctx.lineWidth = 1.5; ctx.strokeStyle = "#0e0e18"; ctx.stroke(); ctx.font = "bold 10px Microsoft YaHei UI, sans-serif"; ctx.fillStyle = color; ctx.textBaseline = "bottom"; if (labelSide === "left") { ctx.textAlign = "right"; ctx.fillText(label, x - 7, y - 4); } else if (labelSide === "right") { ctx.textAlign = "left"; ctx.fillText(label, x + 7, y - 4); } else { ctx.textAlign = "center"; ctx.fillText(label, x, y - 7); } ctx.restore(); } var closeTogether = hasExit && entryIdx >= 0 && Math.abs(exitIdx - entryIdx) <= 2; if (entryIdx >= 0 && openP != null) { drawDot( entryIdx, openP, entryColor, "入", closeTogether ? "left" : "center" ); } if (hasExit) { drawDot( exitIdx, closeP, exitColor, "出", closeTogether ? "right" : "center" ); } } function readMarkers(el) { return { openTs: num(el.dataset.openTs), closeTs: num(el.dataset.closeTs), openPrice: num(el.dataset.openPrice), closePrice: num(el.dataset.closePrice), side: el.dataset.side || "", }; } function paintAll() { document.querySelectorAll("canvas.chart-canvas").forEach(function (el) { try { var bars = JSON.parse(el.dataset.bars || "[]"); var tf = (el.dataset.tf || el.id || "").replace(/^chart-/, "") || "M1"; drawCandleChart(el, bars, readMarkers(el), tf); } catch (e) { console.warn(e); } }); } paintAll(); window.addEventListener("resize", paintAll); })();