From ab862efc4e1fd426cc28cc998294b6bfc53a8d61 Mon Sep 17 00:00:00 2001 From: dekun Date: Fri, 12 Jun 2026 20:45:09 +0800 Subject: [PATCH] feat(hub): add yesterday close and high-low price line toggles on market chart Co-authored-by: Cursor --- manual_trading_hub/static/chart.js | 208 ++++++++++++++++++++++++++- manual_trading_hub/static/index.html | 8 +- 2 files changed, 209 insertions(+), 7 deletions(-) diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index e704734..a563eb2 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -193,8 +193,13 @@ const elIndEma = document.getElementById("market-ind-ema"); const elIndMacd = document.getElementById("market-ind-macd"); const elIndRsi = document.getElementById("market-ind-rsi"); + const elPrevCloseLine = document.getElementById("market-prev-close-line"); + const elPrevHlLines = document.getElementById("market-prev-hl-lines"); const elDaySplit = document.getElementById("market-day-split"); + const PREV_CLOSE_LINE_STORAGE_KEY = "hub-market-prev-close-line"; + const PREV_HL_LINES_STORAGE_KEY = "hub-market-prev-hl-lines"; const DAY_SPLIT_STORAGE_KEY = "hub-market-day-split"; + const BJ_OFFSET_SEC = 8 * 60 * 60; const elFsToolbar = document.getElementById("market-fs-toolbar"); const elFsExchange = document.getElementById("market-fs-exchange"); const elFsSymbol = document.getElementById("market-fs-symbol"); @@ -226,6 +231,7 @@ let priceTick = null; let priceAutoScale = true; let rangeMarkers = []; + let yesterdayPriceLines = []; let positionLines = []; let posContext = null; let posPnlTimer = null; @@ -310,19 +316,129 @@ syncChartWrapLayout(); } - function loadDaySplitPref() { + function loadBoolPref(key, defaultValue) { try { - const raw = localStorage.getItem(DAY_SPLIT_STORAGE_KEY); + const raw = localStorage.getItem(key); if (raw === "1" || raw === "true") return true; if (raw === "0" || raw === "false") return false; } catch (_) {} - return false; + return !!defaultValue; + } + + function saveBoolPref(key, on) { + try { + localStorage.setItem(key, on ? "1" : "0"); + } catch (_) {} + } + + function loadDaySplitPref() { + return loadBoolPref(DAY_SPLIT_STORAGE_KEY, false); } function saveDaySplitPref(on) { - try { - localStorage.setItem(DAY_SPLIT_STORAGE_KEY, on ? "1" : "0"); - } catch (_) {} + saveBoolPref(DAY_SPLIT_STORAGE_KEY, on); + } + + function loadPrevCloseLinePref() { + return loadBoolPref(PREV_CLOSE_LINE_STORAGE_KEY, false); + } + + function savePrevCloseLinePref(on) { + saveBoolPref(PREV_CLOSE_LINE_STORAGE_KEY, on); + } + + function loadPrevHlLinesPref() { + return loadBoolPref(PREV_HL_LINES_STORAGE_KEY, false); + } + + function savePrevHlLinesPref(on) { + saveBoolPref(PREV_HL_LINES_STORAGE_KEY, on); + } + + function chartResetHour() { + return chartMeta && chartMeta.volume_rank_reset_hour != null + ? Number(chartMeta.volume_rank_reset_hour) + : 8; + } + + function utcSecToBjParts(utcSec) { + const d = new Date((Number(utcSec) + BJ_OFFSET_SEC) * 1000); + return { + y: d.getUTCFullYear(), + m: d.getUTCMonth(), + d: d.getUTCDate(), + h: d.getUTCHours(), + }; + } + + function tradingDayKeyFromUtcSec(utcSec, resetHour) { + const p = utcSecToBjParts(utcSec); + let y = p.y; + let m = p.m; + let d = p.d; + if (p.h < resetHour) { + const prev = new Date(Date.UTC(y, m, d) - 86400000); + y = prev.getUTCFullYear(); + m = prev.getUTCMonth(); + d = prev.getUTCDate(); + } + return ( + y + + "-" + + String(m + 1).padStart(2, "0") + + "-" + + String(d).padStart(2, "0") + ); + } + + function prevTradingDayKey(tdKey) { + const parts = String(tdKey || "").split("-"); + if (parts.length !== 3) return ""; + const dt = new Date(Date.UTC(Number(parts[0]), Number(parts[1]) - 1, Number(parts[2]))); + const prev = new Date(dt.getTime() - 86400000); + return ( + prev.getUTCFullYear() + + "-" + + String(prev.getUTCMonth() + 1).padStart(2, "0") + + "-" + + String(prev.getUTCDate()).padStart(2, "0") + ); + } + + function computePrevTradingDayOhlc(candles, resetHour) { + if (!candles || !candles.length) return null; + const curTd = tradingDayKeyFromUtcSec(candles[candles.length - 1].time, resetHour); + const prevTd = prevTradingDayKey(curTd); + if (!prevTd) return null; + const dayCandles = candles + .filter(function (c) { + return c && tradingDayKeyFromUtcSec(c.time, resetHour) === prevTd; + }) + .sort(function (a, b) { + return a.time - b.time; + }); + if (!dayCandles.length) return null; + let hi = null; + let lo = null; + dayCandles.forEach(function (c) { + if (!hi || c.high > hi) hi = c.high; + if (!lo || c.low < lo) lo = c.low; + }); + const last = dayCandles[dayCandles.length - 1]; + return { + close: last.close, + high: hi, + low: lo, + tradingDay: prevTd, + }; + } + + function syncPrevDayLineUi() { + const closeOn = !!(elPrevCloseLine && elPrevCloseLine.checked); + const hlOn = !!(elPrevHlLines && elPrevHlLines.checked); + savePrevCloseLinePref(closeOn); + savePrevHlLinesPref(hlOn); + updateYesterdayPriceLines(); } function applyTradingDaySplit(enabled) { @@ -2127,6 +2243,74 @@ rangeMarkers = []; } + function clearYesterdayPriceLines() { + if (candleSeries) { + yesterdayPriceLines.forEach(function (m) { + try { + candleSeries.removePriceLine(m); + } catch (e) {} + }); + } + yesterdayPriceLines = []; + } + + function updateYesterdayPriceLines() { + clearYesterdayPriceLines(); + if (!candleSeries || !lastCandles.length) return; + const showClose = !!(elPrevCloseLine && elPrevCloseLine.checked); + const showHl = !!(elPrevHlLines && elPrevHlLines.checked); + if (!showClose && !showHl) return; + const stats = computePrevTradingDayOhlc(lastCandles, chartResetHour()); + if (!stats) return; + if (showClose && stats.close != null && Number.isFinite(Number(stats.close))) { + const px = Number(roundToTick(stats.close)); + if (Number.isFinite(px)) { + yesterdayPriceLines.push( + candleSeries.createPriceLine({ + price: px, + color: "#a78bfa", + lineWidth: 1, + lineStyle: 2, + axisLabelVisible: true, + title: "昨收", + }) + ); + } + } + if (showHl) { + if (stats.high != null && Number.isFinite(Number(stats.high))) { + const hiPx = Number(roundToTick(stats.high)); + if (Number.isFinite(hiPx)) { + yesterdayPriceLines.push( + candleSeries.createPriceLine({ + price: hiPx, + color: "#ffb84d", + lineWidth: 1, + lineStyle: 2, + axisLabelVisible: true, + title: "昨高", + }) + ); + } + } + if (stats.low != null && Number.isFinite(Number(stats.low))) { + const loPx = Number(roundToTick(stats.low)); + if (Number.isFinite(loPx)) { + yesterdayPriceLines.push( + candleSeries.createPriceLine({ + price: loPx, + color: "#4cd97f", + lineWidth: 1, + lineStyle: 2, + axisLabelVisible: true, + title: "昨低", + }) + ); + } + } + } + } + function viewKey(exKey, sym, tf) { return (exKey || "") + "|" + (sym || "") + "|" + (tf || ""); } @@ -2199,6 +2383,7 @@ applyChartRightGap(); restoreVisibleLogicalRange(range, lastCandles.length); updateVisibleRangeMarkers(); + updateYesterdayPriceLines(); } applyOnce(); requestAnimationFrame(applyOnce); @@ -2249,6 +2434,7 @@ function clearChartSeriesData() { lastCandles = []; candleByTime = {}; + clearYesterdayPriceLines(); if (candleSeries) candleSeries.setData([]); if (volumeSeries) volumeSeries.setData([]); } @@ -2315,6 +2501,7 @@ } catch (indErr) {} } updateVisibleRangeMarkers(); + updateYesterdayPriceLines(); showLatestOhlcv(); return true; } @@ -2349,6 +2536,7 @@ applyPriceAutoScale(); } updateVisibleRangeMarkers(); + updateYesterdayPriceLines(); try { updateIndicators(); } catch (indErr) {} @@ -3036,6 +3224,14 @@ updateIndicators(); }); }); + if (elPrevCloseLine) { + elPrevCloseLine.checked = loadPrevCloseLinePref(); + elPrevCloseLine.addEventListener("change", syncPrevDayLineUi); + } + if (elPrevHlLines) { + elPrevHlLines.checked = loadPrevHlLinesPref(); + elPrevHlLines.addEventListener("change", syncPrevDayLineUi); + } if (elDaySplit) { elDaySplit.checked = loadDaySplitPref(); elDaySplit.addEventListener("change", syncTradingDaySplitUi); diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index d7d9199..10e4937 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -132,6 +132,12 @@ 1d
+ + @@ -588,7 +594,7 @@
- +