window.AlphaXCharts = (function () {
var cssInjected = false;
function injectCss() {
if (cssInjected) return;
cssInjected = true;
var style = document.createElement("style");
style.textContent = [
".ax-chart{position:relative;width:100%;height:100%;min-height:220px;outline:none;touch-action:pan-y}",
".ax-chart canvas{display:block;width:100%;height:100%}",
".ax-tooltip{position:absolute;z-index:4;min-width:172px;max-width:240px;padding:10px 11px;border:1px solid rgba(148,163,184,.32);border-radius:14px;background:rgba(255,255,255,.94);box-shadow:0 18px 42px rgba(15,23,42,.16);backdrop-filter:blur(10px);font-size:12px;color:var(--slate,#4b5563);pointer-events:none;transform:translate(-50%,-112%);opacity:0;transition:opacity .12s ease}",
".ax-tooltip.show{opacity:1}",
".ax-tip-date{font-weight:950;color:var(--ink,#111827);margin-bottom:6px}",
".ax-tip-row{display:flex;justify-content:space-between;gap:14px;line-height:1.55}",
".ax-tip-row b{color:var(--ink,#111827)}",
".ax-tip-row .pos{color:var(--green,#00b473)}",
".ax-tip-row .neg{color:var(--red,#e53e3e)}",
".ax-hint{position:absolute;right:10px;bottom:8px;border:1px solid rgba(148,163,184,.24);background:rgba(255,255,255,.72);border-radius:999px;padding:4px 8px;color:var(--stone,#8e91a0);font-size:10px;font-weight:850;pointer-events:none}"
].join("");
document.head.appendChild(style);
}
function esc(v) {
return String(v == null ? "" : v).replace(/[&<>"']/g, function (c) {
return {"&": "&", "<": "<", ">": ">", '"': """, "'": "'"}[c];
});
}
function fmt(v, d) {
v = Number(v || 0);
return v.toLocaleString(undefined, {maximumFractionDigits: d == null ? 2 : d, minimumFractionDigits: 0});
}
function color(name, fallback) {
var value = getComputedStyle(document.documentElement).getPropertyValue(name).trim();
return value || fallback;
}
function cls(v) {
v = Number(v || 0);
return v > 0 ? "pos" : v < 0 ? "neg" : "";
}
function renderEquity(container, payload) {
injectCss();
var points = (payload && payload.points) || [];
if (!container) return;
if (container._axDestroy) {
container._axDestroy();
container._axDestroy = null;
}
if (!points.length) {
container.innerHTML = '
暂无收益曲线数据
';
return;
}
container.innerHTML = '';
var root = container.querySelector(".ax-chart");
var canvas = root.querySelector("canvas");
var ctx = canvas.getContext("2d");
var tip = root.querySelector(".ax-tooltip");
var locked = false;
var activeIdx = points.length - 1;
var ro = null;
var equities = points.map(function (p) { return Number(p.equity_usdt || 0); });
var pnls = points.map(function (p) { return Number(p.daily_pnl_usdt || 0); });
var minEq = Math.min.apply(null, equities);
var maxEq = Math.max.apply(null, equities);
if (maxEq === minEq) { maxEq += 1; minEq -= 1; }
var maxAbs = Math.max.apply(null, [1].concat(pnls.map(function (v) { return Math.abs(v); })));
function metrics() {
var rect = root.getBoundingClientRect();
var w = Math.max(320, rect.width || container.clientWidth || 960);
var h = Math.max(220, rect.height || container.clientHeight || 260);
var padL = 4, padR = 4, top = 22, barBase = h - 36, lineH = Math.max(96, barBase - top - 52);
var chartW = w - padL - padR;
return {w: w, h: h, padL: padL, padR: padR, top: top, barBase: barBase, lineH: lineH, chartW: chartW};
}
function x(i, m) {
return m.padL + (points.length === 1 ? m.chartW / 2 : i * m.chartW / (points.length - 1));
}
function y(v, m) {
return m.top + (maxEq - v) * m.lineH / (maxEq - minEq);
}
function resizeCanvas(m) {
var dpr = Math.max(1, window.devicePixelRatio || 1);
var bw = Math.round(m.w * dpr);
var bh = Math.round(m.h * dpr);
if (canvas.width !== bw || canvas.height !== bh) {
canvas.width = bw;
canvas.height = bh;
}
canvas.style.width = m.w + "px";
canvas.style.height = m.h + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function drawGrid(m) {
ctx.strokeStyle = "rgba(148,163,184,.25)";
ctx.lineWidth = 1;
[m.top, m.top + m.lineH / 2, m.top + m.lineH].forEach(function (gy) {
ctx.beginPath();
ctx.moveTo(m.padL, gy);
ctx.lineTo(m.w - m.padR, gy);
ctx.stroke();
});
}
function drawBars(m) {
var barW = Math.max(3, m.chartW / points.length * .55);
points.forEach(function (p, i) {
var v = Number(p.daily_pnl_usdt || 0);
var bh = Math.abs(v) / maxAbs * 44;
var bx = x(i, m) - barW / 2;
var by = v >= 0 ? m.barBase - bh : m.barBase;
ctx.fillStyle = v >= 0 ? "rgba(0,180,115,.45)" : "rgba(229,62,62,.42)";
ctx.fillRect(bx, by, barW, Math.max(1, bh));
});
}
function drawEquity(m) {
var blue = color("--blue", "#4262ff");
var area = ctx.createLinearGradient(0, m.top, 0, m.barBase);
area.addColorStop(0, "rgba(66,98,255,.16)");
area.addColorStop(1, "rgba(66,98,255,.015)");
ctx.beginPath();
ctx.moveTo(x(0, m), m.barBase);
points.forEach(function (p, i) { ctx.lineTo(x(i, m), y(Number(p.equity_usdt || 0), m)); });
ctx.lineTo(x(points.length - 1, m), m.barBase);
ctx.closePath();
ctx.fillStyle = area;
ctx.fill();
ctx.beginPath();
points.forEach(function (p, i) {
var px = x(i, m), py = y(Number(p.equity_usdt || 0), m);
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
});
ctx.strokeStyle = blue;
ctx.lineWidth = 3;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.stroke();
}
function drawLabels(m) {
ctx.fillStyle = color("--stone", "#8e91a0");
ctx.font = "800 11px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.fillText(String(points[0].date || ""), m.padL + 6, m.h - 14);
ctx.fillText("权益 " + fmt(minEq, 0) + "U - " + fmt(maxEq, 0) + "U", m.padL + 6, 14);
ctx.textAlign = "right";
ctx.fillText(String(points[points.length - 1].date || ""), m.w - m.padR - 6, m.h - 14);
ctx.fillText("点击锁定 · Esc 取消", m.w - m.padR - 6, 14);
}
function drawFocus(m) {
if (activeIdx == null) return;
var p = points[activeIdx];
var px = x(activeIdx, m);
var py = y(Number(p.equity_usdt || 0), m);
ctx.save();
ctx.strokeStyle = "rgba(15,23,42,.38)";
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(px, m.top);
ctx.lineTo(px, m.barBase);
ctx.moveTo(m.padL, py);
ctx.lineTo(m.w - m.padR, py);
ctx.stroke();
ctx.restore();
ctx.beginPath();
ctx.arc(px, py, 5, 0, Math.PI * 2);
ctx.fillStyle = color("--canvas", "#fff");
ctx.fill();
ctx.strokeStyle = color("--blue", "#4262ff");
ctx.lineWidth = 3;
ctx.stroke();
placeTip(p, px, py, m);
}
function render() {
var m = metrics();
resizeCanvas(m);
ctx.clearRect(0, 0, m.w, m.h);
drawGrid(m);
drawBars(m);
drawEquity(m);
drawFocus(m);
drawLabels(m);
}
function nearestFromEvent(ev) {
var rect = canvas.getBoundingClientRect();
var sx = ev.clientX - rect.left;
var m = metrics();
var idx = Math.round((sx - m.padL) / Math.max(1, m.chartW) * (points.length - 1));
return Math.max(0, Math.min(points.length - 1, idx));
}
function placeTip(p, px, py, m) {
tip.innerHTML =
'' + esc(p.date || "--") + '
' +
'账户权益' + fmt(p.equity_usdt, 2) + 'U
' +
'每日收益' + (Number(p.daily_pnl_usdt || 0) > 0 ? "+" : "") + fmt(p.daily_pnl_usdt, 2) + 'U
' +
'累计收益' + (Number(p.total_pnl_usdt || 0) > 0 ? "+" : "") + fmt(p.total_pnl_usdt, 2) + 'U
';
tip.style.left = Math.max(96, Math.min(m.w - 96, px)) + "px";
tip.style.top = Math.max(42, py) + "px";
tip.classList.add("show");
}
function hide() {
if (locked) return;
activeIdx = null;
tip.classList.remove("show");
render();
}
root.addEventListener("mousemove", function (ev) {
if (locked) return;
activeIdx = nearestFromEvent(ev);
render();
});
root.addEventListener("mouseleave", hide);
root.addEventListener("click", function (ev) {
activeIdx = nearestFromEvent(ev);
locked = !locked;
render();
});
root.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") {
locked = false;
hide();
}
});
if (window.ResizeObserver) {
ro = new ResizeObserver(render);
ro.observe(root);
} else {
window.addEventListener("resize", render);
}
render();
root._axDestroy = function () {
if (ro) ro.disconnect();
window.removeEventListener("resize", render);
};
container._axDestroy = root._axDestroy;
}
function priceDecimals(ref) {
ref = Math.abs(Number(ref || 0));
if (ref >= 1000) return 1;
if (ref >= 100) return 2;
if (ref >= 1) return 4;
if (ref >= 0.01) return 6;
return 8;
}
function fmtPrice(v, decimals) {
v = Number(v || 0);
if (!Number.isFinite(v)) return "--";
return v.toLocaleString(undefined, {
minimumFractionDigits: 0,
maximumFractionDigits: decimals == null ? priceDecimals(v) : decimals
});
}
function candleTime(c) {
var t = c && (c.time || c.timestamp || c.open_time || c.openTime || c.t);
if (typeof t === "string") {
var parsed = Date.parse(t);
return Number.isFinite(parsed) ? parsed : 0;
}
t = Number(t || 0);
return t > 0 && t < 100000000000 ? t * 1000 : t;
}
function fmtCandleDate(t) {
if (!t) return "--";
var d = new Date(t);
if (Number.isNaN(d.getTime())) return "--";
var mm = String(d.getMonth() + 1).padStart(2, "0");
var dd = String(d.getDate()).padStart(2, "0");
var hh = String(d.getHours()).padStart(2, "0");
var mi = String(d.getMinutes()).padStart(2, "0");
return mm + "-" + dd + " " + hh + ":" + mi;
}
function roundedRect(ctx, x, y, w, h, r) {
r = Math.max(0, Math.min(r || 0, w / 2, h / 2));
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
}
function renderKline(container, payload) {
injectCss();
if (!container) return;
if (container._axDestroy) {
container._axDestroy();
container._axDestroy = null;
}
var candles = ((payload && payload.candles) || []).slice(-60).map(function (c) {
return {
time: candleTime(c),
open: Number(c.open || 0),
high: Number(c.high || 0),
low: Number(c.low || 0),
close: Number(c.close || 0),
volume: Number(c.volume || 0)
};
}).filter(function (c) {
return c.high > 0 && c.low > 0 && c.close > 0;
});
if (candles.length < 5) {
container.innerHTML = 'K线数据不足
';
return;
}
var symbol = (payload && payload.symbol) || "";
var entryPrice = Number((payload && payload.entryPrice) || 0);
var stopLoss = Number((payload && payload.stopLoss) || 0);
var tp1 = Number((payload && payload.tp1) || 0);
var recTime = payload && payload.recTime ? Date.parse(payload.recTime) : 0;
var tp1Time = payload && payload.tp1Time ? Date.parse(payload.tp1Time) : 0;
var slTime = payload && payload.slTime ? Date.parse(payload.slTime) : 0;
var actionStatus = (payload && payload.actionStatus) || "";
var tradeMarkers = ((payload && payload.tradeMarkers) || []).map(function (marker) {
return {
time: marker && marker.time ? Date.parse(marker.time) : 0,
price: Number((marker && marker.price) || 0),
label: String((marker && marker.label) || "操作"),
type: String((marker && marker.type) || "event"),
note: String((marker && marker.note) || "")
};
}).filter(function (marker) {
return marker.time > 0;
});
var refPrice = Number((payload && payload.refPrice) || entryPrice || candles[candles.length - 1].close || 0);
var decimals = priceDecimals(refPrice || entryPrice || candles[candles.length - 1].close);
container.innerHTML = '';
var root = container.querySelector(".ax-chart");
var canvas = root.querySelector("canvas");
var ctx = canvas.getContext("2d");
var tip = root.querySelector(".ax-tooltip");
var locked = false;
var activeIdx = null;
var ro = null;
var minPrice = Infinity, maxPrice = -Infinity, maxVol = 1;
candles.forEach(function (c) {
minPrice = Math.min(minPrice, c.low);
maxPrice = Math.max(maxPrice, c.high);
maxVol = Math.max(maxVol, c.volume || 0);
});
[entryPrice, stopLoss, tp1].forEach(function (p) {
if (p > 0) {
minPrice = Math.min(minPrice, p);
maxPrice = Math.max(maxPrice, p);
}
});
tradeMarkers.forEach(function (marker) {
if (marker.price > 0) {
minPrice = Math.min(minPrice, marker.price);
maxPrice = Math.max(maxPrice, marker.price);
}
});
var padding = Math.max((maxPrice - minPrice) * 0.06, maxPrice * 0.001);
minPrice -= padding;
maxPrice += padding;
if (maxPrice <= minPrice) maxPrice = minPrice + 1;
function metrics() {
var rect = root.getBoundingClientRect();
var w = Math.max(300, rect.width || container.clientWidth || 360);
var h = Math.max(190, rect.height || container.clientHeight || 200);
var padL = 4, padR = 4, padT = 12, padB = 20, volH = 32;
var chartH = Math.max(108, h - padT - padB - volH);
var chartW = Math.max(1, w - padL - padR);
return {w: w, h: h, padL: padL, padR: padR, padT: padT, padB: padB, volH: volH, chartH: chartH, chartW: chartW};
}
function resizeCanvas(m) {
var dpr = Math.max(1, window.devicePixelRatio || 1);
var bw = Math.round(m.w * dpr);
var bh = Math.round(m.h * dpr);
if (canvas.width !== bw || canvas.height !== bh) {
canvas.width = bw;
canvas.height = bh;
}
canvas.style.width = m.w + "px";
canvas.style.height = m.h + "px";
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
function x(i, m) {
var step = m.chartW / candles.length;
return m.padL + step * i + step / 2;
}
function y(price, m) {
return m.padT + (maxPrice - price) * m.chartH / (maxPrice - minPrice);
}
function nearestIndexFromTime(ts) {
if (!ts) return -1;
var best = -1, diff = Infinity;
candles.forEach(function (c, i) {
var d = Math.abs(c.time - ts);
if (d < diff) {
diff = d;
best = i;
}
});
return best;
}
function drawGrid(m) {
ctx.strokeStyle = "rgba(148,163,184,.20)";
ctx.lineWidth = 1;
ctx.font = "800 10px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
ctx.fillStyle = color("--stone", "#8e91a0");
ctx.textBaseline = "middle";
ctx.textAlign = "right";
for (var i = 0; i <= 3; i++) {
var gy = m.padT + m.chartH * i / 3;
var p = maxPrice - (maxPrice - minPrice) * i / 3;
ctx.beginPath();
ctx.moveTo(m.padL, gy);
ctx.lineTo(m.w - m.padR, gy);
ctx.stroke();
ctx.fillText(fmtPrice(p, decimals), m.w - m.padR - 4, gy + 8);
}
}
function drawCandles(m) {
var step = m.chartW / candles.length;
var cw = Math.max(2, Math.min(9, step * 0.62));
candles.forEach(function (c, i) {
var up = c.close >= c.open;
var cx = x(i, m);
var x0 = cx - cw / 2;
var yo = y(c.open, m), yc = y(c.close, m), yh = y(c.high, m), yl = y(c.low, m);
ctx.strokeStyle = up ? "rgba(0,180,115,.95)" : "rgba(229,62,62,.92)";
ctx.fillStyle = up ? "rgba(0,180,115,.86)" : "rgba(229,62,62,.84)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx, yh);
ctx.lineTo(cx, yl);
ctx.stroke();
ctx.fillRect(x0, Math.min(yo, yc), cw, Math.max(1, Math.abs(yc - yo)));
var vh = Math.max(1, (c.volume || 0) / maxVol * (m.volH - 4));
ctx.fillStyle = up ? "rgba(0,180,115,.24)" : "rgba(229,62,62,.20)";
ctx.fillRect(x0, m.h - m.padB - vh, cw, vh);
});
}
function drawPriceMarker(m, value, label, markerColor) {
if (!(value > 0)) return;
var py = y(value, m);
ctx.save();
ctx.strokeStyle = markerColor;
ctx.globalAlpha = 0.75;
ctx.lineWidth = 1;
ctx.setLineDash([5, 4]);
ctx.beginPath();
ctx.moveTo(m.padL, py);
ctx.lineTo(m.w - m.padR, py);
ctx.stroke();
ctx.restore();
var text = label + " " + fmtPrice(value, decimals);
ctx.font = "900 10px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
var tw = ctx.measureText(text).width + 10;
ctx.fillStyle = "rgba(255,255,255,.88)";
ctx.strokeStyle = markerColor;
ctx.lineWidth = 1;
ctx.beginPath();
roundedRect(ctx, m.padL + 4, py - 9, tw, 18, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = markerColor;
ctx.textAlign = "left";
ctx.textBaseline = "middle";
ctx.fillText(text, m.padL + 9, py);
}
function drawEventMarker(m, ts, label, markerColor, price) {
var idx = nearestIndexFromTime(ts);
if (idx < 0) return;
var px = x(idx, m);
var baseY = m.h - m.padB - m.volH + 14;
ctx.save();
ctx.strokeStyle = markerColor;
ctx.globalAlpha = 0.78;
ctx.setLineDash([3, 4]);
ctx.beginPath();
ctx.moveTo(px, y(candles[idx].low, m) + 3);
ctx.lineTo(px, baseY);
ctx.stroke();
ctx.restore();
ctx.font = "950 12px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = markerColor;
ctx.fillText(label, px, baseY + 9);
if (price > 0) {
ctx.font = "850 9px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
ctx.fillText("$" + fmtPrice(price, decimals), px, baseY + 22);
}
}
function markerColorFor(marker) {
var type = String((marker && marker.type) || "");
if (type === "open" || type === "fill") return color("--green", "#00b473");
if (type === "close") return color("--blue", "#4262ff");
if (type === "cancel") return color("--red", "#e53e3e");
if (type === "trailing") return "#a05a00";
if (type === "order") return color("--yellow-deep", "#fcb900");
return color("--stone", "#8e91a0");
}
function drawTradeMarker(m, marker, stack) {
var idx = nearestIndexFromTime(marker.time);
if (idx < 0) return;
var px = x(idx, m);
var rawY = marker.price > 0 ? y(marker.price, m) : y(candles[idx].close, m);
var py = Math.max(m.padT + 16, Math.min(m.padT + m.chartH - 12, rawY));
var markerColor = markerColorFor(marker);
var label = String(marker.label || "操作").slice(0, 4);
var labelY = Math.max(m.padT + 10, py - 20 - (stack % 3) * 18);
ctx.save();
ctx.strokeStyle = markerColor;
ctx.globalAlpha = 0.78;
ctx.setLineDash([3, 4]);
ctx.beginPath();
ctx.moveTo(px, m.padT);
ctx.lineTo(px, m.h - m.padB - m.volH + 10);
ctx.stroke();
ctx.restore();
ctx.beginPath();
ctx.arc(px, py, 4.5, 0, Math.PI * 2);
ctx.fillStyle = markerColor;
ctx.fill();
ctx.strokeStyle = "rgba(255,255,255,.92)";
ctx.lineWidth = 2;
ctx.stroke();
ctx.font = "950 10px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
var tw = ctx.measureText(label).width + 12;
var x0 = Math.max(m.padL + 2, Math.min(m.w - m.padR - tw - 2, px - tw / 2));
ctx.fillStyle = "rgba(255,255,255,.92)";
ctx.strokeStyle = markerColor;
ctx.lineWidth = 1;
ctx.beginPath();
roundedRect(ctx, x0, labelY - 9, tw, 18, 9);
ctx.fill();
ctx.stroke();
ctx.fillStyle = markerColor;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(label, x0 + tw / 2, labelY);
}
function drawLabels(m) {
ctx.fillStyle = color("--stone", "#8e91a0");
ctx.font = "850 10px ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
ctx.textBaseline = "middle";
ctx.textAlign = "left";
ctx.fillText(fmtCandleDate(candles[0].time), m.padL + 6, m.h - 10);
ctx.textAlign = "right";
ctx.fillText(fmtCandleDate(candles[candles.length - 1].time), m.w - m.padR - 6, m.h - 10);
}
function drawFocus(m) {
if (activeIdx == null) return;
var c = candles[activeIdx];
var px = x(activeIdx, m);
var py = y(c.close, m);
ctx.save();
ctx.strokeStyle = "rgba(15,23,42,.35)";
ctx.lineWidth = 1;
ctx.setLineDash([4, 4]);
ctx.beginPath();
ctx.moveTo(px, m.padT);
ctx.lineTo(px, m.h - m.padB);
ctx.moveTo(m.padL, py);
ctx.lineTo(m.w - m.padR, py);
ctx.stroke();
ctx.restore();
ctx.beginPath();
ctx.arc(px, py, 4, 0, Math.PI * 2);
ctx.fillStyle = color("--canvas", "#fff");
ctx.fill();
ctx.strokeStyle = color("--blue", "#4262ff");
ctx.lineWidth = 2;
ctx.stroke();
placeTip(c, px, py, m);
}
function render() {
var m = metrics();
resizeCanvas(m);
ctx.clearRect(0, 0, m.w, m.h);
drawGrid(m);
drawPriceMarker(m, tp1, "TP", color("--green", "#00b473"));
drawPriceMarker(m, entryPrice, actionStatus === "等回踩" ? "回踩" : "入场", color("--blue", "#4262ff"));
drawPriceMarker(m, stopLoss, "SL", color("--red", "#e53e3e"));
drawCandles(m);
if (tradeMarkers.length) {
var stacks = {};
tradeMarkers.forEach(function (marker) {
var idx = nearestIndexFromTime(marker.time);
if (idx < 0) return;
stacks[idx] = stacks[idx] || 0;
drawTradeMarker(m, marker, stacks[idx]);
stacks[idx] += 1;
});
} else {
if (recTime) drawEventMarker(m, recTime, actionStatus === "等回踩" ? "△" : "▲", actionStatus === "等回踩" ? "#8e91a0" : color("--blue", "#4262ff"), entryPrice);
if (tp1Time) drawEventMarker(m, tp1Time, "✓", color("--green", "#00b473"), tp1);
if (slTime) drawEventMarker(m, slTime, "×", color("--red", "#e53e3e"), stopLoss);
}
drawFocus(m);
drawLabels(m);
}
function nearestFromEvent(ev) {
var rect = canvas.getBoundingClientRect();
var sx = ev.clientX - rect.left;
var m = metrics();
var idx = Math.floor((sx - m.padL) / Math.max(1, m.chartW) * candles.length);
return Math.max(0, Math.min(candles.length - 1, idx));
}
function placeTip(c, px, py, m) {
var pct = c.open ? (c.close / c.open - 1) * 100 : 0;
tip.innerHTML =
'' + esc(symbol ? symbol + " · " : "") + esc(fmtCandleDate(c.time)) + '
' +
'开$' + fmtPrice(c.open, decimals) + '
' +
'高 / 低$' + fmtPrice(c.high, decimals) + ' / $' + fmtPrice(c.low, decimals) + '
' +
'收$' + fmtPrice(c.close, decimals) + ' (' + (pct > 0 ? "+" : "") + pct.toFixed(2) + '%)
' +
'成交量' + fmt(c.volume, 0) + '
';
tip.style.left = Math.max(96, Math.min(m.w - 96, px)) + "px";
tip.style.top = Math.max(42, py) + "px";
tip.classList.add("show");
}
function hide() {
if (locked) return;
activeIdx = null;
tip.classList.remove("show");
render();
}
root.addEventListener("mousemove", function (ev) {
if (locked) return;
activeIdx = nearestFromEvent(ev);
render();
});
root.addEventListener("mouseleave", hide);
root.addEventListener("click", function (ev) {
activeIdx = nearestFromEvent(ev);
locked = !locked;
render();
});
root.addEventListener("keydown", function (ev) {
if (ev.key === "Escape") {
locked = false;
hide();
}
});
if (window.ResizeObserver) {
ro = new ResizeObserver(render);
ro.observe(root);
} else {
window.addEventListener("resize", render);
}
render();
root._axDestroy = function () {
if (ro) ro.disconnect();
window.removeEventListener("resize", render);
};
container._axDestroy = root._axDestroy;
}
return { renderEquity: renderEquity, renderKline: renderKline };
})();