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 @@
+