feat: add trading day split lines on hub market chart
Add toggle before technical indicators to show blue dashed vertical lines at Beijing 8:00 day boundaries. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2831,6 +2831,34 @@ body.login-page {
|
|||||||
gap: 6px 10px;
|
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 {
|
.market-ind-menu {
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
|
|||||||
@@ -193,6 +193,8 @@
|
|||||||
const elIndEma = document.getElementById("market-ind-ema");
|
const elIndEma = document.getElementById("market-ind-ema");
|
||||||
const elIndMacd = document.getElementById("market-ind-macd");
|
const elIndMacd = document.getElementById("market-ind-macd");
|
||||||
const elIndRsi = document.getElementById("market-ind-rsi");
|
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 elFsToolbar = document.getElementById("market-fs-toolbar");
|
||||||
const elFsExchange = document.getElementById("market-fs-exchange");
|
const elFsExchange = document.getElementById("market-fs-exchange");
|
||||||
const elFsSymbol = document.getElementById("market-fs-symbol");
|
const elFsSymbol = document.getElementById("market-fs-symbol");
|
||||||
@@ -308,6 +310,33 @@
|
|||||||
syncChartWrapLayout();
|
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() {
|
function ensureDrawLayer() {
|
||||||
if (drawAttached || !window.HubChartDraw || !chart || !candleSeries) return;
|
if (drawAttached || !window.HubChartDraw || !chart || !candleSeries) return;
|
||||||
window.HubChartDraw.attach({
|
window.HubChartDraw.attach({
|
||||||
@@ -322,6 +351,7 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
window.HubChartDraw.setViewKey(currentChartViewKey());
|
window.HubChartDraw.setViewKey(currentChartViewKey());
|
||||||
|
applyTradingDaySplit(elDaySplit ? elDaySplit.checked : loadDaySplitPref());
|
||||||
drawAttached = true;
|
drawAttached = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3006,6 +3036,11 @@
|
|||||||
updateIndicators();
|
updateIndicators();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if (elDaySplit) {
|
||||||
|
elDaySplit.checked = loadDaySplitPref();
|
||||||
|
elDaySplit.addEventListener("change", syncTradingDaySplitUi);
|
||||||
|
applyTradingDaySplit(elDaySplit.checked);
|
||||||
|
}
|
||||||
const pageMarket = document.getElementById("page-market");
|
const pageMarket = document.getElementById("page-market");
|
||||||
const fsKeyTargets = [window, pageMarket, elChartWrap, chartHost].filter(Boolean);
|
const fsKeyTargets = [window, pageMarket, elChartWrap, chartHost].filter(Boolean);
|
||||||
fsKeyTargets.forEach(function (el) {
|
fsKeyTargets.forEach(function (el) {
|
||||||
|
|||||||
@@ -67,6 +67,8 @@
|
|||||||
let menuEl = null;
|
let menuEl = null;
|
||||||
let unsubClick = null;
|
let unsubClick = null;
|
||||||
let mainBound = false;
|
let mainBound = false;
|
||||||
|
let tradingDaySplitEnabled = false;
|
||||||
|
const BJ_OFFSET_SEC = 8 * 60 * 60;
|
||||||
|
|
||||||
function uid() {
|
function uid() {
|
||||||
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
return "d" + Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||||
@@ -354,6 +356,59 @@
|
|||||||
drawLine(ctx, x, 0, x, h, selected);
|
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) {
|
function drawRect(ctx, x1, y1, x2, y2, selected) {
|
||||||
if (x1 == null || y1 == null || x2 == null || y2 == null) return;
|
if (x1 == null || y1 == null || x2 == null || y2 == null) return;
|
||||||
const l = Math.min(x1, x2);
|
const l = Math.min(x1, x2);
|
||||||
@@ -687,6 +742,7 @@
|
|||||||
const w = hostEl.clientWidth;
|
const w = hostEl.clientWidth;
|
||||||
const h = hostEl.clientHeight;
|
const h = hostEl.clientHeight;
|
||||||
ctx.clearRect(0, 0, w, h);
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
drawTradingDaySplits(ctx, w, h);
|
||||||
drawings.forEach(function (d) {
|
drawings.forEach(function (d) {
|
||||||
if (d.hidden) ctx.globalAlpha = 0.14;
|
if (d.hidden) ctx.globalAlpha = 0.14;
|
||||||
renderDrawing(ctx, d, w, h, d.id === selectedId);
|
renderDrawing(ctx, d, w, h, d.id === selectedId);
|
||||||
@@ -1390,9 +1446,15 @@
|
|||||||
setChartInteraction(true);
|
setChartInteraction(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setTradingDaySplit(enabled) {
|
||||||
|
tradingDaySplitEnabled = !!enabled;
|
||||||
|
scheduleRedraw();
|
||||||
|
}
|
||||||
|
|
||||||
window.HubChartDraw = {
|
window.HubChartDraw = {
|
||||||
attach: attach,
|
attach: attach,
|
||||||
setViewKey: setViewKey,
|
setViewKey: setViewKey,
|
||||||
|
setTradingDaySplit: setTradingDaySplit,
|
||||||
resize: scheduleRedraw,
|
resize: scheduleRedraw,
|
||||||
redraw: scheduleRedraw,
|
redraw: scheduleRedraw,
|
||||||
destroy: destroy,
|
destroy: destroy,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260609-hub-mobile-ai-v3" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260609-market-day-split" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -129,6 +129,9 @@
|
|||||||
<span id="mkt-symbol-label">—</span>
|
<span id="mkt-symbol-label">—</span>
|
||||||
<span id="mkt-tf-label">1d</span>
|
<span id="mkt-tf-label">1d</span>
|
||||||
<div class="market-chart-actions">
|
<div class="market-chart-actions">
|
||||||
|
<label class="market-day-split-opt" title="北京时间 8:00 交易切日竖线">
|
||||||
|
<input type="checkbox" id="market-day-split" /> 交易间隔日
|
||||||
|
</label>
|
||||||
<details class="market-ind-menu">
|
<details class="market-ind-menu">
|
||||||
<summary>技术指标</summary>
|
<summary>技术指标</summary>
|
||||||
<div class="market-ind-options">
|
<div class="market-ind-options">
|
||||||
@@ -420,8 +423,8 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart_draw.js?v=20260608-market-vol-rank"></script>
|
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
|
||||||
<script src="/assets/chart.js?v=20260608-market-tz8"></script>
|
<script src="/assets/chart.js?v=20260609-market-day-split"></script>
|
||||||
<script src="/assets/archive.js?v=20260608-hub-archive-history"></script>
|
<script src="/assets/archive.js?v=20260608-hub-archive-history"></script>
|
||||||
<script src="/assets/ai_review_render.js?v=2"></script>
|
<script src="/assets/ai_review_render.js?v=2"></script>
|
||||||
<script src="/assets/app.js?v=20260609-hub-mobile-ai-v3"></script>
|
<script src="/assets/app.js?v=20260609-hub-mobile-ai-v3"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user