feat(web): 增量自动刷新、结构区修复与默认指标/周期
自动刷新常态只拉 recent 尾部 K,每 1 分钟全量重算缠论;修复结构区缓存导入;默认指标/4h·1h·15m/近30天;同步 ECR-009 screener 相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,11 +9,26 @@ function updateTradingViewData() {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前的可视范围
|
||||
// 优先用请求前冻结的视窗;否则现场拍(自动刷新短间隔 delta≈0,两种都稳)
|
||||
const frozen = window._preserveViewOnRefresh;
|
||||
const oldBarCount = window._preserveViewBarCount || 0;
|
||||
let savedScrollPosition = null;
|
||||
if (tvWidget.mainChart) {
|
||||
tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange();
|
||||
tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange();
|
||||
const ts = tvWidget.mainChart.timeScale();
|
||||
if (frozen) {
|
||||
tvWidget.state.visibleRange = frozen.visibleRange;
|
||||
tvWidget.state.logicalRange = frozen.logicalRange;
|
||||
savedScrollPosition = (typeof frozen.scrollPosition === 'number') ? frozen.scrollPosition : null;
|
||||
} else {
|
||||
tvWidget.state.visibleRange = ts.getVisibleRange();
|
||||
tvWidget.state.logicalRange = ts.getVisibleLogicalRange();
|
||||
try {
|
||||
savedScrollPosition = ts.scrollPosition ? ts.scrollPosition() : null;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
window._preserveViewOnRefresh = null;
|
||||
window._preserveViewBarCount = 0;
|
||||
|
||||
// 检查是否显示原始K线
|
||||
const showOriginalKline = $('#showOriginalKline').is(':checked');
|
||||
@@ -71,6 +86,9 @@ function updateTradingViewData() {
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const newBarCount = candles.length;
|
||||
const barDelta = (oldBarCount > 0 && newBarCount > 0) ? (newBarCount - oldBarCount) : 0;
|
||||
|
||||
// 更新主系列数据(根据klineType)
|
||||
const klineType = ($('#klineType').val() || (showOriginalKline ? 'candlestick' : 'line'));
|
||||
@@ -270,23 +288,54 @@ function updateTradingViewData() {
|
||||
// 更新EMA52显示
|
||||
updateEMA52Display(currentData);
|
||||
|
||||
// 恢复之前的可视范围 - 优先使用visibleRange以确保时间轴对齐
|
||||
// 与自动刷新一致:增量更新绝不碰 barSpacing(缩放本来就留在图表实例上)。
|
||||
// 一写 barSpacing,LWC 会按右边缘重锚 → 放大往右、缩小往左。
|
||||
// 这里只在 setData 之后把位置扳回刷新前的 logical / time 窗口。
|
||||
if (tvWidget.mainChart) {
|
||||
if (tvWidget.state.visibleRange) {
|
||||
console.log('🔄 恢复可见范围:', tvWidget.state.visibleRange);
|
||||
tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||||
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||||
if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||||
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||||
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange);
|
||||
} else if (tvWidget.state.logicalRange) {
|
||||
console.log('🔄 恢复逻辑范围:', tvWidget.state.logicalRange);
|
||||
tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||||
if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||||
if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||||
if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||||
if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange);
|
||||
}
|
||||
const charts = [
|
||||
tvWidget.mainChart,
|
||||
tvWidget.volumeChart,
|
||||
tvWidget.atrChart,
|
||||
tvWidget.macdChart,
|
||||
tvWidget.chanMacdChart
|
||||
].filter(Boolean);
|
||||
|
||||
const vr = tvWidget.state.visibleRange;
|
||||
const lr = tvWidget.state.logicalRange;
|
||||
const savedScroll = savedScrollPosition;
|
||||
|
||||
const applyPosition = function (tag) {
|
||||
let ok = false;
|
||||
if (lr && lr.from !== undefined && lr.to !== undefined) {
|
||||
charts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleLogicalRange({ from: lr.from, to: lr.to });
|
||||
ok = true;
|
||||
} catch (e) {}
|
||||
});
|
||||
if (ok) console.log('🔄 恢复位置 logical' + (tag || '') + ':', lr);
|
||||
}
|
||||
if (!ok && vr && vr.from !== undefined && vr.to !== undefined) {
|
||||
charts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleRange(vr);
|
||||
ok = true;
|
||||
} catch (e) {}
|
||||
});
|
||||
if (ok) console.log('🔄 恢复位置 time' + (tag || '') + ':', vr);
|
||||
}
|
||||
if (!ok && typeof savedScroll === 'number') {
|
||||
const pos = savedScroll + (barDelta || 0);
|
||||
charts.forEach(c => {
|
||||
try { c.timeScale().scrollToPosition(pos, false); } catch (e) {}
|
||||
});
|
||||
console.log('🔄 恢复位置 scroll' + (tag || '') + ':', pos);
|
||||
}
|
||||
};
|
||||
|
||||
applyPosition('');
|
||||
setTimeout(function () { applyPosition('@0'); }, 0);
|
||||
setTimeout(function () { applyPosition('@50'); }, 50);
|
||||
}
|
||||
|
||||
console.log('增量更新图表完成');
|
||||
|
||||
@@ -24,15 +24,13 @@ function chartTvFinalize(ctx) {
|
||||
var chanMacdChart = ctx.chanMacdChart;
|
||||
var createChartOptions = ctx.createChartOptions;
|
||||
// 同步所有图表的时间轴配置
|
||||
const hasPendingRestoreView = !!window._pendingRestoreView;
|
||||
const pendingView = window._pendingRestoreView;
|
||||
const syncTimeScaleSettings = () => {
|
||||
// 获取主图表的时间轴设置
|
||||
const mainTimeScale = mainChart.timeScale();
|
||||
const baseOptions = {
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
borderColor: '#ddd',
|
||||
barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10,
|
||||
rightOffset: 12,
|
||||
lockVisibleTimeRangeOnResize: true,
|
||||
// 关键:确保所有图表边缘行为完全一致
|
||||
fixLeftEdge: false,
|
||||
@@ -41,6 +39,12 @@ function chartTvFinalize(ctx) {
|
||||
ticksVisible: true,
|
||||
minimumHeight: 0,
|
||||
};
|
||||
// 有待恢复视图时不要先写 barSpacing/rightOffset(会钉右缘导致图往右偏),
|
||||
// 交给后面 setVisibleRange 一次锁定位置+缩放。
|
||||
if (!pendingView) {
|
||||
baseOptions.barSpacing = symbolConfig.type === 'a_stock' ? 6 : 10;
|
||||
baseOptions.rightOffset = 12;
|
||||
}
|
||||
|
||||
console.log('🔧 同步时间轴设置:', baseOptions);
|
||||
|
||||
@@ -59,8 +63,12 @@ function chartTvFinalize(ctx) {
|
||||
// 仅在没有待恢复视图时,设置默认可见范围
|
||||
const totalBars = candles ? candles.length : 0;
|
||||
const visibleBarsCount = 200;
|
||||
const hasPendingRestoreView = !!window._pendingRestoreView;
|
||||
if (!hasPendingRestoreView) {
|
||||
const allChartsNow = [mainChart, volumeChart, atrChart]
|
||||
.concat(showMacd && macdChart ? [macdChart] : [])
|
||||
.concat(showMacd && chanMacdChart ? [chanMacdChart] : []);
|
||||
if (hasPendingRestoreView && pendingView) {
|
||||
restoreChartViewState(allChartsNow, pendingView, { preferTime: true });
|
||||
} else {
|
||||
// 显示最近 200 根K线而非全部挤压(避免K线过多时重叠)
|
||||
if (totalBars > visibleBarsCount) {
|
||||
const rangeFrom = totalBars - visibleBarsCount;
|
||||
@@ -71,8 +79,12 @@ function chartTvFinalize(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// 立即同步其他图表到主图表的范围
|
||||
// 立即同步其他图表到主图表的范围(无 pending 时)
|
||||
setTimeout(() => {
|
||||
if (window._pendingRestoreView) {
|
||||
restoreChartViewState(allChartsNow, window._pendingRestoreView, { preferTime: true });
|
||||
return;
|
||||
}
|
||||
const logRange = mainChart.timeScale().getVisibleLogicalRange();
|
||||
if (logRange) {
|
||||
console.log('🔧 同步可见范围:', logRange);
|
||||
@@ -120,11 +132,10 @@ function chartTvFinalize(ctx) {
|
||||
}
|
||||
|
||||
const defaultMAs = [
|
||||
{ type: 'EMA', length: 13, color: '#800080', name: 'EMA13', visible: true }, // 紫色
|
||||
{ type: 'EMA', length: 26, color: '#FF8C00', name: 'EMA26', visible: true }, // 橙色
|
||||
{ type: 'EMA', length: 52, color: '#000000', name: 'EMA52', visible: false }, // 黑色
|
||||
{ type: 'EMA', length: 104, color: '#1E90FF', name: 'EMA104', visible: false }, // 蓝色
|
||||
{ type: 'EMA', length: 156, color: '#F700FF', name: 'EMA156', visible: false } // 粉色
|
||||
{ type: 'EMA', length: 26, color: '#FF8C00', name: 'EMA26', visible: false }, // 橙色
|
||||
{ type: 'EMA', length: 52, color: '#000000', name: 'EMA52', visible: true }, // 黑色 · 默认开
|
||||
{ type: 'SMA', length: 30, color: '#1E90FF', name: 'MA30', visible: true }, // 蓝色 · 默认开
|
||||
{ type: 'SMA', length: 250, color: '#800080', name: 'MA250', visible: true } // 紫色 · 默认开
|
||||
];
|
||||
|
||||
defaultMAs.forEach(ma => {
|
||||
@@ -191,9 +202,9 @@ function chartTvFinalize(ctx) {
|
||||
window._pendingRestoreView = null;
|
||||
|
||||
if (pending) {
|
||||
// 恢复刷新前的缩放和位置(优先可见范围/逻辑范围,最后回退到滚动位置)
|
||||
// 恢复刷新前的缩放和位置(时间范围优先,避免数据滑动后逻辑索引错位)
|
||||
console.log('📌 恢复图表视图:', JSON.stringify(pending));
|
||||
restoreChartViewState(allCharts, pending);
|
||||
restoreChartViewState(allCharts, pending, { preferTime: true });
|
||||
} else {
|
||||
// 无保存视图,正常同步主图到子图
|
||||
const visibleRange = mainChart.timeScale().getVisibleRange();
|
||||
|
||||
+128
-15
@@ -1,4 +1,46 @@
|
||||
/* chart_view.js — split from chart.js */
|
||||
|
||||
/** 用尾部 N 根合并进已有 K 线(同 timestamp 覆盖,更新则追加) */
|
||||
function mergeKlineTail(existing, incoming) {
|
||||
if (!Array.isArray(incoming) || !incoming.length) {
|
||||
return Array.isArray(existing) ? existing : [];
|
||||
}
|
||||
if (!Array.isArray(existing) || !existing.length) {
|
||||
return incoming.slice();
|
||||
}
|
||||
const out = existing.slice();
|
||||
const barTs = (row) => {
|
||||
if (row && row.timestamp != null && row.timestamp !== '') {
|
||||
const n = Number(row.timestamp);
|
||||
if (!Number.isNaN(n)) return n;
|
||||
}
|
||||
const t = row && row.date != null ? new Date(row.date).getTime() : NaN;
|
||||
return Number.isNaN(t) ? null : t;
|
||||
};
|
||||
for (let i = 0; i < incoming.length; i++) {
|
||||
const row = incoming[i];
|
||||
const ts = barTs(row);
|
||||
if (ts == null) continue;
|
||||
let idx = -1;
|
||||
const scanFrom = Math.max(0, out.length - 8);
|
||||
for (let j = out.length - 1; j >= scanFrom; j--) {
|
||||
if (barTs(out[j]) === ts) {
|
||||
idx = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx >= 0) {
|
||||
out[idx] = Object.assign({}, out[idx], row);
|
||||
} else {
|
||||
const lastTs = barTs(out[out.length - 1]);
|
||||
if (lastTs == null || ts > lastTs) {
|
||||
out.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function updateChart(options) {
|
||||
options = options || {};
|
||||
// 只显示旋转加载图标
|
||||
@@ -47,9 +89,79 @@ function updateChart(options) {
|
||||
if (options.fromAutoRefresh && window._analyzeXhr && window._analyzeXhr.readyState !== 4) {
|
||||
try { window._analyzeXhr.abort(); } catch (e) {}
|
||||
}
|
||||
|
||||
// 请求发出前冻结视窗(与自动刷新同一套;避免等响应时/setData 后 logical 索引漂移)
|
||||
try {
|
||||
if (tvWidget && tvWidget.mainChart) {
|
||||
window._preserveViewOnRefresh = captureChartViewState(tvWidget.mainChart);
|
||||
const prev = currentData && (
|
||||
($('#subSubPeriodKline').is(':checked') && currentData.sub_sub_kline_data) ||
|
||||
($('#elementPeriodKline').is(':checked') && currentData.element_kline_data) ||
|
||||
currentData.kline_data
|
||||
);
|
||||
window._preserveViewBarCount = Array.isArray(prev) ? prev.length : 0;
|
||||
console.log('📌 刷新前冻结视窗 bars=', window._preserveViewBarCount, window._preserveViewOnRefresh);
|
||||
}
|
||||
} catch (e) {
|
||||
window._preserveViewOnRefresh = null;
|
||||
window._preserveViewBarCount = 0;
|
||||
}
|
||||
|
||||
const requestId = ++lastRequestId;
|
||||
const chartsReady = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart);
|
||||
const hasBaseline = !!(currentData && Array.isArray(currentData.kline_data) && currentData.kline_data.length);
|
||||
// 自动刷新常态:只拉最近 2 根;fullAnalyze(约每 1 分钟)走全量 analyze 更新缠论
|
||||
const useRecentTail = !!(options.fromAutoRefresh && !options.fullAnalyze && chartsReady && hasBaseline);
|
||||
|
||||
if (useRecentTail) {
|
||||
console.log('自动刷新 → /api/klines/recent limit=2');
|
||||
window._analyzeXhr = $.ajax({
|
||||
url: '/api/klines/recent',
|
||||
data: {
|
||||
symbol: symbol,
|
||||
timeframe: timeframe,
|
||||
limit: 2,
|
||||
element_timeframe: elementTimeframe || undefined,
|
||||
sub_sub_timeframe: subSubTimeframe || undefined
|
||||
},
|
||||
success: function(partial) {
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
if (requestId !== lastRequestId) return;
|
||||
if (!partial || !Array.isArray(partial.kline_data)) {
|
||||
console.warn('recent 响应无效,回退全量 analyze');
|
||||
updateChart({ incremental: true, reason: 'recent-fallback' });
|
||||
return;
|
||||
}
|
||||
currentData.kline_data = mergeKlineTail(currentData.kline_data, partial.kline_data);
|
||||
if (Array.isArray(partial.element_kline_data)) {
|
||||
currentData.element_kline_data = mergeKlineTail(
|
||||
currentData.element_kline_data, partial.element_kline_data
|
||||
);
|
||||
if (partial.element_timeframe) {
|
||||
currentData.element_timeframe = partial.element_timeframe;
|
||||
}
|
||||
}
|
||||
if (Array.isArray(partial.sub_sub_kline_data)) {
|
||||
currentData.sub_sub_kline_data = mergeKlineTail(
|
||||
currentData.sub_sub_kline_data, partial.sub_sub_kline_data
|
||||
);
|
||||
if (partial.sub_sub_timeframe) {
|
||||
currentData.sub_sub_timeframe = partial.sub_sub_timeframe;
|
||||
}
|
||||
}
|
||||
refreshChart(currentData, { incremental: true, skipTables: true });
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
if (textStatus === 'abort') return;
|
||||
console.warn('recent 失败,回退全量 analyze:', errorThrown);
|
||||
updateChart({ incremental: true, reason: 'recent-error-fallback' });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const requestId = ++lastRequestId; // 标记本次请求
|
||||
// 手动 / 首拉:全量 analyze
|
||||
window._analyzeXhr = $.ajax({
|
||||
url: '/api/analyze',
|
||||
data: {
|
||||
@@ -81,15 +193,21 @@ function updateChart(options) {
|
||||
delete currentData.original_macd;
|
||||
}
|
||||
currentData = data;
|
||||
window._lastFullAnalyzeAt = Date.now();
|
||||
if (typeof renderWyckoffCycleSummary === 'function') {
|
||||
renderWyckoffCycleSummary();
|
||||
}
|
||||
|
||||
refreshChart(data, {
|
||||
incremental: options.incremental !== undefined
|
||||
? !!options.incremental
|
||||
: !!options.fromAutoRefresh
|
||||
});
|
||||
// 有图则增量;笔/段/中枢/结构区只在全量 init 绘制,fullAnalyze 必须重建
|
||||
const ready = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart);
|
||||
const structureZonesOn = $('#showMainStructureZone').is(':checked');
|
||||
let wantIncremental = options.incremental !== undefined
|
||||
? !!options.incremental
|
||||
: (ready || !!options.fromAutoRefresh);
|
||||
if (structureZonesOn || options.fullAnalyze) {
|
||||
wantIncremental = false;
|
||||
}
|
||||
refreshChart(data, { incremental: wantIncremental });
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
// 隐藏加载图标
|
||||
@@ -121,24 +239,21 @@ function captureChartViewState(chart) {
|
||||
}
|
||||
|
||||
function restoreChartViewState(charts, viewState) {
|
||||
// 全量重建备用:先缩放,再位置;不要在位置前写 rightOffset(会右边缘锚定)
|
||||
if (!viewState || !Array.isArray(charts) || charts.length === 0) return;
|
||||
const validCharts = charts.filter(c => c && c.timeScale);
|
||||
if (validCharts.length === 0) return;
|
||||
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
const optionsPatch = {};
|
||||
if (typeof viewState.barSpacing === 'number') optionsPatch.barSpacing = viewState.barSpacing;
|
||||
if (typeof viewState.rightOffset === 'number') optionsPatch.rightOffset = viewState.rightOffset;
|
||||
if (Object.keys(optionsPatch).length) {
|
||||
c.timeScale().applyOptions(optionsPatch);
|
||||
if (typeof viewState.barSpacing === 'number') {
|
||||
c.timeScale().applyOptions({ barSpacing: viewState.barSpacing });
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
|
||||
let restored = false;
|
||||
|
||||
// 优先按逻辑范围恢复(对新数据更稳健)
|
||||
if (viewState.logicalRange && viewState.logicalRange.from !== undefined && viewState.logicalRange.to !== undefined) {
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
@@ -148,7 +263,6 @@ function restoreChartViewState(charts, viewState) {
|
||||
});
|
||||
}
|
||||
|
||||
// 逻辑范围失败时,回退到时间可见范围
|
||||
if (!restored && viewState.visibleRange && viewState.visibleRange.from !== undefined && viewState.visibleRange.to !== undefined) {
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
@@ -158,7 +272,6 @@ function restoreChartViewState(charts, viewState) {
|
||||
});
|
||||
}
|
||||
|
||||
// 最后回退到滚动位置
|
||||
if (!restored && typeof viewState.scrollPosition === 'number') {
|
||||
validCharts.forEach(c => {
|
||||
try { c.timeScale().scrollToPosition(viewState.scrollPosition, false); } catch (e) {}
|
||||
|
||||
@@ -85,9 +85,9 @@ $(document).on('change', '#showMainBiZs', function() {
|
||||
$(document).on('change', '#showMainStructureZone', function() {
|
||||
const on = $('#showMainStructureZone').is(':checked');
|
||||
console.log('结构区切换为:', on);
|
||||
// 勾选后才向服务器请求多周期结构区数据;取消勾选仅重绘,不重复拉取
|
||||
// 勾选后才向服务器请求多周期结构区数据;结构区叠层只在全量 init 里绘制,必须 incremental:false
|
||||
if (on) {
|
||||
updateChart();
|
||||
updateChart({ incremental: false });
|
||||
} else {
|
||||
updateChartDisplay();
|
||||
}
|
||||
|
||||
+43
-14
@@ -272,10 +272,10 @@ function loadSymbols() {
|
||||
});
|
||||
}
|
||||
|
||||
// 设置默认时间范围(需覆盖威科夫 lookback;1 天在 4h/1h 上几乎检不出区间)
|
||||
// 设置默认时间范围:最近 1 个月
|
||||
function setDefaultTimeRange() {
|
||||
const now = new Date();
|
||||
const daysBack = 14;
|
||||
const daysBack = 30;
|
||||
const start = new Date(now.getTime() - (daysBack * 24 * 60 * 60 * 1000));
|
||||
|
||||
// 格式化为datetime-local输入框所需的格式 YYYY-MM-DDThh:mm
|
||||
@@ -493,6 +493,9 @@ $(document).ready(function() {
|
||||
let autoRefreshTimer = null;
|
||||
let nextRefreshTime = null;
|
||||
let autoRefreshTick = 0;
|
||||
/** 自动刷新时,缠论全量重算间隔(毫秒);时间戳见 window._lastFullAnalyzeAt */
|
||||
const AUTO_FULL_ANALYZE_MS = 60 * 1000;
|
||||
|
||||
// 初始化自动刷新功能
|
||||
function initAutoRefresh() {
|
||||
// 监听自动刷新勾选框变化
|
||||
@@ -519,10 +522,10 @@ function startAutoRefresh() {
|
||||
stopAutoRefresh();
|
||||
|
||||
// 获取刷新频率(分钟)
|
||||
const interval = parseFloat($('#refreshInterval').val()) || 5;
|
||||
const interval = parseFloat($('#refreshInterval').val()) || (5 / 60);
|
||||
const intervalMs = interval * 60 * 1000;
|
||||
|
||||
console.log(`开始自动刷新,频率: ${interval}分钟 (${intervalMs}毫秒)`);
|
||||
console.log(`开始自动刷新,频率: ${interval}分钟 (${intervalMs}毫秒);缠论全量每 ${AUTO_FULL_ANALYZE_MS / 1000}s`);
|
||||
|
||||
// 计算下次刷新时间
|
||||
nextRefreshTime = new Date(Date.now() + intervalMs);
|
||||
@@ -531,16 +534,36 @@ function startAutoRefresh() {
|
||||
// 启动定时器
|
||||
autoRefreshTick = 0;
|
||||
autoRefreshTimer = setInterval(function() {
|
||||
// 更新结束时间为当前时间
|
||||
// 刷新前先钉住当前缩放/位置(updateEndTime / 请求返回前都可能被改写)
|
||||
if (tvWidget && tvWidget.mainChart && typeof captureChartViewState === 'function') {
|
||||
try {
|
||||
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
|
||||
} catch (e) {
|
||||
window._pendingRestoreView = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新结束时间显示(仅 UI)
|
||||
updateEndTimeToNow();
|
||||
|
||||
// 多数周期增量更新;每隔若干次全量重建以刷新笔/段/中枢(dispose 已防泄漏)
|
||||
autoRefreshTick += 1;
|
||||
const fullRebuild = (autoRefreshTick % 6) === 0;
|
||||
updateChart({
|
||||
fromAutoRefresh: true,
|
||||
incremental: !fullRebuild
|
||||
});
|
||||
const now = Date.now();
|
||||
const lastFull = window._lastFullAnalyzeAt || 0;
|
||||
const needFullAnalyze = !lastFull || (now - lastFull >= AUTO_FULL_ANALYZE_MS);
|
||||
// 常态:/api/klines/recent 合并尾部 K;满 1 分钟:全量 /api/analyze 刷新缠论
|
||||
if (needFullAnalyze) {
|
||||
console.log('自动刷新 → 全量缠论 analyze(距上次', lastFull ? Math.round((now - lastFull) / 1000) + 's' : '首次', ')');
|
||||
updateChart({
|
||||
fromAutoRefresh: true,
|
||||
fullAnalyze: true,
|
||||
incremental: true
|
||||
});
|
||||
} else {
|
||||
updateChart({
|
||||
fromAutoRefresh: true,
|
||||
incremental: true
|
||||
});
|
||||
}
|
||||
|
||||
// 更新下次刷新时间
|
||||
nextRefreshTime = new Date(Date.now() + intervalMs);
|
||||
@@ -784,7 +807,8 @@ function refreshChart(data, options) {
|
||||
// 自动刷新:增量更新,避免每次销毁/重建 Lightweight Charts
|
||||
if (preferIncremental && chartsReady) {
|
||||
try {
|
||||
if (tvWidget.mainChart) {
|
||||
// 若定时器已捕获则保留;否则此刻再捕获一次
|
||||
if (!window._pendingRestoreView && tvWidget.mainChart) {
|
||||
try {
|
||||
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
|
||||
} catch (e) {
|
||||
@@ -792,7 +816,10 @@ function refreshChart(data, options) {
|
||||
}
|
||||
}
|
||||
updateTradingViewData();
|
||||
updateTables(data);
|
||||
// recent-tail 刷新结构未变,跳过表格重绘以提速
|
||||
if (!options.skipTables) {
|
||||
updateTables(data);
|
||||
}
|
||||
if (currentData && currentData.ema52_dict) {
|
||||
updateEMA52Display(currentData);
|
||||
}
|
||||
@@ -804,7 +831,7 @@ function refreshChart(data, options) {
|
||||
|
||||
// 保存当前缩放(barSpacing)和滚动位置(scrollPosition)到 window
|
||||
// tvWidget 会在 initTradingView 内被重建,所以必须存到 window 上
|
||||
if (tvWidget && tvWidget.mainChart) {
|
||||
if (!window._pendingRestoreView && tvWidget && tvWidget.mainChart) {
|
||||
try {
|
||||
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
|
||||
console.log('📌 保存图表视图:', JSON.stringify(window._pendingRestoreView));
|
||||
@@ -812,6 +839,8 @@ function refreshChart(data, options) {
|
||||
console.warn('保存图表视图失败:', e);
|
||||
window._pendingRestoreView = null;
|
||||
}
|
||||
} else if (window._pendingRestoreView) {
|
||||
console.log('📌 使用已保存图表视图:', JSON.stringify(window._pendingRestoreView));
|
||||
}
|
||||
|
||||
initTradingView($('#symbol').val(), $('#timeframe').val());
|
||||
|
||||
Reference in New Issue
Block a user