Fix review chart markers to use containing bar time, not nearest open.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-10-21 20:24:28 +08:00
parent 94d7557b1c
commit 429010da6b
+49 -20
View File
@@ -1,6 +1,7 @@
/** 复盘四周期:固定根数裁剪;进出场只用圆点,价签放在图外图例 */
/** 复盘四周期:按「包含成交时刻」的 K 线落点,避免偏到下一根 */
(function () {
var TF_MAX = { M1: 500, M5: 200, M15: 100, H1: 50 };
var TF_SEC = { M1: 60, M5: 300, M15: 900, H1: 3600 };
function num(v) {
if (v == null || v === "") return null;
@@ -8,18 +9,23 @@
return Number.isFinite(n) ? n : null;
}
function nearestBarIndex(bars, ts) {
/** 找到覆盖 ts 的那根 K 线:barOpen <= ts < barOpen+period(最后一根含右边界) */
function barIndexAt(bars, ts, tf) {
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;
var period = TF_SEC[tf] || 60;
var t = Number(ts);
var i;
for (i = 0; i < bars.length; i++) {
var open = Number(bars[i].ts);
var end = open + period;
var last = i === bars.length - 1;
if (t >= open && (t < end || (last && t <= end))) {
return i;
}
}
return best;
// 落在首根之前 / 末根之后:夹到边界
if (t < Number(bars[0].ts)) return 0;
return bars.length - 1;
}
function cropBars(bars, markers, tf) {
@@ -27,8 +33,9 @@
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;
var a = barIndexAt(bars, markers.openTs, tf);
var b =
markers.closeTs != null ? barIndexAt(bars, markers.closeTs, tf) : a;
if (a < 0) a = Math.floor(bars.length / 2);
if (b < 0) b = a;
var lo = Math.min(a, b);
@@ -41,6 +48,17 @@
return bars.slice(start, end);
}
function fmtBjt(ts) {
if (ts == null) return "";
var d = new Date(Number(ts) * 1000);
// 用 UTC+8 显示,与复盘页北京时间一致
var utc = d.getTime() + d.getTimezoneOffset() * 60000;
var bjt = new Date(utc + 8 * 3600000);
var hh = String(bjt.getHours()).padStart(2, "0");
var mm = String(bjt.getMinutes()).padStart(2, "0");
return hh + ":" + mm;
}
function drawCandleChart(canvas, bars, markers, tf) {
if (!canvas || !bars || !bars.length) return;
bars = cropBars(bars, markers || {}, tf);
@@ -57,7 +75,7 @@
var padL = 4;
var padR = 4;
var padT = 4;
var padB = 4;
var padB = 16; // 底部时间轴
var min = Infinity;
var max = -Infinity;
@@ -112,12 +130,20 @@
ctx.fillRect(x - bodyW / 2, top, bodyW, bh);
});
// 仅圆点,不画横线/竖线/价签(价签在图例外)
// 底部左右时间(北京时间),方便对照成交时刻
ctx.fillStyle = "#888899";
ctx.font = "10px Microsoft YaHei UI, sans-serif";
ctx.textBaseline = "top";
ctx.textAlign = "left";
ctx.fillText(fmtBjt(bars[0].ts), padL, cssH - padB + 2);
ctx.textAlign = "right";
ctx.fillText(fmtBjt(bars[n - 1].ts), cssW - padR, cssH - padB + 2);
var entryColor = "#4cc2ff";
var exitColor = "#f0b429";
var entryIdx = nearestBarIndex(bars, markers.openTs);
var entryIdx = barIndexAt(bars, markers.openTs, tf);
var exitIdx =
markers.closeTs != null ? nearestBarIndex(bars, markers.closeTs) : -1;
markers.closeTs != null ? barIndexAt(bars, markers.closeTs, tf) : -1;
var hasExit =
markers.closeTs != null &&
markers.closePrice != null &&
@@ -143,7 +169,6 @@
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);
@@ -163,7 +188,6 @@
}
}
// 极短引线:点旁只写「入」「出」一字,不写价格
ctx.font = "bold 10px Microsoft YaHei UI, sans-serif";
if (pe) {
ctx.fillStyle = entryColor;
@@ -178,10 +202,15 @@
ctx.fillText("出", px.x, px.y - 6);
}
// 更新图外图例里的实际显示根数
var meta = canvas.parentElement && canvas.parentElement.querySelector(".chart-meta");
if (meta) {
meta.textContent = bars.length + " 根";
meta.textContent =
bars.length +
" 根 · " +
fmtBjt(bars[0].ts) +
"–" +
fmtBjt(bars[n - 1].ts) +
" 北京时间";
}
}