diff --git a/web/app.py b/web/app.py index 4326039..3633a62 100644 --- a/web/app.py +++ b/web/app.py @@ -52,12 +52,18 @@ exchange = ccxt.binance({ # 时间周期映射 TIMEFRAMES = { '1m': '1分钟', + '3m': '3分钟', '5m': '5分钟', '15m': '15分钟', '30m': '30分钟', '1h': '1小时', + '2h': '2小时', '4h': '4小时', + '6h': '6小时', + '8h': '8小时', + '12h': '12小时', '1d': '日线', + '3d': '3日', '1w': '周线', '1M': '月线', } @@ -115,8 +121,10 @@ def get_crypto_kl_data(symbol, timeframe, limit=1000, start_time=None, end_time= batch_size = 1000 # 默认批次大小 if timeframe in ['1m', '3m', '5m']: batch_size = 500 # 分钟级数据减少批次大小 - elif timeframe in ['15m', '30m', '1h']: + elif timeframe in ['15m', '30m', '1h', '2h']: batch_size = 1000 + elif timeframe in ['4h', '6h', '8h', '12h']: + batch_size = 1200 # 小时级数据可以获取更多 else: batch_size = 1500 # 日线及以上可以获取更多 @@ -280,6 +288,9 @@ def add_indicators(df): df['ma250'] = (ta.MA(df, timeperiod=250)).fillna(0) df['rsi'] = ta.RSI(df, timeperiod=14) + # 计算ATR (平均真实波幅) + df['atr'] = ta.ATR(df, timeperiod=14).fillna(0) + # 计算布林带 (当前周期 - 20周期,2标准差) bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0) df['bb_upper'] = bb['upperband'].fillna(0) diff --git a/web/cn_stock_data.py b/web/cn_stock_data.py index cb916f9..02a949c 100644 --- a/web/cn_stock_data.py +++ b/web/cn_stock_data.py @@ -186,11 +186,18 @@ class ChinaStockData: """将时间周期转换为akshare的period参数""" mapping = { '1m': '1', # 1分钟 + '3m': '5', # 3分钟 (A股不支持3分钟,使用5分钟替代) '5m': '5', # 5分钟 '15m': '15', # 15分钟 '30m': '30', # 30分钟 '1h': '60', # 60分钟 + '2h': '60', # 2小时 (A股不支持2小时,使用1小时替代) + '4h': '60', # 4小时 (A股不支持4小时,使用1小时替代) + '6h': '60', # 6小时 (A股不支持6小时,使用1小时替代) + '8h': '60', # 8小时 (A股不支持8小时,使用1小时替代) + '12h': '60', # 12小时 (A股不支持12小时,使用1小时替代) '1d': 'daily', # 日线 + '3d': 'daily', # 3日 (A股不支持3日,使用日线替代) '1w': 'weekly',# 周线 '1M': 'monthly'# 月线 } @@ -235,7 +242,7 @@ class ChinaStockData: # 分钟级数据,每次获取7天 batch_days = 7 elif period == '60': - # 小时级数据,每次获取30天 + # 小时级数据(所有小时级别都映射到60分钟),每次获取30天 batch_days = 30 else: # 日线及以上,每次获取365天 @@ -587,7 +594,7 @@ class ChinaStockData: df['date'] = df['date'].dt.normalize() + pd.Timedelta(hours=15) # 对于分钟级数据,过滤非交易时间的数据 - elif timeframe in ['1m', '5m', '15m', '30m', '1h']: + elif timeframe in ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h']: # 过滤交易日 df = df[df['date'].apply(self.is_trading_day)] @@ -651,7 +658,7 @@ class ChinaStockData: return df # 对于分钟级数据,创建完整的交易时间序列 - if timeframe in ['1m', '5m', '15m', '30m', '1h']: + if timeframe in ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h']: # 获取数据的开始和结束时间 start_date = df['date'].min().date() end_date = df['date'].max().date() @@ -660,8 +667,11 @@ class ChinaStockData: complete_times = [] current_date = start_date - # 获取时间间隔(分钟) - freq_map = {'1m': 1, '5m': 5, '15m': 15, '30m': 30, '1h': 60} + # 获取时间间隔(分钟)- 注意A股新增的时间周期都映射到了已有的时间间隔 + freq_map = { + '1m': 1, '3m': 5, '5m': 5, '15m': 15, '30m': 30, + '1h': 60, '2h': 60, '4h': 60, '6h': 60, '8h': 60, '12h': 60 + } freq_minutes = freq_map.get(timeframe, 5) while current_date <= end_date: @@ -769,7 +779,7 @@ class ChinaStockData: df[col] = df[col].replace([np.nan, np.inf, -np.inf], 0) # 确保时间序列连续性(仅对分钟级数据) - if timeframe in ['1m', '5m', '15m', '30m', '1h']: + if timeframe in ['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h', '6h', '8h', '12h']: df = self.fill_trading_gaps(df, timeframe) # 最后再次检查并清理任何剩余的NaN值 diff --git a/web/templates/index.html b/web/templates/index.html index ca3a384..4faf105 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -322,9 +322,21 @@
@@ -440,9 +452,21 @@
@@ -663,12 +687,19 @@
@@ -736,6 +767,7 @@ macdLineSeries: null, signalLineSeries: null, histogramSeries: null, + atrLineSeries: null, mainBiSeries: [], mainSegSeries: [], mainZsSeries: [], @@ -1396,22 +1428,34 @@ volumeChartContainer.style.right = '0'; volumeChartContainer.style.borderTop = '1px solid #e0e0e0'; - // 如果需要显示MACD,创建MACD容器 + // 如果需要显示MACD,创建MACD和ATR容器 let macdChartContainer = null; + let atrChartContainer = null; if (showMacd) { - // 设置各图表高度 - 为三个图表分配合理比例,主图表适度增加高度 - mainChartContainer.style.height = '55%'; // 主图占55%(约385px) - volumeChartContainer.style.top = '55%'; - volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5%(约157.5px) + // 设置各图表高度 - 为四个图表分配合理比例 + mainChartContainer.style.height = '45%'; // 主图占45%(约315px) + volumeChartContainer.style.top = '45%'; + volumeChartContainer.style.height = '18.5%'; // 成交量图占18.5%(约129.5px) + // 创建MACD容器 macdChartContainer = document.createElement('div'); macdChartContainer.style.width = '100%'; - macdChartContainer.style.height = '22.5%'; // MACD图占22.5%(约157.5px) + macdChartContainer.style.height = '18.5%'; // MACD图占18.5%(约129.5px) macdChartContainer.style.position = 'absolute'; - macdChartContainer.style.top = '77.5%'; // 从77.5%位置开始 + macdChartContainer.style.top = '63.5%'; // 从63.5%位置开始 macdChartContainer.style.left = '0'; macdChartContainer.style.right = '0'; macdChartContainer.style.borderTop = '1px solid #e0e0e0'; + + // 创建ATR容器 + atrChartContainer = document.createElement('div'); + atrChartContainer.style.width = '100%'; + atrChartContainer.style.height = '18%'; // ATR图占18%(约126px) + atrChartContainer.style.position = 'absolute'; + atrChartContainer.style.top = '82%'; // 从82%位置开始 + atrChartContainer.style.left = '0'; + atrChartContainer.style.right = '0'; + atrChartContainer.style.borderTop = '1px solid #e0e0e0'; } else { // 不显示MACD时的高度 - 主图和成交量图分配 mainChartContainer.style.height = '72%'; // 主图占72%(约504px) @@ -1421,7 +1465,10 @@ container.appendChild(mainChartContainer); container.appendChild(volumeChartContainer); - if (showMacd) container.appendChild(macdChartContainer); + if (showMacd) { + container.appendChild(macdChartContainer); + container.appendChild(atrChartContainer); + } // 防止同步过程中的无限循环 let syncInProgress = false; @@ -1436,6 +1483,8 @@ chartHeight = volumeChartContainer.clientHeight; } else if (chartType === 'macd') { chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0; + } else if (chartType === 'atr') { + chartHeight = atrChartContainer ? atrChartContainer.clientHeight : 0; } else { chartHeight = mainChartContainer.clientHeight; } @@ -1585,8 +1634,29 @@ // 创建MACD图表(如果需要) let macdChart = null; + let atrChart = null; if (showMacd) { - macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false, 'macd')); + macdChart = LightweightCharts.createChart(macdChartContainer, createChartOptions(false, 'macd')); + atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr')); + + // 强制所有图表使用相同的时间刻度配置,确保时间对齐 + const timeScaleOptions = { + borderVisible: false, + timeVisible: true, + secondsVisible: false, + tickMarkMaxCharacterLength: 8, + }; + + // 应用统一的时间刻度到所有图表 + mainChart.timeScale().applyOptions(timeScaleOptions); + volumeChart.timeScale().applyOptions(timeScaleOptions); + macdChart.timeScale().applyOptions(timeScaleOptions); + atrChart.timeScale().applyOptions(timeScaleOptions); + + mainChart.timeScale().applyOptions(timeScaleOptions); + volumeChart.timeScale().applyOptions(timeScaleOptions); + macdChart.timeScale().applyOptions(timeScaleOptions); + atrChart.timeScale().applyOptions(timeScaleOptions); } // 创建蜡烛图系列并设置数据 @@ -1623,9 +1693,10 @@ // 转换成交量数据 - 始终使用主K线周期数据 let volumes = []; // 使用与K线和MACD相同的数据源选择逻辑 - const volumeDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data; + // 强制使用主周期数据源,确保与MACD、ATR图表时间完全一致 + const volumeDataSource = currentData.kline_data; // 总是使用主周期数据 - console.log('成交量数据源选择:', useElementPeriod ? '次周期' : '主周期'); + console.log('成交量数据源: 主周期(强制与MACD、ATR一致)'); console.log('成交量数据长度:', volumeDataSource.length); if (volumeDataSource && Array.isArray(volumeDataSource)) { @@ -1688,13 +1759,11 @@ const signalData = []; const histogramData = []; - // 使用与K线数据相同的数据源来确保时间对齐 - const klineDataSource = useElementPeriod ? currentData.element_kline_data : currentData.kline_data; - const macdDataSource = useElementPeriod ? - (currentData.element_macd || currentData.macd) : // 如果有次周期MACD数据则使用,否则使用主周期 - currentData.macd; // 主周期使用主周期MACD数据 + // 强制使用主周期数据源,确保与ATR图表时间完全一致 + const klineDataSource = currentData.kline_data; // 总是使用主周期数据 + const macdDataSource = currentData.macd; // 总是使用主周期MACD数据 - console.log('MACD数据源选择:', useElementPeriod ? '次周期' : '主周期'); + console.log('MACD数据源: 主周期(强制与ATR一致)'); console.log('K线数据长度:', klineDataSource.length); console.log('MACD数据:', macdDataSource); @@ -1735,6 +1804,71 @@ tvWidget.series.histogramSeries = histogramSeries; } + // 添加ATR图表 - 独立显示ATR数据 + if (showMacd && currentData.kline_data && Array.isArray(currentData.kline_data)) { + // 创建ATR线 + const atrLineSeries = atrChart.addLineSeries({ + color: '#FF9800', + lineWidth: 2, + title: 'ATR', + lastValueVisible: false, + priceLineVisible: false, + baseLineVisible: false, + crosshairMarkerVisible: false, + pointMarkersVisible: false, + }); + + // 提取ATR数据 + const atrData = []; + + // 强制使用主周期数据源,确保与MACD图表时间完全一致 + const klineDataSource = currentData.kline_data; // 总是使用主周期数据 + + console.log('ATR数据源: 主周期(强制与MACD一致)'); + console.log('ATR K线数据长度:', klineDataSource.length); + + // 创建一个隐藏的基础数据系列来保持时间轴对齐 + const baseLineSeries = atrChart.addLineSeries({ + color: 'transparent', + lineWidth: 0, + lastValueVisible: false, + priceLineVisible: false, + baseLineVisible: false, + crosshairMarkerVisible: false, + pointMarkersVisible: false, + visible: false + }); + + // 添加完整的时间数据(透明)来保持时间轴连续 + const baseData = []; + for (let i = 0; i < klineDataSource.length; i++) { + const kline = klineDataSource[i]; + const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); + baseData.push({ + time: timestamp, + value: 0.1 // 使用很小的值,不会显示 + }); + + // 只添加ATR>0的有效数据到可见系列 + if (kline.atr && kline.atr > 0) { + atrData.push({ + time: timestamp, + value: kline.atr + }); + } + } + + // 先设置基础数据保持时间轴 + baseLineSeries.setData(baseData); + // 再设置ATR可见数据 + atrLineSeries.setData(atrData); + + console.log('ATR基础数据点数:', baseData.length); + console.log('ATR可见数据点数:', atrData.length); + + tvWidget.series.atrLineSeries = atrLineSeries; + } + // 实现三图联动滚动 // 同步图表的时间范围 @@ -1748,7 +1882,8 @@ syncInProgress = true; console.log('🚀 开始同步图表,来源:', sourceChart === mainChart ? '主图' : - sourceChart === volumeChart ? '成交量图' : 'MACD图'); + sourceChart === volumeChart ? '成交量图' : + sourceChart === macdChart ? 'MACD图' : 'ATR图'); try { if (sourceChart && sourceChart.timeScale) { @@ -1787,6 +1922,16 @@ } } + // 同步ATR图 + if (showMacd && atrChart && sourceChart !== atrChart && atrChart.timeScale) { + try { + atrChart.timeScale().setVisibleLogicalRange(logicalRange); + console.log('✅ ATR图同步完成'); + } catch (e) { + console.error('❌ ATR图同步失败:', e); + } + } + // 保存当前的可见范围到全局状态 if (tvWidget && tvWidget.state) { tvWidget.state.logicalRange = logicalRange; @@ -1812,7 +1957,8 @@ let localDragStates = { main: false, volume: false, - macd: false + macd: false, + atr: false }; // 全局鼠标抬起事件(只添加一次) @@ -1828,10 +1974,15 @@ // 为每个图表添加事件监听 const addChartSyncEvents = (chartContainer, chart) => { - console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : 'MACD图'); + console.log('为图表添加同步事件监听:', + chart === mainChart ? '主图' : + chart === volumeChart ? '成交量图' : + chart === macdChart ? 'MACD图' : 'ATR图'); // 确定当前图表类型 - const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd'; + const chartType = chart === mainChart ? 'main' : + chart === volumeChart ? 'volume' : + chart === macdChart ? 'macd' : 'atr'; // 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法) chart.timeScale().subscribeVisibleTimeRangeChange(() => { @@ -1890,6 +2041,9 @@ if (showMacd && macdChart) { addChartSyncEvents(macdChartContainer, macdChart); } + if (showMacd && atrChart) { + addChartSyncEvents(atrChartContainer, atrChart); + } // 窗口大小变化时重绘图表 window.addEventListener('resize', () => { @@ -1913,6 +2067,14 @@ }); } + // 调整ATR图大小 + if (showMacd && atrChart && atrChartContainer) { + atrChart.applyOptions({ + width: atrChartContainer.clientWidth, + height: atrChartContainer.clientHeight + }); + } + // 重新同步 - 使用主图作为同步源 setTimeout(() => { if (mainChart) { @@ -3035,15 +3197,50 @@ if (showMacd && macdChart) { macdChart.timeScale().fitContent(); } + if (showMacd && atrChart) { + atrChart.timeScale().fitContent(); + } // 保存图表对象 tvWidget.mainChart = mainChart; tvWidget.volumeChart = volumeChart; tvWidget.macdChart = macdChart; + tvWidget.atrChart = atrChart; tvWidget.state.isInitialized = true; // 绑定同步事件 - bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd); + bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, mainChart, volumeChart, macdChart, atrChart, showMacd); + + // 强制同步所有图表时间范围(确保时间对齐) + setTimeout(() => { + if (mainChart && mainChart.timeScale) { + // 先让所有图表自适应内容 + mainChart.timeScale().fitContent(); + if (volumeChart) volumeChart.timeScale().fitContent(); + if (showMacd && macdChart) macdChart.timeScale().fitContent(); + if (showMacd && atrChart) atrChart.timeScale().fitContent(); + + // 等待适应完成后强制同步时间范围 + setTimeout(() => { + const mainRange = mainChart.timeScale().getVisibleLogicalRange(); + console.log('主图时间范围:', mainRange); + + if (mainRange && volumeChart && volumeChart.timeScale) { + volumeChart.timeScale().setVisibleLogicalRange(mainRange); + console.log('成交量图时间范围已同步'); + } + if (mainRange && showMacd && macdChart && macdChart.timeScale) { + macdChart.timeScale().setVisibleLogicalRange(mainRange); + console.log('MACD图时间范围已同步'); + } + if (mainRange && showMacd && atrChart && atrChart.timeScale) { + atrChart.timeScale().setVisibleLogicalRange(mainRange); + console.log('ATR图时间范围已同步'); + } + console.log('所有图表时间范围已强制同步完成'); + }, 50); + } + }, 200); // 只有在时间输入框都为空时才设置图表默认时间范围 if (!$('#start_time').val() && !$('#end_time').val()) { @@ -3051,7 +3248,7 @@ } // 添加买卖点提示 - setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, showMacd); + setupTooltip(mainChart, [], [], mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, volumeChart, macdChart, atrChart, showMacd); console.log('图表初始化完成'); } catch (e) { @@ -3128,18 +3325,9 @@ tvWidget.series.lineSeries.setData(lineData); } - // 更新成交量数据 + // 更新成交量数据 - 强制使用主周期数据源确保时间对齐 let volumes = []; - if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) { - volumes = currentData.element_kline_data.map(kline => { - const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); - return { - time: timestamp, - value: parseFloat(kline.volume), - color: parseFloat(kline.close) >= parseFloat(kline.open) ? 'rgba(220, 53, 69, 0.5)' : 'rgba(40, 167, 69, 0.5)', - }; - }); - } else if (currentData.kline_data && Array.isArray(currentData.kline_data)) { + if (currentData.kline_data && Array.isArray(currentData.kline_data)) { volumes = currentData.kline_data.map(kline => { const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); return { @@ -3191,6 +3379,26 @@ tvWidget.series.histogramSeries.setData(histogramData); } + // 更新ATR数据 - 保持时间轴对齐 + if (currentData.kline_data && Array.isArray(currentData.kline_data) && tvWidget.series.atrLineSeries) { + const atrData = []; + + for (let i = 0; i < currentData.kline_data.length; i++) { + const kline = currentData.kline_data[i]; + const timestamp = Math.floor(new Date(kline.date).getTime() / 1000); + + // 只添加ATR>0的有效数据,不显示ATR=0的点 + if (kline.atr && kline.atr > 0) { + atrData.push({ + time: timestamp, + value: kline.atr + }); + } + } + + tvWidget.series.atrLineSeries.setData(atrData); + } + // 先恢复可视范围,避免先显示默认范围再跳转的跳跃效果 if (tvWidget.mainChart && (tvWidget.state.logicalRange || tvWidget.state.visibleRange)) { console.log('回放模式:优先恢复视图范围以避免跳跃'); @@ -3206,10 +3414,12 @@ tvWidget.mainChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); + if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleLogicalRange(tvWidget.state.logicalRange); } else if (tvWidget.state.visibleRange) { tvWidget.mainChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); + if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleRange(tvWidget.state.visibleRange); } }, 20); } @@ -3231,7 +3441,7 @@ } } - function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, mainChart, volumeChart, macdChart, showMacd) { + function bindSyncEvents(mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, mainChart, volumeChart, macdChart, atrChart, showMacd) { // 防止同步过程中的无限循环 let syncInProgress = false; @@ -3239,7 +3449,8 @@ let localDragStates = { main: false, volume: false, - macd: false + macd: false, + atr: false }; // 同步图表的时间范围 @@ -3253,7 +3464,8 @@ syncInProgress = true; console.log('🚀 开始同步图表,来源:', sourceChart === mainChart ? '主图' : - sourceChart === volumeChart ? '成交量图' : 'MACD图'); + sourceChart === volumeChart ? '成交量图' : + sourceChart === macdChart ? 'MACD图' : 'ATR图'); try { if (sourceChart && sourceChart.timeScale) { @@ -3292,6 +3504,16 @@ } } + // 同步ATR图 + if (showMacd && atrChart && sourceChart !== atrChart && atrChart.timeScale) { + try { + atrChart.timeScale().setVisibleLogicalRange(logicalRange); + console.log('✅ ATR图同步完成'); + } catch (e) { + console.error('❌ ATR图同步失败:', e); + } + } + // 保存当前的可见范围到全局状态 if (tvWidget && tvWidget.state) { tvWidget.state.logicalRange = logicalRange; @@ -3315,10 +3537,15 @@ // 为每个图表添加事件监听 const addChartSyncEvents = (chartContainer, chart) => { - console.log('为图表添加同步事件监听:', chart === mainChart ? '主图' : chart === volumeChart ? '成交量图' : 'MACD图'); + console.log('为图表添加同步事件监听:', + chart === mainChart ? '主图' : + chart === volumeChart ? '成交量图' : + chart === macdChart ? 'MACD图' : 'ATR图'); // 确定当前图表类型 - const chartType = chart === mainChart ? 'main' : chart === volumeChart ? 'volume' : 'macd'; + const chartType = chart === mainChart ? 'main' : + chart === volumeChart ? 'volume' : + chart === macdChart ? 'macd' : 'atr'; // 使用LightweightCharts内置的时间范围变化事件(这是最可靠的方法) chart.timeScale().subscribeVisibleTimeRangeChange(() => { @@ -3381,6 +3608,9 @@ if (showMacd && macdChartContainer && macdChart) { addChartSyncEvents(macdChartContainer, macdChart); } + if (showMacd && atrChartContainer && atrChart) { + addChartSyncEvents(atrChartContainer, atrChart); + } // 窗口大小变化时重绘图表 window.addEventListener('resize', () => { @@ -3408,6 +3638,14 @@ }); } + // 调整ATR图大小 + if (showMacd && atrChart && atrChartContainer) { + atrChart.applyOptions({ + width: atrChartContainer.clientWidth, + height: atrChartContainer.clientHeight + }); + } + // 重新同步 - 使用主图作为同步源 setTimeout(() => { if (mainChart) { @@ -3417,7 +3655,7 @@ }); } - function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, macdChartContainer, volumeChart, macdChart, showMacd) { + function setupTooltip(mainChart, buyMarkers = [], sellMarkers = [], mainChartContainer, volumeChartContainer, macdChartContainer, atrChartContainer, volumeChart, macdChart, atrChart, showMacd) { // 调试变量 window.debugMode = true; @@ -3451,6 +3689,8 @@ existingVolumeLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); // 获取时间对应的坐标位置 const mainTimeCoordinate = mainChart.timeScale().timeToCoordinate(param.time); @@ -3495,6 +3735,26 @@ document.body.appendChild(macdLine); } } + + // 如果有ATR图,也在ATR图上绘制垂直线 + if (showMacd && atrChart && atrChartContainer) { + const atrTimeCoordinate = atrChart.timeScale().timeToCoordinate(param.time); + if (atrTimeCoordinate !== null) { + const atrChartRect = atrChartContainer.getBoundingClientRect(); + const atrLine = document.createElement('div'); + atrLine.className = 'atr-crosshair-line'; + atrLine.style.position = 'fixed'; // 改为fixed定位 + atrLine.style.left = (atrChartRect.left + atrTimeCoordinate) + 'px'; + atrLine.style.top = atrChartRect.top + 'px'; + atrLine.style.width = '1px'; + atrLine.style.height = atrChartRect.height + 'px'; + atrLine.style.backgroundColor = 'rgba(128, 128, 128, 0.5)'; + atrLine.style.borderLeft = '1px dashed rgba(128, 128, 128, 0.5)'; + atrLine.style.pointerEvents = 'none'; + atrLine.style.zIndex = '1000'; + document.body.appendChild(atrLine); + } + } } } catch (e) { console.debug('十字线同步出错:', e); @@ -3506,6 +3766,8 @@ existingVolumeLines.forEach(line => line.remove()); const existingMacdLines = document.querySelectorAll('.macd-crosshair-line'); existingMacdLines.forEach(line => line.remove()); + const existingAtrLines = document.querySelectorAll('.atr-crosshair-line'); + existingAtrLines.forEach(line => line.remove()); } catch (e) { console.debug('清除十字线时出错:', e); } @@ -4037,6 +4299,7 @@ tvWidget.mainChart = null; tvWidget.volumeChart = null; tvWidget.macdChart = null; + tvWidget.atrChart = null; // 重置系列数据 tvWidget.series = { candleSeries: null, @@ -4045,6 +4308,7 @@ macdLineSeries: null, signalLineSeries: null, histogramSeries: null, + atrLineSeries: null, mainBiSeries: [], mainSegSeries: [], mainZsSeries: [],