/** 轻量蜡烛图(无外部依赖):复盘四周期 + 入场/出场标注 */ (function () { 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; } function drawCandleChart(canvas, bars, markers) { if (!canvas || !bars || !bars.length) return; var dpr = window.devicePixelRatio || 1; var cssW = canvas.clientWidth || 400; var cssH = canvas.clientHeight || 180; canvas.width = cssW * dpr; canvas.height = cssH * dpr; var ctx = canvas.getContext("2d"); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); var padL = 8; var padR = 8; var padT = 18; var padB = 8; var min = Infinity; var max = -Infinity; bars.forEach(function (b) { min = Math.min(min, b.low); max = Math.max(max, b.high); }); markers = markers || {}; 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.04 || 0.5; 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 线 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 - Math.max(1, slot * 0.3), top, Math.max(2, slot * 0.6), bh); }); function drawPriceLine(price, color, label) { if (price == null) return; var y = yOf(price); ctx.save(); ctx.strokeStyle = color; ctx.lineWidth = 1.2; ctx.setLineDash([5, 4]); ctx.beginPath(); ctx.moveTo(padL, y); ctx.lineTo(cssW - padR, y); ctx.stroke(); ctx.setLineDash([]); ctx.fillStyle = color; ctx.font = "11px Microsoft YaHei UI, sans-serif"; ctx.textAlign = "left"; ctx.textBaseline = "bottom"; ctx.fillText(label + " " + price.toFixed(3), padL + 2, y - 2); ctx.restore(); } function drawMarker(i, price, color, label) { if (i < 0 || price == null) return; var x = xOf(i); var y = yOf(price); // 竖线标时刻 ctx.save(); ctx.strokeStyle = color; ctx.globalAlpha = 0.35; ctx.lineWidth = 1; ctx.setLineDash([3, 3]); ctx.beginPath(); ctx.moveTo(x, padT); ctx.lineTo(x, cssH - padB); ctx.stroke(); ctx.setLineDash([]); ctx.globalAlpha = 1; // 圆点 ctx.beginPath(); ctx.arc(x, y, 5, 0, Math.PI * 2); ctx.fillStyle = color; ctx.fill(); ctx.lineWidth = 1.5; ctx.strokeStyle = "#ffffff"; ctx.stroke(); // 三角指向 ctx.beginPath(); ctx.moveTo(x, y - 12); ctx.lineTo(x - 5, y - 5); ctx.lineTo(x + 5, y - 5); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); ctx.font = "bold 11px Microsoft YaHei UI, sans-serif"; ctx.fillStyle = color; ctx.textAlign = "center"; ctx.textBaseline = "bottom"; ctx.fillText(label, x, y - 14); ctx.restore(); } var entryColor = "#4cc2ff"; var exitColor = "#f0b429"; var entryIdx = nearestBarIndex(bars, markers.openTs); var exitIdx = nearestBarIndex(bars, markers.closeTs); drawPriceLine(markers.openPrice, entryColor, "入场"); drawPriceLine(markers.closePrice, exitColor, "出场"); drawMarker(entryIdx, markers.openPrice, entryColor, "入场"); if (markers.closeTs != null && markers.closePrice != null) { drawMarker(exitIdx, markers.closePrice, exitColor, "出场"); } // 入场→出场连线(同图内) if ( entryIdx >= 0 && exitIdx >= 0 && markers.openPrice != null && markers.closePrice != null && markers.closeTs != null ) { ctx.save(); ctx.strokeStyle = "#c084fc"; ctx.lineWidth = 1.4; ctx.setLineDash([4, 3]); ctx.beginPath(); ctx.moveTo(xOf(entryIdx), yOf(markers.openPrice)); ctx.lineTo(xOf(exitIdx), yOf(markers.closePrice)); ctx.stroke(); ctx.restore(); } } 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 || "[]"); drawCandleChart(el, bars, readMarkers(el)); } catch (e) { console.warn(e); } }); } paintAll(); window.addEventListener("resize", paintAll); })();