feat(web): 增量自动刷新、结构区修复与默认指标/周期
自动刷新常态只拉 recent 尾部 K,每 1 分钟全量重算缠论;修复结构区缓存导入;默认指标/4h·1h·15m/近30天;同步 ECR-009 screener 相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+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) {}
|
||||
|
||||
Reference in New Issue
Block a user