Use Sina-only market K-lines and editable admin login synced to .env.
Market page uses Sina for quotes and bars with an auto-follow toggle and incremental chart updates while panning. Settings lets users change username and password, persisting to the database and .env. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+102
-21
@@ -20,9 +20,11 @@
|
||||
var streamActive = false;
|
||||
var reconnectTimer = null;
|
||||
var lastData = null;
|
||||
var lastRenderedPrepared = null;
|
||||
var lastPrevClose = null;
|
||||
var chartOpts = { prevClose: false, ma: false, gapDay: false };
|
||||
var followingLatest = true;
|
||||
var autoFollow = true;
|
||||
var DEFAULT_VISIBLE_BARS = 80;
|
||||
|
||||
var PERIOD_SECONDS = {
|
||||
@@ -163,6 +165,7 @@
|
||||
ma55Series = null;
|
||||
prevCloseLine = null;
|
||||
currentChartMode = '';
|
||||
lastRenderedPrepared = null;
|
||||
}
|
||||
|
||||
function buildChart(mode) {
|
||||
@@ -313,20 +316,58 @@
|
||||
});
|
||||
}
|
||||
|
||||
function renderChart(data, preserveRange) {
|
||||
if (!chartEl || !window.LightweightCharts) return;
|
||||
lastData = data;
|
||||
if (data.prev_close != null) lastPrevClose = data.prev_close;
|
||||
function shouldPreserveView() {
|
||||
return !autoFollow || !followingLatest;
|
||||
}
|
||||
|
||||
var isLine = data.chart_type === 'line' || data.period === 'timeshare';
|
||||
var mode = isLine ? 'line' : 'candle';
|
||||
if (!chart || currentChartMode !== mode) buildChart(mode);
|
||||
if (!chart) return;
|
||||
function applyBarUpdate(bar, mode, prepared) {
|
||||
if (mode === 'line') {
|
||||
areaSeries.update({ time: bar.time, value: bar.close });
|
||||
return;
|
||||
}
|
||||
candleSeries.update({
|
||||
time: bar.time,
|
||||
open: bar.open,
|
||||
high: bar.high,
|
||||
low: bar.low,
|
||||
close: bar.close,
|
||||
});
|
||||
var up = bar.close >= bar.open;
|
||||
var c = themeColors();
|
||||
volumeSeries.update({
|
||||
time: bar.time,
|
||||
value: bar.volume,
|
||||
color: up ? c.up : c.down,
|
||||
});
|
||||
if (chartOpts.ma && ma21Series && ma55Series && prepared && prepared.length) {
|
||||
var ma21 = calcMA(21, prepared);
|
||||
var ma55 = calcMA(55, prepared);
|
||||
if (ma21.length) ma21Series.update(ma21[ma21.length - 1]);
|
||||
if (ma55.length) ma55Series.update(ma55[ma55.length - 1]);
|
||||
}
|
||||
}
|
||||
|
||||
var prepared = prepareBars(data.bars || [], data.period || currentPeriod);
|
||||
data.preparedBars = prepared;
|
||||
if (!prepared.length) return;
|
||||
function tryIncrementalUpdate(prepared, mode) {
|
||||
if (!lastRenderedPrepared || !prepared.length) return false;
|
||||
var prev = lastRenderedPrepared;
|
||||
var prevLast = prev[prev.length - 1];
|
||||
var newLast = prepared[prepared.length - 1];
|
||||
|
||||
if (prepared.length === prev.length && newLast.time === prevLast.time) {
|
||||
applyBarUpdate(newLast, mode, prepared);
|
||||
return true;
|
||||
}
|
||||
if (prepared.length === prev.length + 1 && prepared[prepared.length - 2].time === prevLast.time) {
|
||||
applyBarUpdate(newLast, mode, prepared);
|
||||
if (autoFollow && followingLatest) {
|
||||
setVisibleRange(prepared, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function renderChartFull(prepared, data, mode, preserveRange) {
|
||||
if (mode === 'line') {
|
||||
areaSeries.setData(prepared.map(function (b) {
|
||||
return { time: b.time, value: b.close };
|
||||
@@ -350,8 +391,33 @@
|
||||
}
|
||||
applyPrevCloseLine(lastPrevClose != null ? lastPrevClose : data.prev_close);
|
||||
}
|
||||
if (!shouldPreserveView()) {
|
||||
setVisibleRange(prepared, !!preserveRange);
|
||||
}
|
||||
}
|
||||
|
||||
setVisibleRange(prepared, !!preserveRange);
|
||||
function renderChart(data, options) {
|
||||
options = options || {};
|
||||
if (!chartEl || !window.LightweightCharts) return;
|
||||
lastData = data;
|
||||
if (data.prev_close != null) lastPrevClose = data.prev_close;
|
||||
|
||||
var isLine = data.chart_type === 'line' || data.period === 'timeshare';
|
||||
var mode = isLine ? 'line' : 'candle';
|
||||
if (!chart || currentChartMode !== mode) buildChart(mode);
|
||||
if (!chart) return;
|
||||
|
||||
var prepared = prepareBars(data.bars || [], data.period || currentPeriod);
|
||||
data.preparedBars = prepared;
|
||||
if (!prepared.length) return;
|
||||
|
||||
if (!options.forceFull && shouldPreserveView() && tryIncrementalUpdate(prepared, mode)) {
|
||||
lastRenderedPrepared = prepared;
|
||||
return;
|
||||
}
|
||||
|
||||
renderChartFull(prepared, data, mode, options.preserveRange);
|
||||
lastRenderedPrepared = prepared;
|
||||
}
|
||||
|
||||
function periodLabel(key) {
|
||||
@@ -388,8 +454,6 @@
|
||||
}
|
||||
|
||||
function klineSourceLabel(src) {
|
||||
if (src === 'ctp') return 'CTP';
|
||||
if (src === 'ctp+remote') return '新浪+CTP';
|
||||
if (src === 'local') return '本地缓存';
|
||||
return '新浪';
|
||||
}
|
||||
@@ -414,9 +478,9 @@
|
||||
src = ' · ' + klineSourceLabel(lastData.source);
|
||||
}
|
||||
if (isTradingSession()) {
|
||||
el.textContent = 'TradingView 图表 · 交易中 SSE 推送' + src;
|
||||
el.textContent = '新浪数据 · 交易中 SSE 推送' + src;
|
||||
} else {
|
||||
el.textContent = 'TradingView 图表 · 非交易时段低频刷新' + src;
|
||||
el.textContent = '新浪数据 · 非交易时段低频刷新' + src;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -495,7 +559,7 @@
|
||||
var data = JSON.parse(e.data);
|
||||
if (!data.bars || !data.bars.length) return;
|
||||
hideEmptyOverlay();
|
||||
renderChart(data, lastData !== null);
|
||||
renderChart(data, { preserveRange: lastData !== null });
|
||||
updateQuoteMeta(data);
|
||||
if (data.prev_close != null) updatePrevCloseDisplay(data.prev_close);
|
||||
updateRefreshHint(false);
|
||||
@@ -522,7 +586,7 @@
|
||||
if (data.count) parts.push('共 ' + data.count + ' 根 · ' + periodLabel(data.period));
|
||||
if (data.source) parts.push('K线 ' + klineSourceLabel(data.source));
|
||||
if (data.quote_source) {
|
||||
parts.push('报价 ' + (data.quote_source === 'ctp' ? 'CTP' : '新浪'));
|
||||
parts.push('报价 新浪');
|
||||
}
|
||||
meta.textContent = parts.join(' · ');
|
||||
}
|
||||
@@ -580,6 +644,21 @@
|
||||
if (zoomReset) zoomReset.addEventListener('click', resetDataZoom);
|
||||
}
|
||||
|
||||
function bindAutoButton() {
|
||||
var btn = document.getElementById('market-auto-btn');
|
||||
if (!btn) return;
|
||||
btn.addEventListener('click', function () {
|
||||
autoFollow = !autoFollow;
|
||||
btn.classList.toggle('is-active', autoFollow);
|
||||
if (autoFollow) {
|
||||
followingLatest = true;
|
||||
if (lastData && lastData.preparedBars) {
|
||||
setVisibleRange(lastData.preparedBars, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindChartOptions() {
|
||||
var prevCb = document.getElementById('chart-opt-prev-close');
|
||||
var maCb = document.getElementById('chart-opt-ma');
|
||||
@@ -597,7 +676,7 @@
|
||||
chartOpts.ma = maCb.checked;
|
||||
if (lastData) {
|
||||
destroyChart();
|
||||
renderChart(lastData, false);
|
||||
renderChart(lastData, { forceFull: true });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -605,7 +684,7 @@
|
||||
gapCb.addEventListener('change', function () {
|
||||
chartOpts.gapDay = gapCb.checked;
|
||||
followingLatest = true;
|
||||
if (lastData) renderChart(lastData, false);
|
||||
if (lastData) renderChart(lastData, { forceFull: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -617,13 +696,14 @@
|
||||
}
|
||||
bindPeriodTabs();
|
||||
bindZoomButtons();
|
||||
bindAutoButton();
|
||||
bindChartOptions();
|
||||
|
||||
document.addEventListener('click', function (e) {
|
||||
if (e.target.closest('[data-theme-pick]') && lastData) {
|
||||
setTimeout(function () {
|
||||
destroyChart();
|
||||
renderChart(lastData, false);
|
||||
renderChart(lastData, { forceFull: true });
|
||||
}, 80);
|
||||
}
|
||||
});
|
||||
@@ -640,6 +720,7 @@
|
||||
input.addEventListener('symbol-selected', function () {
|
||||
lastPrevClose = null;
|
||||
lastData = null;
|
||||
lastRenderedPrepared = null;
|
||||
destroyChart();
|
||||
updatePrevCloseDisplay(null);
|
||||
loadKline(true);
|
||||
|
||||
Reference in New Issue
Block a user