feat(hub): add yesterday close and high-low price line toggles on market chart

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-12 20:45:09 +08:00
parent 309eebc61d
commit ab862efc4e
2 changed files with 209 additions and 7 deletions
+202 -6
View File
@@ -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);
+7 -1
View File
@@ -132,6 +132,12 @@
<span id="mkt-symbol-label"></span>
<span id="mkt-tf-label">1d</span>
<div class="market-chart-actions">
<label class="market-day-split-opt" title="上一交易日收盘价水平线(北京切日)">
<input type="checkbox" id="market-prev-close-line" /> 昨日收盘价
</label>
<label class="market-day-split-opt" title="上一交易日最高/最低价水平线(北京切日)">
<input type="checkbox" id="market-prev-hl-lines" /> 昨日高低点
</label>
<label class="market-day-split-opt" title="北京时间 8:00 交易切日竖线">
<input type="checkbox" id="market-day-split" /> 交易间隔日
</label>
@@ -588,7 +594,7 @@
<div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart_draw.js?v=20260609-market-day-split"></script>
<script src="/assets/chart.js?v=20260609-market-day-split"></script>
<script src="/assets/chart.js?v=20260609-prev-day-lines"></script>
<script src="/assets/archive.js?v=20260612-archive-ai-chat"></script>
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>