行情区:60s 刷新、横向 OHLCV、交易所标识、价格自动按钮
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
/**
|
||||
* 中控行情区:K 线 + 底部成交量;十字线时显示 OHLCV;可视区间高低点。
|
||||
* 中控行情区:K 线 + 成交量;默认最新 OHLCV;60s 自动刷新;价格轴「自动」。
|
||||
*/
|
||||
(function () {
|
||||
const AUTO_REFRESH_MS = 60000;
|
||||
const chartHost = document.getElementById("market-chart");
|
||||
if (!chartHost) return;
|
||||
|
||||
@@ -11,25 +12,29 @@
|
||||
const elRefresh = document.getElementById("market-refresh");
|
||||
const elStatus = document.getElementById("market-status");
|
||||
const elUpdated = document.getElementById("market-updated");
|
||||
const elOverlay = document.querySelector(".market-ohlcv-overlay");
|
||||
const elO = document.getElementById("mkt-o");
|
||||
const elH = document.getElementById("mkt-h");
|
||||
const elL = document.getElementById("mkt-l");
|
||||
const elC = document.getElementById("mkt-c");
|
||||
const elV = document.getElementById("mkt-v");
|
||||
const elExLabel = document.getElementById("mkt-exchange-label");
|
||||
const elExBadge = document.getElementById("market-exchange-badge");
|
||||
const elSymLabel = document.getElementById("mkt-symbol-label");
|
||||
const elTfLabel = document.getElementById("mkt-tf-label");
|
||||
const elPriceAuto = document.getElementById("market-price-auto");
|
||||
|
||||
let chart = null;
|
||||
let candleSeries = null;
|
||||
let volumeSeries = null;
|
||||
let priceTick = null;
|
||||
let priceAutoScale = true;
|
||||
let rangeMarkers = [];
|
||||
let lastCandles = [];
|
||||
let candleByTime = {};
|
||||
let chartMeta = null;
|
||||
let loadToken = 0;
|
||||
let marketInited = false;
|
||||
let refreshTimer = null;
|
||||
|
||||
function fmtVol(v) {
|
||||
if (v == null || Number.isNaN(Number(v))) return "-";
|
||||
@@ -62,9 +67,26 @@
|
||||
return text;
|
||||
}
|
||||
|
||||
function setOverlayVisible(on) {
|
||||
if (!elOverlay) return;
|
||||
elOverlay.classList.toggle("is-active", !!on);
|
||||
function exchangeLabel() {
|
||||
if (!elExchange) return "";
|
||||
const opt = elExchange.options[elExchange.selectedIndex];
|
||||
if (opt && opt.textContent) return opt.textContent.trim();
|
||||
return (elExchange.value || "").trim().toUpperCase();
|
||||
}
|
||||
|
||||
function updateExchangeDisplay() {
|
||||
const label = exchangeLabel();
|
||||
if (elExLabel) elExLabel.textContent = label;
|
||||
if (elExBadge) {
|
||||
elExBadge.textContent = label;
|
||||
elExBadge.setAttribute("aria-hidden", label ? "false" : "true");
|
||||
}
|
||||
}
|
||||
|
||||
function updateHeaderLabels(sym, tf) {
|
||||
if (elSymLabel) elSymLabel.textContent = sym || "—";
|
||||
if (elTfLabel) elTfLabel.textContent = tf || "—";
|
||||
updateExchangeDisplay();
|
||||
}
|
||||
|
||||
function paintOhlcv(bar) {
|
||||
@@ -82,9 +104,18 @@
|
||||
if (elV) elV.textContent = fmtVol(bar.volume);
|
||||
}
|
||||
|
||||
function hideOhlcvOverlay() {
|
||||
setOverlayVisible(false);
|
||||
paintOhlcv(null);
|
||||
function latestCandle() {
|
||||
return lastCandles.length ? lastCandles[lastCandles.length - 1] : null;
|
||||
}
|
||||
|
||||
function showLatestOhlcv() {
|
||||
paintOhlcv(latestCandle());
|
||||
}
|
||||
|
||||
function applyPriceAutoScale() {
|
||||
if (!chart) return;
|
||||
chart.priceScale("right").applyOptions({ autoScale: priceAutoScale });
|
||||
if (elPriceAuto) elPriceAuto.classList.toggle("is-on", priceAutoScale);
|
||||
}
|
||||
|
||||
function indexCandles(candles) {
|
||||
@@ -125,7 +156,7 @@
|
||||
vertLines: { visible: false },
|
||||
horzLines: { visible: false },
|
||||
},
|
||||
rightPriceScale: { borderColor: "#2a4058" },
|
||||
rightPriceScale: { borderColor: "#2a4058", autoScale: true },
|
||||
timeScale: { borderColor: "#2a4058", timeVisible: true, secondsVisible: false },
|
||||
crosshair: {
|
||||
mode: LightweightCharts.CrosshairMode
|
||||
@@ -175,18 +206,18 @@
|
||||
chart.priceScale("volume").applyOptions({
|
||||
scaleMargins: { top: 0.78, bottom: 0 },
|
||||
});
|
||||
applyPriceAutoScale();
|
||||
|
||||
chart.subscribeCrosshairMove(function (param) {
|
||||
if (!param || param.time == null) {
|
||||
hideOhlcvOverlay();
|
||||
showLatestOhlcv();
|
||||
return;
|
||||
}
|
||||
const bar = candleAtTime(param.time);
|
||||
if (!bar) {
|
||||
hideOhlcvOverlay();
|
||||
showLatestOhlcv();
|
||||
return;
|
||||
}
|
||||
setOverlayVisible(true);
|
||||
paintOhlcv(bar);
|
||||
});
|
||||
|
||||
@@ -199,7 +230,6 @@
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
});
|
||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||
hideOhlcvOverlay();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -240,7 +270,7 @@
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
axisLabelVisible: true,
|
||||
title: "可视高",
|
||||
title: "高点",
|
||||
})
|
||||
);
|
||||
rangeMarkers.push(
|
||||
@@ -250,7 +280,7 @@
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
axisLabelVisible: true,
|
||||
title: "可视低",
|
||||
title: "低点",
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -270,6 +300,20 @@
|
||||
if (elTf && !elTf.value) elTf.value = "1d";
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh();
|
||||
refreshTimer = setInterval(function () {
|
||||
const page = document.getElementById("page-market");
|
||||
if (!page || page.classList.contains("hidden")) return;
|
||||
loadChart(false);
|
||||
}, AUTO_REFRESH_MS);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
|
||||
async function loadMeta() {
|
||||
const r = await fetch("/api/chart/meta", { credentials: "same-origin" });
|
||||
chartMeta = await r.json();
|
||||
@@ -283,6 +327,7 @@
|
||||
});
|
||||
readQuery();
|
||||
applyDefaults();
|
||||
updateExchangeDisplay();
|
||||
}
|
||||
|
||||
async function loadChart(force) {
|
||||
@@ -302,9 +347,7 @@
|
||||
elStatus.className = "market-status";
|
||||
elStatus.textContent = "加载中…";
|
||||
}
|
||||
hideOhlcvOverlay();
|
||||
if (elSymLabel) elSymLabel.textContent = sym;
|
||||
if (elTfLabel) elTfLabel.textContent = tf;
|
||||
updateHeaderLabels(sym, tf);
|
||||
|
||||
const qs = new URLSearchParams({
|
||||
exchange_key: exKey,
|
||||
@@ -330,7 +373,9 @@
|
||||
candleSeries.setData(lastCandles);
|
||||
volumeSeries.setData(buildVolumeData(lastCandles));
|
||||
chart.timeScale().fitContent();
|
||||
applyPriceAutoScale();
|
||||
updateVisibleRangeMarkers();
|
||||
showLatestOhlcv();
|
||||
|
||||
const limit = data.limit || lastCandles.length;
|
||||
let hint =
|
||||
@@ -342,9 +387,7 @@
|
||||
(data.from_cache || 0) +
|
||||
" / 新拉 " +
|
||||
(data.fetched || 0) +
|
||||
")· 保留 " +
|
||||
(data.retention_days || 15) +
|
||||
" 天";
|
||||
")· 每 60s 刷新";
|
||||
if (data.stale && data.stale_message) {
|
||||
hint += " · 缓存:" + data.stale_message;
|
||||
}
|
||||
@@ -375,6 +418,7 @@
|
||||
}
|
||||
if (elExchange) {
|
||||
elExchange.addEventListener("change", function () {
|
||||
updateExchangeDisplay();
|
||||
loadChart(false);
|
||||
});
|
||||
}
|
||||
@@ -389,6 +433,12 @@
|
||||
loadChart(false);
|
||||
});
|
||||
}
|
||||
if (elPriceAuto) {
|
||||
elPriceAuto.addEventListener("click", function () {
|
||||
priceAutoScale = !priceAutoScale;
|
||||
applyPriceAutoScale();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.hubMarketChart = {
|
||||
@@ -398,11 +448,14 @@
|
||||
await loadMeta();
|
||||
bind();
|
||||
}
|
||||
startAutoRefresh();
|
||||
await loadChart(false);
|
||||
},
|
||||
reload: function (force) {
|
||||
loadChart(!!force);
|
||||
},
|
||||
startAutoRefresh: startAutoRefresh,
|
||||
stopAutoRefresh: stopAutoRefresh,
|
||||
};
|
||||
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user