diff --git a/web/templates/index.html b/web/templates/index.html index c9662fa..7fe99ee 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -18,7 +18,7 @@ body { font-family: "Helvetica Neue", Arial, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif; margin: 0; - padding: 20px; + padding: 10px; background-color: #f8f9fa; color: #333; } @@ -26,18 +26,17 @@ max-width: 100%; margin: 0 auto; background-color: white; - padding: 20px; + padding: 5px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .header { - margin-bottom: 20px; - padding-bottom: 10px; - border-bottom: 1px solid #eee; + margin-bottom: 0px; + padding-bottom: 0px; } .controls { - margin-bottom: 20px; - padding: 20px; + margin-bottom: 10px; + padding: 10px; background-color: #f8f9fa; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); @@ -50,7 +49,7 @@ .chart-container { width: 100%; height: 600px; - margin-top: 20px; + margin-top: 10px; border: 1px solid #e9ecef; border-radius: 8px; padding: 10px; @@ -74,10 +73,10 @@ box-shadow: 0 1px 4px rgba(0,0,0,0.1); } .data-container { - margin-top: 30px; + margin-top: 10px; } .nav-tabs { - margin-bottom: 15px; + margin-bottom: 10px; } .table-container { overflow-x: auto; @@ -215,12 +214,22 @@ margin-left: 8px; vertical-align: middle; } + + /* 数据回放相关样式 */ + #replayStatus { + min-width: 120px; + } + + #replayProgress { + border-radius: 4px; + overflow: hidden; + }
-

缠论分析系统

+
缠论分析系统
@@ -266,7 +275,7 @@
-
+
@@ -286,7 +295,7 @@
- +
@@ -294,11 +303,6 @@
- -
- - -
@@ -307,30 +311,30 @@
- +
- +
- +
- +
- - + +
- +
@@ -338,24 +342,24 @@
- +
- +
- +
- - + +
-
+
+ + + + + + +
+ + + +
+ + +
@@ -3871,6 +3902,514 @@ var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { return new bootstrap.Tooltip(tooltipTriggerEl) }) + + // 数据回放相关变量 + let replayTimer = null; + let replayData = null; + let currentReplayIndex = 0; + let totalReplaySteps = 0; + let isReplaying = false; + let replayStartTime = null; + let replayEndTime = null; + + // 数据回放功能初始化 + $(document).ready(function() { + // 开始回放按钮 + $('#startReplay').click(function() { + startDataReplay(); + }); + + // 暂停回放按钮 + $('#pauseReplay').click(function() { + pauseDataReplay(); + }); + + // 停止回放按钮 + $('#stopReplay').click(function() { + stopDataReplay(); + }); + }); + + // 开始数据回放 + function startDataReplay() { + // 如果自动刷新开启,需要先关闭 + if ($('#autoRefresh').is(':checked')) { + $('#autoRefresh').prop('checked', false).change(); + } + + // 检查是否设置了时间范围 + if (!$('#start_time').val() || !$('#end_time').val()) { + alert('请先设置开始时间和结束时间'); + return; + } + + // 如果已经在回放中,且是暂停状态,则继续回放 + if (isReplaying && replayTimer === null) { + continueDataReplay(); + return; + } + + // 停止任何现有的回放 + stopDataReplay(); + + // 获取回放参数 + replayStartTime = new Date($('#start_time').val()).getTime(); + replayEndTime = new Date($('#end_time').val()).getTime(); + + // 检查时间范围是否有效 + if (replayStartTime >= replayEndTime) { + alert('开始时间必须早于结束时间'); + return; + } + + // 获取当前参数 + const symbol = $('#symbol').val() || 'SOL/USDT:USDT'; + const timeframe = $('#timeframe').val() || '5m'; + const timezone = $('#timezone').val() || 'Asia/Shanghai'; + const elementTimeframe = $('#elementTimeframe').val() || '1m'; + + // 显示回放状态 + $('#replayStatus').text('正在加载数据...'); + $('#replayProgress').show(); + $('.progress-bar').css('width', '0%'); + + // 切换按钮状态 + $('#startReplay').hide(); + $('#stopReplay').show(); + + // 尝试使用当前已有数据 + if (currentData && currentData.kline_data && currentData.kline_data.length > 0) { + const currentStart = new Date(currentData.kline_data[0].date).getTime(); + const currentEnd = new Date(currentData.kline_data[currentData.kline_data.length - 1].date).getTime(); + + // 检查当前数据是否覆盖所需的时间范围 + if (currentStart <= replayStartTime && currentEnd >= replayEndTime) { + console.log('使用当前数据进行回放,无需重新请求'); + replayData = currentData; + initReplay(); + return; + } + } + + // 请求整个时间范围的数据 + $.ajax({ + url: '/api/analyze', + data: { + symbol: symbol, + timeframe: timeframe, + timezone: timezone, + element_timeframe: elementTimeframe, + start_time: replayStartTime, + end_time: replayEndTime, + elements_only: false + }, + success: function(data) { + // 检查是否有有效数据 + if (!data || !data.kline_data || data.kline_data.length === 0) { + $('#replayStatus').text('服务器返回的数据无效'); + $('#replayProgress').hide(); + $('#startReplay').show(); + $('#stopReplay').hide(); + console.error('服务器返回的数据无效:', data); + return; + } + + // 保存完整数据 + replayData = data; + + // 初始化回放 + initReplay(); + }, + error: function(jqXHR, textStatus, errorThrown) { + $('#replayStatus').text('加载数据失败'); + $('#replayProgress').hide(); + $('#startReplay').show(); + $('#stopReplay').hide(); + alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown)); + } + }); + } + + // 初始化回放 + function initReplay() { + if (!replayData || !replayData.kline_data || replayData.kline_data.length === 0) { + $('#replayStatus').text('没有可回放的数据'); + $('#replayProgress').hide(); + $('#startReplay').show(); + $('#stopReplay').hide(); + return; + } + + // 设置回放变量 + isReplaying = true; + currentReplayIndex = 0; + totalReplaySteps = replayData.kline_data.length; + + // 更新回放状态 + $('#replayStatus').text(`准备回放 (0/${totalReplaySteps})`); + $('.progress-bar').css('width', '0%'); + + // 准备初始数据 + prepareInitialReplayData(); + + // 开始回放 + continueDataReplay(); + } + + // 准备初始回放数据 + function prepareInitialReplayData() { + // 创建初始数据集,只包含第一根K线 + const initialData = $.extend(true, {}, replayData); + + // 确保第一根K线数据有效 + if (!replayData.kline_data || replayData.kline_data.length === 0) { + console.error('无有效K线数据用于回放'); + $('#replayStatus').text('无有效数据用于回放'); + $('#replayProgress').hide(); + return; + } + + initialData.kline_data = [replayData.kline_data[0]]; + + // 确保初始K线数据的所有字段都有值 + const firstKline = initialData.kline_data[0]; + if (firstKline) { + // 确保K线数据的每个字段都是有效的数值 + ['open', 'high', 'low', 'close', 'volume'].forEach(field => { + if (firstKline[field] === null || firstKline[field] === undefined || isNaN(firstKline[field])) { + console.warn(`K线数据的${field}字段无效,设置为默认值`); + firstKline[field] = field === 'volume' ? 0 : firstKline.close || 0; + } + }); + } + + // 清空其他数据列表,因为还没有处理 + initialData.bi_list = []; + initialData.seg_list = []; + initialData.zs_list = []; + initialData.uncompleted_zs_list = []; + initialData.element_bi_list = []; + initialData.element_seg_list = []; + initialData.element_zs_list = []; + initialData.element_uncompleted_zs_list = []; + initialData.macd_divergence = []; + initialData.klc_fx_type = []; + + // 初始化MACD数据 + if (initialData.macd && initialData.macd.length > 0) { + initialData.macd = [initialData.macd[0]]; + + // 确保MACD数据有效 + const firstMacd = initialData.macd[0]; + if (firstMacd) { + ['dif', 'dea', 'macd'].forEach(field => { + if (firstMacd[field] === null || firstMacd[field] === undefined || isNaN(firstMacd[field])) { + console.warn(`MACD数据的${field}字段无效,设置为0`); + firstMacd[field] = 0; + } + }); + } + } + + // 更新图表,固定坐标轴范围 + currentData = initialData; + + // 清除当前图表 + if (tvWidget.state.isInitialized) { + // 保存当前的图表可见范围以供后续使用 + try { + tvWidget.state.visibleRange = tvWidget.mainChart.timeScale().getVisibleRange(); + tvWidget.state.logicalRange = tvWidget.mainChart.timeScale().getVisibleLogicalRange(); + } catch (e) { + console.error('保存图表范围失败', e); + } + } + + // 刷新图表 + refreshChart(initialData); + + // 固定y轴范围 + fixYAxisRange(); + } + + // 固定y轴范围,保持图表不因数据变化而变化 + function fixYAxisRange() { + if (!tvWidget.mainChart) return; + + // 获取全部数据的最高价和最低价,为了适当的范围 + let minPrice = Infinity; + let maxPrice = -Infinity; + + // 计算整个数据集的价格范围 + replayData.kline_data.forEach(kline => { + const high = parseFloat(kline.high); + const low = parseFloat(kline.low); + + if (high > maxPrice) maxPrice = high; + if (low < minPrice) minPrice = low; + }); + + // 添加一些边距(约5%) + const priceRange = maxPrice - minPrice; + maxPrice += priceRange * 0.05; + minPrice -= priceRange * 0.05; + + // 修复: 使用正确的API设置固定价格范围 + try { + // 禁用自动缩放 + tvWidget.mainChart.priceScale().applyOptions({ + autoScale: false, + scaleMargins: { + top: 0.1, + bottom: 0.2 + }, + // 直接在options中设置最小和最大值 + minimumValue: minPrice, + maximumValue: maxPrice + }); + } catch (e) { + console.error('设置Y轴范围失败:', e); + } + } + + // 继续回放 + function continueDataReplay() { + if (!isReplaying || !replayData) return; + + // 获取回放速度 + const interval = parseFloat($('#replayInterval').val()) * 1000 || 1000; + + // 切换按钮状态 + $('#startReplay').hide(); + $('#pauseReplay').show(); + $('#stopReplay').show(); + + // 开始回放定时器 + replayTimer = setInterval(replayNextStep, interval); + } + + // 回放下一步 + function replayNextStep() { + if (!isReplaying || currentReplayIndex >= totalReplaySteps) { + stopDataReplay(); + return; + } + + // 更新回放状态 + currentReplayIndex++; + updateReplayStatus(); + + // 创建截止到当前索引的数据子集 + const subsetData = createDataSubset(currentReplayIndex); + + // 更新图表 + currentData = subsetData; + refreshChart(subsetData); + + // 固定Y轴范围 + fixYAxisRange(); + + // 保持最新数据可见 + keepLatestDataVisible(); + + // 如果到达最后一步,停止回放 + if (currentReplayIndex >= totalReplaySteps) { + $('#replayStatus').text('回放完成'); + pauseDataReplay(); + } + } + + // 保持最新数据可见 + function keepLatestDataVisible() { + if (!tvWidget.mainChart) return; + + try { + // 获取当前数据的最后一个时间点 + if (currentData && currentData.kline_data && currentData.kline_data.length > 0) { + const lastBar = currentData.kline_data[currentData.kline_data.length - 1]; + const lastTime = Math.floor(new Date(lastBar.date).getTime() / 1000); + + // 计算时间范围 + const barCount = Math.min(50, currentData.kline_data.length); // 显示最近的50根K线或所有K线 + const firstVisibleIndex = Math.max(0, currentData.kline_data.length - barCount); + const firstVisibleBar = currentData.kline_data[firstVisibleIndex]; + const firstTime = Math.floor(new Date(firstVisibleBar.date).getTime() / 1000); + + // 设置时间轴范围 + tvWidget.mainChart.timeScale().setVisibleRange({ + from: firstTime, + to: lastTime + }); + } + } catch (e) { + console.error('保持最新数据可见失败:', e); + } + } + + // 创建数据子集 + function createDataSubset(endIndex) { + // 创建数据副本 + const subsetData = $.extend(true, {}, replayData); + + // 截取K线数据 + subsetData.kline_data = replayData.kline_data.slice(0, endIndex); + + // 验证K线数据 + subsetData.kline_data.forEach((kline, index) => { + ['open', 'high', 'low', 'close', 'volume'].forEach(field => { + if (kline[field] === null || kline[field] === undefined || isNaN(kline[field])) { + console.warn(`K线数据[${index}]的${field}字段无效,使用替代值`); + // 对于价格字段,尝试使用其他价格字段,或者前一个K线的对应值 + if (field !== 'volume') { + if (field === 'open' && kline.close !== null && !isNaN(kline.close)) { + kline[field] = kline.close; + } else if (index > 0) { + kline[field] = subsetData.kline_data[index-1][field] || 0; + } else { + kline[field] = 0; + } + } else { + kline[field] = 0; // 对于成交量,设为0 + } + } + }); + }); + + // 处理其他数据结构(笔、线段、中枢等) + // 我们需要根据当前K线的时间来过滤其他数据结构 + try { + const currentEndTime = new Date(subsetData.kline_data[endIndex - 1].date).getTime(); + + // 过滤笔列表 + if (subsetData.bi_list && subsetData.bi_list.length) { + subsetData.bi_list = filterDataByTime(replayData.bi_list, currentEndTime); + } + + // 过滤线段列表 + if (subsetData.seg_list && subsetData.seg_list.length) { + subsetData.seg_list = filterDataByTime(replayData.seg_list, currentEndTime); + } + + // 过滤中枢列表 + if (subsetData.zs_list && subsetData.zs_list.length) { + subsetData.zs_list = filterDataByTime(replayData.zs_list, currentEndTime); + } + + // 过滤未完成中枢列表 + if (subsetData.uncompleted_zs_list && subsetData.uncompleted_zs_list.length) { + subsetData.uncompleted_zs_list = filterDataByTime(replayData.uncompleted_zs_list, currentEndTime); + } + + // 过滤小周期数据 + if (subsetData.element_bi_list && subsetData.element_bi_list.length) { + subsetData.element_bi_list = filterDataByTime(replayData.element_bi_list, currentEndTime); + } + + if (subsetData.element_seg_list && subsetData.element_seg_list.length) { + subsetData.element_seg_list = filterDataByTime(replayData.element_seg_list, currentEndTime); + } + + if (subsetData.element_zs_list && subsetData.element_zs_list.length) { + subsetData.element_zs_list = filterDataByTime(replayData.element_zs_list, currentEndTime); + } + + if (subsetData.element_uncompleted_zs_list && subsetData.element_uncompleted_zs_list.length) { + subsetData.element_uncompleted_zs_list = filterDataByTime(replayData.element_uncompleted_zs_list, currentEndTime); + } + + // 过滤MACD + if (subsetData.macd && subsetData.macd.length) { + subsetData.macd = replayData.macd.slice(0, endIndex); + + // 验证MACD数据 + subsetData.macd.forEach((item, index) => { + ['dif', 'dea', 'macd'].forEach(field => { + if (item[field] === null || item[field] === undefined || isNaN(item[field])) { + console.warn(`MACD数据[${index}]的${field}字段无效,设置为0`); + item[field] = 0; + } + }); + }); + } + + // 过滤MACD背离 + if (subsetData.macd_divergence && subsetData.macd_divergence.length) { + subsetData.macd_divergence = filterDataByTime(replayData.macd_divergence, currentEndTime); + } + + // 过滤分型类型 + if (subsetData.klc_fx_type && subsetData.klc_fx_type.length) { + subsetData.klc_fx_type = filterDataByTime(replayData.klc_fx_type, currentEndTime, 'time'); + } + } catch (e) { + console.error('过滤数据时出错:', e); + } + + return subsetData; + } + + // 根据时间过滤数据 + function filterDataByTime(dataList, endTime, timeField = 'end_time') { + if (!dataList || !Array.isArray(dataList)) return []; + + return dataList.filter(item => { + const itemTime = new Date(item[timeField]).getTime(); + return itemTime <= endTime; + }); + } + + // 更新回放状态显示 + function updateReplayStatus() { + const percent = Math.floor((currentReplayIndex / totalReplaySteps) * 100); + $('#replayStatus').text(`回放中 (${currentReplayIndex}/${totalReplaySteps})`); + $('.progress-bar').css('width', `${percent}%`); + } + + // 暂停回放 + function pauseDataReplay() { + if (replayTimer) { + clearInterval(replayTimer); + replayTimer = null; + } + + // 切换按钮状态 + $('#pauseReplay').hide(); + $('#startReplay').show(); + + if (currentReplayIndex >= totalReplaySteps) { + $('#replayStatus').text('回放完成'); + } else { + $('#replayStatus').text(`已暂停 (${currentReplayIndex}/${totalReplaySteps})`); + } + } + + // 停止回放 + function stopDataReplay() { + // 清除定时器 + if (replayTimer) { + clearInterval(replayTimer); + replayTimer = null; + } + + // 重置回放状态 + isReplaying = false; + currentReplayIndex = 0; + replayData = null; + + // 更新UI + $('#replayStatus').text(''); + $('#replayProgress').hide(); + $('#pauseReplay').hide(); + $('#stopReplay').hide(); + $('#startReplay').show(); + + // 恢复图表自动缩放 + if (tvWidget.mainChart) { + tvWidget.mainChart.priceScale().applyOptions({ + autoScale: true + }); + } + } \ No newline at end of file