diff --git a/manual_trading_hub/static/app.css b/manual_trading_hub/static/app.css index 44c8d40..1039955 100644 --- a/manual_trading_hub/static/app.css +++ b/manual_trading_hub/static/app.css @@ -2831,6 +2831,34 @@ body.login-page { gap: 6px 10px; } +.market-day-split-opt { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.72rem; + color: var(--muted); + cursor: pointer; + user-select: none; + padding: 2px 8px; + border-radius: 4px; + border: 1px solid var(--border-soft); + white-space: nowrap; +} + +.market-day-split-opt:hover { + color: var(--text); + border-color: var(--border); +} + +.market-day-split-opt input { + accent-color: #3b82f6; +} + +.market-day-split-opt:has(input:checked) { + color: #3b82f6; + border-color: rgba(59, 130, 246, 0.45); +} + .market-ind-menu { position: relative; font-size: 0.72rem; diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index 3baae74..e704734 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -193,6 +193,8 @@ const elIndEma = document.getElementById("market-ind-ema"); const elIndMacd = document.getElementById("market-ind-macd"); const elIndRsi = document.getElementById("market-ind-rsi"); + const elDaySplit = document.getElementById("market-day-split"); + const DAY_SPLIT_STORAGE_KEY = "hub-market-day-split"; const elFsToolbar = document.getElementById("market-fs-toolbar"); const elFsExchange = document.getElementById("market-fs-exchange"); const elFsSymbol = document.getElementById("market-fs-symbol"); @@ -308,6 +310,33 @@ syncChartWrapLayout(); } + function loadDaySplitPref() { + try { + const raw = localStorage.getItem(DAY_SPLIT_STORAGE_KEY); + if (raw === "1" || raw === "true") return true; + if (raw === "0" || raw === "false") return false; + } catch (_) {} + return false; + } + + function saveDaySplitPref(on) { + try { + localStorage.setItem(DAY_SPLIT_STORAGE_KEY, on ? "1" : "0"); + } catch (_) {} + } + + function applyTradingDaySplit(enabled) { + if (window.HubChartDraw && typeof window.HubChartDraw.setTradingDaySplit === "function") { + window.HubChartDraw.setTradingDaySplit(enabled); + } + } + + function syncTradingDaySplitUi() { + const on = !!(elDaySplit && elDaySplit.checked); + saveDaySplitPref(on); + applyTradingDaySplit(on); + } + function ensureDrawLayer() { if (drawAttached || !window.HubChartDraw || !chart || !candleSeries) return; window.HubChartDraw.attach({ @@ -322,6 +351,7 @@ }, }); window.HubChartDraw.setViewKey(currentChartViewKey()); + applyTradingDaySplit(elDaySplit ? elDaySplit.checked : loadDaySplitPref()); drawAttached = true; } @@ -3006,6 +3036,11 @@ updateIndicators(); }); }); + if (elDaySplit) { + elDaySplit.checked = loadDaySplitPref(); + elDaySplit.addEventListener("change", syncTradingDaySplitUi); + applyTradingDaySplit(elDaySplit.checked); + } const pageMarket = document.getElementById("page-market"); const fsKeyTargets = [window, pageMarket, elChartWrap, chartHost].filter(Boolean); fsKeyTargets.forEach(function (el) { diff --git a/manual_trading_hub/static/chart_draw.js b/manual_trading_hub/static/chart_draw.js index d04276b..05aafe4 100644 --- a/manual_trading_hub/static/chart_draw.js +++ b/manual_trading_hub/static/chart_draw.js @@ -67,6 +67,8 @@ let menuEl = null; let unsubClick = null; let mainBound = false; + let tradingDaySplitEnabled = false; + const BJ_OFFSET_SEC = 8 * 60 * 60; function uid() { return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7); @@ -354,6 +356,59 @@ drawLine(ctx, x, 0, x, h, selected); } + 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 collectTradingDayBoundaries(candles) { + if (!candles.length) return []; + const minT = Number(candles[0].time); + const maxT = Number(candles[candles.length - 1].time); + const minP = utcSecToBjParts(minT); + const maxP = utcSecToBjParts(maxT); + const out = []; + let curMs = Date.UTC(minP.y, minP.m, minP.d) - 86400000; + const endMs = Date.UTC(maxP.y, maxP.m, maxP.d) + 2 * 86400000; + while (curMs <= endMs) { + const boundary = Math.floor(curMs / 1000); + if (boundary >= minT - 3600 && boundary <= maxT + 3600) { + if (!out.length || out[out.length - 1] !== boundary) { + out.push(boundary); + } + } + curMs += 86400000; + } + return out; + } + + function drawTradingDaySplits(ctx, w, h) { + if (!tradingDaySplitEnabled || !chart) return; + const candles = getCandles(); + if (!candles.length) return; + const boundaries = collectTradingDayBoundaries(candles); + if (!boundaries.length) return; + ctx.save(); + ctx.strokeStyle = "#3b82f6"; + ctx.lineWidth = 1; + ctx.setLineDash([5, 4]); + boundaries.forEach(function (t) { + const x = timeToX(t); + if (x == null || !Number.isFinite(x) || x < -2 || x > w + 2) return; + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, h); + ctx.stroke(); + }); + ctx.setLineDash([]); + ctx.restore(); + } + function drawRect(ctx, x1, y1, x2, y2, selected) { if (x1 == null || y1 == null || x2 == null || y2 == null) return; const l = Math.min(x1, x2); @@ -687,6 +742,7 @@ const w = hostEl.clientWidth; const h = hostEl.clientHeight; ctx.clearRect(0, 0, w, h); + drawTradingDaySplits(ctx, w, h); drawings.forEach(function (d) { if (d.hidden) ctx.globalAlpha = 0.14; renderDrawing(ctx, d, w, h, d.id === selectedId); @@ -1390,9 +1446,15 @@ setChartInteraction(true); } + function setTradingDaySplit(enabled) { + tradingDaySplitEnabled = !!enabled; + scheduleRedraw(); + } + window.HubChartDraw = { attach: attach, setViewKey: setViewKey, + setTradingDaySplit: setTradingDaySplit, resize: scheduleRedraw, redraw: scheduleRedraw, destroy: destroy, diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index 6723e18..82e890c 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -15,7 +15,7 @@ - + @@ -129,6 +129,9 @@ 1d
+
技术指标
@@ -420,8 +423,8 @@
- - + +