feat(hub): background chart poll with SSE for positions and market watch
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -533,7 +533,10 @@
|
||||
if (page === "market" && window.hubMarketChart) {
|
||||
window.hubMarketChart.init();
|
||||
} else if (window.hubMarketChart) {
|
||||
if (window.hubMarketChart.stopAutoRefresh) window.hubMarketChart.stopAutoRefresh();
|
||||
if (window.hubMarketChart.stopChartLive) window.hubMarketChart.stopChartLive();
|
||||
else {
|
||||
if (window.hubMarketChart.stopAutoRefresh) window.hubMarketChart.stopAutoRefresh();
|
||||
}
|
||||
if (window.hubMarketChart.stopPriceTagTimer) window.hubMarketChart.stopPriceTagTimer();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
/**
|
||||
* 中控行情区:K 线 + 成交量;默认最新 OHLCV;5s 自动刷新;价格轴「自动」。
|
||||
* 中控行情区:K 线 + 成交量;Hub 后台轮询 + SSE 推送;价格轴「自动」。
|
||||
*/
|
||||
(function () {
|
||||
const AUTO_REFRESH_MS = 5000;
|
||||
const CHART_WATCH_HEARTBEAT_MS = 25000;
|
||||
const CHART_SSE_FALLBACK_MS = 30000;
|
||||
const DEFAULT_VISIBLE_BARS = 200;
|
||||
const RIGHT_OFFSET_BARS = 10;
|
||||
const CANDLE_SCALE_BOTTOM = 0.26;
|
||||
@@ -128,6 +130,11 @@
|
||||
let loadToken = 0;
|
||||
let marketInited = false;
|
||||
let refreshTimer = null;
|
||||
let chartWatchTimer = null;
|
||||
let chartEventSource = null;
|
||||
let chartSseReconnectTimer = null;
|
||||
let localChartVersion = 0;
|
||||
let localSeriesVersion = 0;
|
||||
let lastViewKey = "";
|
||||
let currentTf = "1d";
|
||||
let priceTagTimer = null;
|
||||
@@ -1529,18 +1536,117 @@
|
||||
if (elTf && !elTf.value) elTf.value = "1d";
|
||||
}
|
||||
|
||||
function currentViewSeriesKey() {
|
||||
const exKey = (elExchange && elExchange.value) || "";
|
||||
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
|
||||
const tf = (elTf && elTf.value) || "1d";
|
||||
if (!exKey || !sym) return "";
|
||||
return exKey + "|" + sym + "|" + tf;
|
||||
}
|
||||
|
||||
function postChartWatch() {
|
||||
const exKey = (elExchange && elExchange.value) || "";
|
||||
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
|
||||
const tf = (elTf && elTf.value) || "1d";
|
||||
if (!exKey || !sym) return Promise.resolve();
|
||||
return fetch("/api/chart/watch", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ exchange_key: exKey, symbol: sym, timeframe: tf }),
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function postChartUnwatch() {
|
||||
const exKey = (elExchange && elExchange.value) || "";
|
||||
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
|
||||
const tf = (elTf && elTf.value) || "1d";
|
||||
if (!exKey || !sym) return Promise.resolve();
|
||||
return fetch("/api/chart/unwatch", {
|
||||
method: "POST",
|
||||
credentials: "same-origin",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ exchange_key: exKey, symbol: sym, timeframe: tf }),
|
||||
}).catch(function () {});
|
||||
}
|
||||
|
||||
function closeChartStream() {
|
||||
if (chartEventSource) {
|
||||
chartEventSource.close();
|
||||
chartEventSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
function connectChartStream() {
|
||||
closeChartStream();
|
||||
const page = document.getElementById("page-market");
|
||||
if (!page || page.classList.contains("hidden")) return;
|
||||
chartEventSource = new EventSource("/api/chart/stream");
|
||||
chartEventSource.addEventListener("chart", function (ev) {
|
||||
try {
|
||||
const st = JSON.parse(ev.data || "{}");
|
||||
const ver = Number(st.chart_version) || 0;
|
||||
const series = st.series || {};
|
||||
const vKey = currentViewSeriesKey();
|
||||
const sVer = vKey && series[vKey] ? Number(series[vKey].series_version) || 0 : 0;
|
||||
const seriesChanged = vKey && sVer > 0 && sVer !== localSeriesVersion;
|
||||
if (seriesChanged) {
|
||||
localSeriesVersion = sVer;
|
||||
localChartVersion = ver;
|
||||
loadChart(false, { autoTick: true });
|
||||
} else if (ver !== localChartVersion) {
|
||||
localChartVersion = ver;
|
||||
}
|
||||
} catch (_) {}
|
||||
});
|
||||
chartEventSource.onerror = function () {
|
||||
closeChartStream();
|
||||
if (chartSseReconnectTimer) clearTimeout(chartSseReconnectTimer);
|
||||
chartSseReconnectTimer = setTimeout(function () {
|
||||
const p = document.getElementById("page-market");
|
||||
if (p && !p.classList.contains("hidden")) connectChartStream();
|
||||
}, 8000);
|
||||
};
|
||||
}
|
||||
|
||||
function startChartWatchHeartbeat() {
|
||||
stopChartWatchHeartbeat();
|
||||
void postChartWatch();
|
||||
chartWatchTimer = setInterval(function () {
|
||||
const page = document.getElementById("page-market");
|
||||
if (!page || page.classList.contains("hidden")) return;
|
||||
void postChartWatch();
|
||||
}, CHART_WATCH_HEARTBEAT_MS);
|
||||
}
|
||||
|
||||
function stopChartWatchHeartbeat() {
|
||||
if (chartWatchTimer) clearInterval(chartWatchTimer);
|
||||
chartWatchTimer = null;
|
||||
}
|
||||
|
||||
function startAutoRefresh() {
|
||||
stopAutoRefresh();
|
||||
refreshTimer = setInterval(function () {
|
||||
const page = document.getElementById("page-market");
|
||||
if (!page || page.classList.contains("hidden")) return;
|
||||
loadChart(false, { autoTick: true });
|
||||
}, AUTO_REFRESH_MS);
|
||||
}, CHART_SSE_FALLBACK_MS);
|
||||
}
|
||||
|
||||
function stopAutoRefresh() {
|
||||
if (refreshTimer) clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
if (chartSseReconnectTimer) {
|
||||
clearTimeout(chartSseReconnectTimer);
|
||||
chartSseReconnectTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
function stopChartLive() {
|
||||
stopAutoRefresh();
|
||||
stopChartWatchHeartbeat();
|
||||
closeChartStream();
|
||||
void postChartUnwatch();
|
||||
}
|
||||
|
||||
async function loadMeta() {
|
||||
@@ -1563,6 +1669,10 @@
|
||||
async function loadChart(force, options) {
|
||||
options = options || {};
|
||||
const autoTick = !!options.autoTick;
|
||||
if (!autoTick) {
|
||||
localSeriesVersion = 0;
|
||||
void postChartWatch();
|
||||
}
|
||||
if (!ensureChart()) return;
|
||||
const exKey = (elExchange && elExchange.value) || "";
|
||||
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
|
||||
@@ -1645,7 +1755,9 @@
|
||||
(data.from_cache || 0) +
|
||||
" / 新拉 " +
|
||||
(data.fetched || 0) +
|
||||
")· 每 5s 刷新";
|
||||
")· 后台 " +
|
||||
(data.chart_poll_interval_sec || 5) +
|
||||
"s 轮询 · SSE";
|
||||
if (data.stale && data.stale_message) {
|
||||
hint += " · 缓存:" + data.stale_message;
|
||||
}
|
||||
@@ -1654,6 +1766,8 @@
|
||||
elStatus.textContent = hint;
|
||||
}
|
||||
if (elUpdated) elUpdated.textContent = "数据 " + (data.updated_at || "--");
|
||||
if (data.series_version != null) localSeriesVersion = Number(data.series_version) || localSeriesVersion;
|
||||
if (data.chart_version != null) localChartVersion = Number(data.chart_version) || localChartVersion;
|
||||
tickLiveClock();
|
||||
} catch (e) {
|
||||
if (myToken !== loadToken) return;
|
||||
@@ -1778,6 +1892,8 @@
|
||||
readQuery();
|
||||
}
|
||||
focusMarketChartArea();
|
||||
connectChartStream();
|
||||
startChartWatchHeartbeat();
|
||||
startAutoRefresh();
|
||||
await loadChart(false);
|
||||
startPriceTagTimer();
|
||||
@@ -1790,7 +1906,10 @@
|
||||
if (elSymbol && sym) elSymbol.value = String(sym).trim().toUpperCase();
|
||||
if (tf && elTf) elTf.value = tf;
|
||||
lastViewKey = "";
|
||||
localSeriesVersion = 0;
|
||||
updateExchangeDisplay();
|
||||
connectChartStream();
|
||||
startChartWatchHeartbeat();
|
||||
startAutoRefresh();
|
||||
await loadChart(false);
|
||||
startPriceTagTimer();
|
||||
@@ -1800,6 +1919,7 @@
|
||||
},
|
||||
startAutoRefresh: startAutoRefresh,
|
||||
stopAutoRefresh: stopAutoRefresh,
|
||||
stopChartLive: stopChartLive,
|
||||
stopPriceTagTimer: stopPriceTagTimer,
|
||||
};
|
||||
|
||||
|
||||
@@ -248,7 +248,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.js?v=20260604-hub-theme4"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-theme4"></script>
|
||||
<script src="/assets/chart.js?v=20260604-hub-chart-sse"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-chart-sse"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user