Files
gold_monitor/app/static/js/charts.js
T

242 lines
7.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/** 复盘四周期:按「包含成交时刻」的 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;
var n = Number(v);
return Number.isFinite(n) ? n : null;
}
/** 找到覆盖 ts 的那根 K 线:barOpen <= ts < barOpen+period(最后一根含右边界) */
function barIndexAt(bars, ts, tf) {
if (ts == null || !bars.length) return -1;
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;
}
}
// 落在首根之前 / 末根之后:夹到边界
if (t < Number(bars[0].ts)) return 0;
return bars.length - 1;
}
function cropBars(bars, markers, tf) {
if (!bars.length) return bars;
var maxN = TF_MAX[tf] || 100;
if (bars.length <= maxN) return bars;
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);
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 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);
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 = 16; // 底部时间轴
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);
});
// 底部左右时间(北京时间),方便对照成交时刻
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 = barIndexAt(bars, markers.openTs, tf);
var exitIdx =
markers.closeTs != null ? barIndexAt(bars, markers.closeTs, tf) : -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 +
" 根 · " +
fmtBjt(bars[0].ts) +
"–" +
fmtBjt(bars[n - 1].ts) +
" 北京时间";
}
}
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);
})();