Clean up review charts: crop windows, simplify entry/exit markers.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -128,12 +128,12 @@ th { color: var(--muted); font-weight: 600; }
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 10px;
|
||||
min-height: 220px;
|
||||
min-height: 240px;
|
||||
}
|
||||
.chart-box h4 { margin: 0 0 8px; font-size: 13px; color: var(--muted); }
|
||||
.chart-canvas { width: 100%; height: 180px; display: block; }
|
||||
.chart-canvas { width: 100%; height: 200px; display: block; }
|
||||
.empty-chart {
|
||||
height: 180px;
|
||||
height: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
+143
-98
@@ -1,5 +1,7 @@
|
||||
/** 轻量蜡烛图(无外部依赖):复盘四周期 + 入场/出场标注 */
|
||||
/** 复盘四周期蜡烛图:围绕成交裁剪窗口,入场/出场轻量标注 */
|
||||
(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);
|
||||
@@ -20,27 +22,50 @@
|
||||
return best;
|
||||
}
|
||||
|
||||
function drawCandleChart(canvas, bars, markers) {
|
||||
/** 只保留成交前后一段,避免 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 || 180;
|
||||
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 = 8;
|
||||
var padR = 8;
|
||||
var padT = 18;
|
||||
var padB = 8;
|
||||
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);
|
||||
});
|
||||
markers = markers || {};
|
||||
if (markers.openPrice != null) {
|
||||
min = Math.min(min, markers.openPrice);
|
||||
max = Math.max(max, markers.openPrice);
|
||||
@@ -49,11 +74,8 @@
|
||||
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;
|
||||
if (!(max > min)) max = min + 1;
|
||||
var padPrice = (max - min) * 0.08 || 0.8;
|
||||
min -= padPrice;
|
||||
max += padPrice;
|
||||
|
||||
@@ -74,6 +96,7 @@
|
||||
};
|
||||
|
||||
// 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;
|
||||
@@ -88,101 +111,122 @@
|
||||
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);
|
||||
ctx.fillRect(x - bodyW / 2, top, bodyW, 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 &&
|
||||
var exitIdx =
|
||||
markers.closeTs != null ? nearestBarIndex(bars, markers.closeTs) : -1;
|
||||
var hasExit =
|
||||
markers.closeTs != null &&
|
||||
markers.closePrice != null &&
|
||||
markers.closeTs != 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 = "#c084fc";
|
||||
ctx.lineWidth = 1.4;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.globalAlpha = 0.7;
|
||||
ctx.lineWidth = 1;
|
||||
ctx.setLineDash([4, 3]);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(xOf(entryIdx), yOf(markers.openPrice));
|
||||
ctx.lineTo(xOf(exitIdx), yOf(markers.closePrice));
|
||||
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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,7 +244,8 @@
|
||||
document.querySelectorAll("canvas.chart-canvas").forEach(function (el) {
|
||||
try {
|
||||
var bars = JSON.parse(el.dataset.bars || "[]");
|
||||
drawCandleChart(el, bars, readMarkers(el));
|
||||
var tf = (el.dataset.tf || el.id || "").replace(/^chart-/, "") || "M1";
|
||||
drawCandleChart(el, bars, readMarkers(el), tf);
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user