Mark entry/exit on review charts; clarify bars are saved then read.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-21 17:24:24 +08:00
parent 229f0351e0
commit 39bff20d60
3 changed files with 205 additions and 36 deletions
+184 -30
View File
@@ -1,58 +1,212 @@
/** 轻量蜡烛图(无外部依赖),用于复盘四周期预览 */
/** 轻量蜡烛图(无外部依赖):复盘四周期 + 入场/出场标注 */
(function () {
function drawCandleChart(canvas, bars) {
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;
const dpr = window.devicePixelRatio || 1;
const cssW = canvas.clientWidth || 400;
const cssH = canvas.clientHeight || 180;
var dpr = window.devicePixelRatio || 1;
var cssW = canvas.clientWidth || 400;
var cssH = canvas.clientHeight || 180;
canvas.width = cssW * dpr;
canvas.height = cssH * dpr;
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
var ctx = canvas.getContext("2d");
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const pad = 8;
let min = Infinity, max = -Infinity;
bars.forEach((b) => {
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;
}
const w = cssW - pad * 2;
const h = cssH - pad * 2;
const n = bars.length;
const slot = w / n;
// 上下留一点边,避免标注贴边
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);
const yOf = (p) => pad + ((max - p) / (max - min)) * h;
var yOf = function (p) {
return padT + ((max - p) / (max - min)) * h;
};
var xOf = function (i) {
return padL + i * slot + slot * 0.5;
};
bars.forEach((b, i) => {
const x = pad + i * slot + slot * 0.5;
const up = b.close >= b.open;
// 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();
const y1 = yOf(b.open);
const y2 = yOf(b.close);
const top = Math.min(y1, y2);
const bh = Math.max(1, Math.abs(y2 - y1));
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);
}
});
}
document.querySelectorAll("canvas.chart-canvas").forEach((el) => {
try {
const bars = JSON.parse(el.dataset.bars || "[]");
drawCandleChart(el, bars);
} catch (e) {
console.warn(e);
}
});
paintAll();
window.addEventListener("resize", paintAll);
})();