e2847c90df
Co-authored-by: Cursor <cursoragent@cursor.com>
213 lines
6.1 KiB
JavaScript
213 lines
6.1 KiB
JavaScript
/** 复盘四周期:固定根数裁剪;进出场只用圆点,价签放在图外图例 */
|
|
(function () {
|
|
var TF_MAX = { M1: 500, M5: 200, M15: 100, H1: 50 };
|
|
|
|
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 cropBars(bars, markers, tf) {
|
|
if (!bars.length) return bars;
|
|
var maxN = TF_MAX[tf] || 100;
|
|
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(4, 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 || 220;
|
|
canvas.width = cssW * dpr;
|
|
canvas.height = cssH * dpr;
|
|
var ctx = canvas.getContext("2d");
|
|
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
|
|
var padL = 4;
|
|
var padR = 4;
|
|
var padT = 4;
|
|
var padB = 4;
|
|
|
|
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.06 || 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;
|
|
};
|
|
|
|
var bodyW = Math.max(1, Math.min(6, slot * 0.6));
|
|
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;
|
|
|
|
function drawDot(i, price, color) {
|
|
if (i < 0 || price == null) return { x: 0, y: 0 };
|
|
var x = xOf(i);
|
|
var y = yOf(price);
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 4.5, 0, Math.PI * 2);
|
|
ctx.fillStyle = color;
|
|
ctx.fill();
|
|
ctx.lineWidth = 1.5;
|
|
ctx.strokeStyle = "#ffffff";
|
|
ctx.stroke();
|
|
return { x: x, y: y };
|
|
}
|
|
|
|
var pe = null;
|
|
var px = null;
|
|
if (entryIdx >= 0 && markers.openPrice != null) {
|
|
pe = drawDot(entryIdx, markers.openPrice, entryColor);
|
|
}
|
|
if (hasExit) {
|
|
// 同一根或相邻:出场点略向右错开,避免叠成一团
|
|
if (Math.abs(exitIdx - entryIdx) <= 1) {
|
|
var x = xOf(exitIdx) + Math.max(6, slot * 0.8);
|
|
var y = yOf(markers.closePrice);
|
|
if (pe && Math.abs(y - pe.y) < 6) {
|
|
y = pe.y - 8;
|
|
}
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, 4.5, 0, Math.PI * 2);
|
|
ctx.fillStyle = exitColor;
|
|
ctx.fill();
|
|
ctx.lineWidth = 1.5;
|
|
ctx.strokeStyle = "#ffffff";
|
|
ctx.stroke();
|
|
px = { x: x, y: y };
|
|
} else {
|
|
px = drawDot(exitIdx, markers.closePrice, exitColor);
|
|
}
|
|
}
|
|
|
|
// 极短引线:点旁只写「入」「出」一字,不写价格
|
|
ctx.font = "bold 10px Microsoft YaHei UI, sans-serif";
|
|
if (pe) {
|
|
ctx.fillStyle = entryColor;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "bottom";
|
|
ctx.fillText("入", pe.x, pe.y - 6);
|
|
}
|
|
if (px) {
|
|
ctx.fillStyle = exitColor;
|
|
ctx.textAlign = "center";
|
|
ctx.textBaseline = "bottom";
|
|
ctx.fillText("出", px.x, px.y - 6);
|
|
}
|
|
|
|
// 更新图外图例里的实际显示根数
|
|
var meta = canvas.parentElement && canvas.parentElement.querySelector(".chart-meta");
|
|
if (meta) {
|
|
meta.textContent = bars.length + " 根";
|
|
}
|
|
}
|
|
|
|
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 || "M1";
|
|
drawCandleChart(el, bars, readMarkers(el), tf);
|
|
} catch (e) {
|
|
console.warn(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
paintAll();
|
|
window.addEventListener("resize", paintAll);
|
|
})();
|