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:
@@ -193,8 +193,13 @@
|
|||||||
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 elPrevCloseLine = document.getElementById("market-prev-close-line");
|
||||||
|
const elPrevHlLines = document.getElementById("market-prev-hl-lines");
|
||||||
const elDaySplit = document.getElementById("market-day-split");
|
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 DAY_SPLIT_STORAGE_KEY = "hub-market-day-split";
|
||||||
|
const BJ_OFFSET_SEC = 8 * 60 * 60;
|
||||||
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");
|
||||||
@@ -226,6 +231,7 @@
|
|||||||
let priceTick = null;
|
let priceTick = null;
|
||||||
let priceAutoScale = true;
|
let priceAutoScale = true;
|
||||||
let rangeMarkers = [];
|
let rangeMarkers = [];
|
||||||
|
let yesterdayPriceLines = [];
|
||||||
let positionLines = [];
|
let positionLines = [];
|
||||||
let posContext = null;
|
let posContext = null;
|
||||||
let posPnlTimer = null;
|
let posPnlTimer = null;
|
||||||
@@ -310,19 +316,129 @@
|
|||||||
syncChartWrapLayout();
|
syncChartWrapLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDaySplitPref() {
|
function loadBoolPref(key, defaultValue) {
|
||||||
try {
|
try {
|
||||||
const raw = localStorage.getItem(DAY_SPLIT_STORAGE_KEY);
|
const raw = localStorage.getItem(key);
|
||||||
if (raw === "1" || raw === "true") return true;
|
if (raw === "1" || raw === "true") return true;
|
||||||
if (raw === "0" || raw === "false") return false;
|
if (raw === "0" || raw === "false") return false;
|
||||||
} catch (_) {}
|
} 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) {
|
function saveDaySplitPref(on) {
|
||||||
try {
|
saveBoolPref(DAY_SPLIT_STORAGE_KEY, on);
|
||||||
localStorage.setItem(DAY_SPLIT_STORAGE_KEY, on ? "1" : "0");
|
}
|
||||||
} catch (_) {}
|
|
||||||
|
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) {
|
function applyTradingDaySplit(enabled) {
|
||||||
@@ -2127,6 +2243,74 @@
|
|||||||
rangeMarkers = [];
|
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) {
|
function viewKey(exKey, sym, tf) {
|
||||||
return (exKey || "") + "|" + (sym || "") + "|" + (tf || "");
|
return (exKey || "") + "|" + (sym || "") + "|" + (tf || "");
|
||||||
}
|
}
|
||||||
@@ -2199,6 +2383,7 @@
|
|||||||
applyChartRightGap();
|
applyChartRightGap();
|
||||||
restoreVisibleLogicalRange(range, lastCandles.length);
|
restoreVisibleLogicalRange(range, lastCandles.length);
|
||||||
updateVisibleRangeMarkers();
|
updateVisibleRangeMarkers();
|
||||||
|
updateYesterdayPriceLines();
|
||||||
}
|
}
|
||||||
applyOnce();
|
applyOnce();
|
||||||
requestAnimationFrame(applyOnce);
|
requestAnimationFrame(applyOnce);
|
||||||
@@ -2249,6 +2434,7 @@
|
|||||||
function clearChartSeriesData() {
|
function clearChartSeriesData() {
|
||||||
lastCandles = [];
|
lastCandles = [];
|
||||||
candleByTime = {};
|
candleByTime = {};
|
||||||
|
clearYesterdayPriceLines();
|
||||||
if (candleSeries) candleSeries.setData([]);
|
if (candleSeries) candleSeries.setData([]);
|
||||||
if (volumeSeries) volumeSeries.setData([]);
|
if (volumeSeries) volumeSeries.setData([]);
|
||||||
}
|
}
|
||||||
@@ -2315,6 +2501,7 @@
|
|||||||
} catch (indErr) {}
|
} catch (indErr) {}
|
||||||
}
|
}
|
||||||
updateVisibleRangeMarkers();
|
updateVisibleRangeMarkers();
|
||||||
|
updateYesterdayPriceLines();
|
||||||
showLatestOhlcv();
|
showLatestOhlcv();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -2349,6 +2536,7 @@
|
|||||||
applyPriceAutoScale();
|
applyPriceAutoScale();
|
||||||
}
|
}
|
||||||
updateVisibleRangeMarkers();
|
updateVisibleRangeMarkers();
|
||||||
|
updateYesterdayPriceLines();
|
||||||
try {
|
try {
|
||||||
updateIndicators();
|
updateIndicators();
|
||||||
} catch (indErr) {}
|
} catch (indErr) {}
|
||||||
@@ -3036,6 +3224,14 @@
|
|||||||
updateIndicators();
|
updateIndicators();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
if (elPrevCloseLine) {
|
||||||
|
elPrevCloseLine.checked = loadPrevCloseLinePref();
|
||||||
|
elPrevCloseLine.addEventListener("change", syncPrevDayLineUi);
|
||||||
|
}
|
||||||
|
if (elPrevHlLines) {
|
||||||
|
elPrevHlLines.checked = loadPrevHlLinesPref();
|
||||||
|
elPrevHlLines.addEventListener("change", syncPrevDayLineUi);
|
||||||
|
}
|
||||||
if (elDaySplit) {
|
if (elDaySplit) {
|
||||||
elDaySplit.checked = loadDaySplitPref();
|
elDaySplit.checked = loadDaySplitPref();
|
||||||
elDaySplit.addEventListener("change", syncTradingDaySplitUi);
|
elDaySplit.addEventListener("change", syncTradingDaySplitUi);
|
||||||
|
|||||||
@@ -132,6 +132,12 @@
|
|||||||
<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="上一交易日收盘价水平线(北京切日)">
|
||||||
|
<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 交易切日竖线">
|
<label class="market-day-split-opt" title="北京时间 8:00 交易切日竖线">
|
||||||
<input type="checkbox" id="market-day-split" /> 交易间隔日
|
<input type="checkbox" id="market-day-split" /> 交易间隔日
|
||||||
</label>
|
</label>
|
||||||
@@ -588,7 +594,7 @@
|
|||||||
<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=20260609-market-day-split"></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/archive.js?v=20260612-archive-ai-chat"></script>
|
||||||
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
|
<script src="/assets/funds.js?v=20260609-hub-funds-fold"></script>
|
||||||
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
|
<script src="/assets/dashboard.js?v=20260612-dash-monitor-count"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user