/** * 实盘/关键位放大 K 线:交易所 tick 精度、主题感知图表、高对比 meta。 */ (function (global) { "use strict"; let activePriceTick = null; function currentTheme() { return document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark"; } function chartTheme(theme) { if (theme === "light") { return { layout: { background: { color: "#f0f4f9" }, textColor: "#142232" }, grid: { vertLines: { color: "#d0dae4" }, horzLines: { color: "#d0dae4" } }, rightPriceScale: { borderColor: "#b8c8d8" }, timeScale: { borderColor: "#b8c8d8" }, candle: { upColor: "#0a7a3d", downColor: "#c62828", wickUpColor: "#0a7a3d", wickDownColor: "#c62828", }, }; } return { layout: { background: { color: "#0f1320" }, textColor: "#d6deff" }, grid: { vertLines: { color: "#1e263d" }, horzLines: { color: "#1e263d" } }, rightPriceScale: { borderColor: "#2a3150" }, timeScale: { borderColor: "#2a3150" }, candle: { upColor: "#4cd97f", downColor: "#ff6666", wickUpColor: "#4cd97f", wickDownColor: "#ff6666", }, }; } const SAFE_PRICE_FORMAT = { type: "price", precision: 4, minMove: 0.0001 }; function decimalsFromTick(tick) { if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) return null; const minMove = Number(tick); if (minMove >= 1) return 0; const raw = String(minMove); const sci = raw.match(/e-(\d+)/i); if (sci) return Math.min(12, parseInt(sci[1], 10)); const fixed = minMove.toFixed(12); const frac = fixed.split(".")[1] || ""; const trimmed = frac.replace(/0+$/, ""); if (trimmed.length) return Math.min(12, trimmed.length); return Math.max(0, Math.min(12, Math.round(-Math.log10(minMove)))); } function tickToPriceFormat(tick) { try { if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) { return { type: "price", precision: 2, minMove: 0.01 }; } const minMove = Number(tick); let prec = decimalsFromTick(minMove); if (prec == null || prec < 0) prec = 4; prec = Math.min(12, Math.max(0, Math.floor(prec))); return { type: "price", precision: prec, minMove: minMove }; } catch (_) { return SAFE_PRICE_FORMAT; } } function roundToTick(v, tick) { if (v == null || Number.isNaN(Number(v))) return v; const n = Number(v); if (tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0) return n; const t = Number(tick); const rounded = Math.round(n / t) * t; const dec = decimalsFromTick(t); if (dec == null) return rounded; return parseFloat(rounded.toFixed(dec)); } function fmtPriceByTick(v, tick) { if (v == null || Number.isNaN(Number(v))) return "-"; const n = Number(roundToTick(v, tick)); if (n === 0) return "0"; const dec = decimalsFromTick(tick); if (dec != null) return n.toFixed(dec); const av = Math.abs(n); let d = 8; if (av >= 10000) d = 2; else if (av >= 100) d = 3; else if (av >= 1) d = 4; else if (av >= 0.01) d = 6; const text = n.toFixed(d); return text.includes(".") ? text.replace(/\.?0+$/, "") : text; } function setActivePriceTick(tick) { activePriceTick = tick == null || !Number.isFinite(Number(tick)) || Number(tick) <= 0 ? null : Number(tick); } function formatSigned(v, digits) { digits = digits === undefined ? 2 : digits; if (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-"; const n = Number(v); const sign = n > 0 ? "+" : ""; return sign + n.toFixed(digits); } function formatSignedPrice(v) { if (v === null || typeof v === "undefined" || Number.isNaN(Number(v))) return "-"; const n = Number(v); const body = fmtPriceByTick(Math.abs(n), activePriceTick); if (body === "-") return "-"; return (n > 0 ? "+" : n < 0 ? "-" : "") + body; } function formatRrRatio(rr) { if (rr === null || typeof rr === "undefined") return "-:1"; const n = Number(rr); if (Number.isNaN(n)) return "-:1"; const body = Number.isInteger(n) ? String(n) : String(parseFloat(n.toFixed(2))); return body + ":1"; } function displayPrice(orderOrData, field, rawField) { const dispKey = field + "_display"; if (orderOrData && orderOrData[dispKey] && orderOrData[dispKey] !== "-") { return String(orderOrData[dispKey]); } const raw = orderOrData ? orderOrData[rawField || field] : null; if (raw === null || typeof raw === "undefined" || Number.isNaN(Number(raw))) return "-"; return fmtPriceByTick(raw, activePriceTick); } function lineTitle(label, display) { const d = display && display !== "-" ? display : ""; return d ? label + " " + d : label; } function paintOrderMeta(order) { const symEl = document.getElementById("m-symbol"); const dirEl = document.getElementById("m-direction"); const pnlEl = document.getElementById("m-pnl"); if (symEl) symEl.textContent = order.symbol || "-"; if (dirEl) { const isShort = order.direction === "short"; dirEl.textContent = isShort ? "做空" : "做多"; dirEl.className = "v " + (isShort ? "meta-dir-short" : "meta-dir-long"); } const set = function (id, text) { const el = document.getElementById(id); if (el) el.textContent = text; }; set("m-entry", displayPrice(order, "trigger_price")); set("m-sl", displayPrice(order, "stop_loss")); set("m-tp", displayPrice(order, "take_profit")); set("m-rr", formatRrRatio(order.rr_ratio)); set( "m-breakeven", order.breakeven_enabled === false || order.breakeven_enabled === 0 ? "关闭" : "开启" ); set( "m-price", order.current_price_display || order.price_display || displayPrice(order, "current_price") ); if (pnlEl) { pnlEl.textContent = formatSigned(order.float_pnl, 2) + "U (" + formatSigned(order.float_pct, 2) + "%)"; pnlEl.className = "v"; const pnl = Number(order.float_pnl || 0); if (pnl > 0) pnlEl.classList.add("meta-pnl-up"); else if (pnl < 0) pnlEl.classList.add("meta-pnl-down"); } } function paintKeyMeta(data) { const key = data.key_monitor || null; const symEl = document.getElementById("m-symbol"); if (symEl) symEl.textContent = data.symbol || "-"; const set = function (id, text) { const el = document.getElementById(id); if (el) el.textContent = text; }; set( "m-price", data.current_price_display || displayPrice(data, "current_price") ); const dirEl = document.getElementById("m-direction"); if (!key) { set("m-type", "未匹配到关键位"); set("m-direction", "-"); if (dirEl) dirEl.className = "v"; set("m-upper", "-"); set("m-lower", "-"); set("m-updiff", "-"); set("m-lowdiff", "-"); return; } set("m-type", key.monitor_type || "-"); if (dirEl) { const isShort = key.direction === "short"; dirEl.textContent = isShort ? "做空" : "做多"; dirEl.className = "v " + (isShort ? "meta-dir-short" : "meta-dir-long"); } set("m-upper", key.upper_display || displayPrice(key, "upper")); set("m-lower", key.lower_display || displayPrice(key, "lower")); if (activePriceTick != null) { set( "m-updiff", formatSignedPrice(key.upper_diff) + " (" + formatSigned(key.upper_pct, 2) + "%)" ); set( "m-lowdiff", formatSignedPrice(key.lower_diff) + " (" + formatSigned(key.lower_pct, 2) + "%)" ); } else { set( "m-updiff", formatSigned(key.upper_diff, 4) + " (" + formatSigned(key.upper_pct, 2) + "%)" ); set( "m-lowdiff", formatSigned(key.lower_diff, 4) + " (" + formatSigned(key.lower_pct, 2) + "%)" ); } } function applyPriceFormatToSeries(series, pf) { if (!series || !series.applyOptions) return; try { series.applyOptions({ priceFormat: pf }); } catch (_) { try { series.applyOptions({ priceFormat: SAFE_PRICE_FORMAT }); } catch (_2) {} } } function createFocusChart(host) { if (!global.LightweightCharts) return null; const th = chartTheme(currentTheme()); const chart = global.LightweightCharts.createChart(host, { layout: th.layout, grid: th.grid, rightPriceScale: th.rightPriceScale, timeScale: Object.assign({ timeVisible: true, secondsVisible: false }, th.timeScale), crosshair: { mode: 0 }, localization: { priceFormatter: function (p) { return fmtPriceByTick(p, activePriceTick); }, }, }); let candleSeries = null; function applyChartPriceFormat() { let pf = SAFE_PRICE_FORMAT; try { pf = tickToPriceFormat(activePriceTick); } catch (_) { pf = SAFE_PRICE_FORMAT; } applyPriceFormatToSeries(candleSeries, pf); try { chart.applyOptions({ localization: { priceFormatter: function (p) { return fmtPriceByTick(p, activePriceTick); }, }, }); } catch (_) {} } function setPriceTick(tick) { setActivePriceTick(tick); applyChartPriceFormat(); } const opts = Object.assign({ borderVisible: false }, th.candle); if (typeof chart.addCandlestickSeries === "function") { candleSeries = chart.addCandlestickSeries(opts); } else if ( typeof chart.addSeries === "function" && global.LightweightCharts.CandlestickSeries ) { candleSeries = chart.addSeries(global.LightweightCharts.CandlestickSeries, opts); } applyChartPriceFormat(); const priceLines = []; function resetPriceLines() { if (!candleSeries) return; priceLines.forEach(function (line) { try { candleSeries.removePriceLine(line); } catch (_) {} }); priceLines.length = 0; } function addLine(price, title, color) { if (!candleSeries || price === null || typeof price === "undefined") return; const p = Number(roundToTick(price, activePriceTick)); if (Number.isNaN(p) || p <= 0) return; priceLines.push( candleSeries.createPriceLine({ price: p, color: color, lineWidth: 1, lineStyle: 0, axisLabelVisible: true, title: title, }) ); } function applyTheme() { const t = chartTheme(currentTheme()); chart.applyOptions({ layout: t.layout, grid: t.grid, rightPriceScale: t.rightPriceScale, timeScale: t.timeScale, localization: { priceFormatter: function (p) { return fmtPriceByTick(p, activePriceTick); }, }, }); if (candleSeries && typeof candleSeries.applyOptions === "function") { candleSeries.applyOptions(t.candle); } applyChartPriceFormat(); } function resize() { chart.applyOptions({ width: host.clientWidth, height: host.clientHeight }); } global.addEventListener("resize", resize); resize(); const obs = new MutationObserver(applyTheme); obs.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme"], }); return { chart: chart, candleSeries: candleSeries, resetPriceLines: resetPriceLines, addLine: addLine, applyTheme: applyTheme, setPriceTick: setPriceTick, ensureSeries: function () { if (candleSeries) return true; const t = chartTheme(currentTheme()); const o = Object.assign({ borderVisible: false }, t.candle); if (typeof chart.addCandlestickSeries === "function") { candleSeries = chart.addCandlestickSeries(o); } else if ( typeof chart.addSeries === "function" && global.LightweightCharts.CandlestickSeries ) { candleSeries = chart.addSeries(global.LightweightCharts.CandlestickSeries, o); } applyChartPriceFormat(); return !!candleSeries; }, }; } global.FocusChartPage = { currentTheme: currentTheme, chartTheme: chartTheme, formatSigned: formatSigned, formatRrRatio: formatRrRatio, displayPrice: displayPrice, lineTitle: lineTitle, paintOrderMeta: paintOrderMeta, paintKeyMeta: paintKeyMeta, createFocusChart: createFocusChart, setActivePriceTick: setActivePriceTick, fmtPriceByTick: fmtPriceByTick, }; })(typeof window !== "undefined" ? window : globalThis);