diff --git a/manual_trading_hub/static/chart.js b/manual_trading_hub/static/chart.js index debebd1..115190b 100644 --- a/manual_trading_hub/static/chart.js +++ b/manual_trading_hub/static/chart.js @@ -197,9 +197,11 @@ const elPrevCloseLine = document.getElementById("market-prev-close-line"); const elPrevHlLines = document.getElementById("market-prev-hl-lines"); const elDaySplit = document.getElementById("market-day-split"); + const elOptionExpirySplit = document.getElementById("market-option-expiry-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 OPTION_EXPIRY_SPLIT_STORAGE_KEY = "hub-market-option-expiry-split"; const BJ_OFFSET_SEC = 8 * 60 * 60; const elFsToolbar = document.getElementById("market-fs-toolbar"); const elFsExchange = document.getElementById("market-fs-exchange"); @@ -342,6 +344,14 @@ saveBoolPref(DAY_SPLIT_STORAGE_KEY, on); } + function loadOptionExpirySplitPref() { + return loadBoolPref(OPTION_EXPIRY_SPLIT_STORAGE_KEY, false); + } + + function saveOptionExpirySplitPref(on) { + saveBoolPref(OPTION_EXPIRY_SPLIT_STORAGE_KEY, on); + } + function loadPrevCloseLinePref() { return loadBoolPref(PREV_CLOSE_LINE_STORAGE_KEY, false); } @@ -456,6 +466,18 @@ applyTradingDaySplit(on); } + function applyOptionExpirySplit(enabled) { + if (window.HubChartDraw && typeof window.HubChartDraw.setOptionExpirySplit === "function") { + window.HubChartDraw.setOptionExpirySplit(enabled); + } + } + + function syncOptionExpirySplitUi() { + const on = !!(elOptionExpirySplit && elOptionExpirySplit.checked); + saveOptionExpirySplitPref(on); + applyOptionExpirySplit(on); + } + function ensureDrawLayer() { if (drawAttached || !window.HubChartDraw || !chart || !candleSeries) return; window.HubChartDraw.attach({ @@ -471,6 +493,9 @@ }); window.HubChartDraw.setViewKey(currentChartViewKey()); applyTradingDaySplit(elDaySplit ? elDaySplit.checked : loadDaySplitPref()); + applyOptionExpirySplit( + elOptionExpirySplit ? elOptionExpirySplit.checked : loadOptionExpirySplitPref() + ); drawAttached = true; } @@ -3481,6 +3506,11 @@ elDaySplit.addEventListener("change", syncTradingDaySplitUi); applyTradingDaySplit(elDaySplit.checked); } + if (elOptionExpirySplit) { + elOptionExpirySplit.checked = loadOptionExpirySplitPref(); + elOptionExpirySplit.addEventListener("change", syncOptionExpirySplitUi); + applyOptionExpirySplit(elOptionExpirySplit.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 6832f35..ffa43f2 100644 --- a/manual_trading_hub/static/chart_draw.js +++ b/manual_trading_hub/static/chart_draw.js @@ -68,6 +68,7 @@ let unsubClick = null; let mainBound = false; let tradingDaySplitEnabled = false; + let optionExpirySplitEnabled = false; const BJ_OFFSET_SEC = 8 * 60 * 60; function uid() { @@ -409,6 +410,53 @@ ctx.restore(); } + /** OKX 期权到期:每周五 08:00 UTC(=北京 16:00) */ + function collectOptionExpiryBoundaries(candles) { + if (!candles.length) return []; + const minT = Number(candles[0].time); + const maxT = Number(candles[candles.length - 1].time); + if (!Number.isFinite(minT) || !Number.isFinite(maxT)) return []; + const out = []; + const cur = new Date(minT * 1000); + cur.setUTCHours(0, 0, 0, 0); + cur.setUTCDate(cur.getUTCDate() - 1); + const endSec = maxT + 2 * 86400; + while (cur.getTime() / 1000 <= endSec) { + if (cur.getUTCDay() === 5) { + const boundary = Math.floor( + Date.UTC(cur.getUTCFullYear(), cur.getUTCMonth(), cur.getUTCDate(), 8, 0, 0) / 1000 + ); + if (boundary >= minT - 3600 && boundary <= maxT + 3600) { + if (!out.length || out[out.length - 1] !== boundary) out.push(boundary); + } + } + cur.setUTCDate(cur.getUTCDate() + 1); + } + return out; + } + + function drawOptionExpirySplits(ctx, w, h) { + if (!optionExpirySplitEnabled || !chart) return; + const candles = getCandles(); + if (!candles.length) return; + const boundaries = collectOptionExpiryBoundaries(candles); + if (!boundaries.length) return; + ctx.save(); + ctx.strokeStyle = "#eab308"; + 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); @@ -743,6 +791,7 @@ const h = hostEl.clientHeight; ctx.clearRect(0, 0, w, h); drawTradingDaySplits(ctx, w, h); + drawOptionExpirySplits(ctx, w, h); drawings.forEach(function (d) { if (d.hidden) ctx.globalAlpha = 0.14; renderDrawing(ctx, d, w, h, d.id === selectedId); @@ -1451,10 +1500,16 @@ scheduleRedraw(); } + function setOptionExpirySplit(enabled) { + optionExpirySplitEnabled = !!enabled; + scheduleRedraw(); + } + window.HubChartDraw = { attach: attach, setViewKey: setViewKey, setTradingDaySplit: setTradingDaySplit, + setOptionExpirySplit: setOptionExpirySplit, resize: scheduleRedraw, redraw: scheduleRedraw, destroy: destroy, diff --git a/manual_trading_hub/static/index.html b/manual_trading_hub/static/index.html index f7424bd..ea1d244 100644 --- a/manual_trading_hub/static/index.html +++ b/manual_trading_hub/static/index.html @@ -324,6 +324,9 @@ +
技术指标
@@ -1393,8 +1396,8 @@
- - + +