/* chart_tv_shell.js — containers, charts, main price series */ function chartTvBuildShell(ctx) { var symbol = ctx.symbol; var timeframe = ctx.timeframe; // 获取当前交易对的配置 const symbolConfig = getSymbolConfig(symbol); console.log('交易对配置:', symbolConfig); // 检查数据是否存在 if (!currentData || !currentData.kline_data) { console.error('数据加载失败或不存在'); return; } // 检查使用哪一档K线数据:次次周期 / 小周期 / 主周期 const useSubSubPeriod = $('#subSubPeriodKline').is(':checked') && currentData.sub_sub_kline_data && Array.isArray(currentData.sub_sub_kline_data); const useElementPeriod = $('#elementPeriodKline').is(':checked') && currentData.element_kline_data && Array.isArray(currentData.element_kline_data); // 输出K线周期选择状态 const klinePeriodLabel = useSubSubPeriod ? '次次周期' : (useElementPeriod ? '小周期' : '主周期'); console.log('K线周期选择:', klinePeriodLabel); console.log('当前选择时区:', $('#timezone').val()); console.log('交易对类型:', symbolConfig.type); let candles = []; const klineDataSource = useSubSubPeriod ? (currentData.sub_sub_kline_data || []) : (useElementPeriod ? (currentData.element_kline_data || []) : (currentData.kline_data || [])); if (useSubSubPeriod || useElementPeriod) { if (!klineDataSource.length) { console.error(useSubSubPeriod ? '次次周期K线数据不存在或为空' : '小周期K线数据不存在或为空', klineDataSource); return; } candles = klineDataSource.map((kline) => { const date = new Date(kline.date); const timestamp = date.getTime() / 1000; return { time: timestamp, open: parseFloat(kline.open), high: parseFloat(kline.high), low: parseFloat(kline.low), close: parseFloat(kline.close), }; }); } else { if (!currentData.kline_data || !Array.isArray(currentData.kline_data)) { console.error('主周期K线数据不存在或不是数组:', currentData.kline_data); return; } candles = currentData.kline_data.map((kline) => { const date = new Date(kline.date); const timestamp = date.getTime() / 1000; return { time: timestamp, open: parseFloat(kline.open), high: parseFloat(kline.high), low: parseFloat(kline.low), close: parseFloat(kline.close), }; }); } // 根据交易对类型过滤数据(仅用于显示优化) if (symbolConfig.type === 'a_stock' && timeframe.includes('m')) { // 对于A股分钟级数据,过滤非交易时间 const originalLength = candles.length; candles = filterTradingHours(candles, symbolConfig); console.log(`A股数据过滤: ${originalLength} -> ${candles.length} 条记录`); } // 重置图表对象(容器已在 disposeTradingViewCharts 清空) tvWidget = { mainChart: null, volumeChart: null, macdChart: null, series: { candleSeries: null, lineSeries: null, volumeSeries: null, macdLineSeries: null, signalLineSeries: null, histogramSeries: null, mainBiSeries: [], mainUncompletedBiSeries: [], mainSegSeries: [], mainUncompletedSegSeries: [], mainZsSeries: [], mainUncompletedZsSeries: [], elementBiSeries: [], elementUncompletedBiSeries: [], elementSegSeries: [], elementUncompletedSegSeries: [], elementZsSeries: [], elementUncompletedZsSeries: [], subSubBiSeries: [], subSubUncompletedBiSeries: [], subSubSegSeries: [], subSubUncompletedSegSeries: [], subSubZsSeries: [], subSubUncompletedZsSeries: [], tradePointSeries: [], mainBollingerSeries: [], elementBollingerSeries: [], maSeries: [], // 添加均线系列数组 bbSeries: [], // 添加布林带系列数组 ema52Series: [] // 添加EMA52系列数组 }, state: { isInitialized: false, visibleRange: null, logicalRange: null } }; // 设置父容器样式 const container = document.getElementById('tradingview_chart'); container.style.position = 'relative'; container.style.width = '100%'; container.style.height = '100%'; // 是否显示MACD const showMacd = $('#showMacd').is(':checked'); const showOriginalKline = $('#showOriginalKline').is(':checked'); // 创建主图容器 const mainChartContainer = document.createElement('div'); mainChartContainer.style.width = '100%'; mainChartContainer.style.position = 'absolute'; mainChartContainer.style.top = '0'; mainChartContainer.style.left = '0'; mainChartContainer.style.right = '0'; // 创建成交量副图容器 const volumeChartContainer = document.createElement('div'); volumeChartContainer.style.width = '100%'; volumeChartContainer.style.position = 'absolute'; volumeChartContainer.style.left = '0'; volumeChartContainer.style.right = '0'; volumeChartContainer.style.borderTop = '1px solid #e0e0e0'; // 添加ATR图表容器 const atrChartContainer = document.createElement('div'); atrChartContainer.style.width = '100%'; atrChartContainer.style.position = 'absolute'; atrChartContainer.style.left = '0'; atrChartContainer.style.right = '0'; atrChartContainer.style.borderTop = '1px solid #e0e0e0'; // 如果需要显示MACD,创建MACD容器 let macdChartContainer = null; let chanMacdChartContainer = null; if (showMacd) { // 仅显示新的 ChanMACD 图:让其占用原 MACD+ChanMACD 的整体高度 // 新布局:主图(40%) → ChanMACD(30%) → 成交量(17.5%) → ATR(12.5%) mainChartContainer.style.height = '40%'; // 隐藏旧 MACD 容器(不创建) // 创建 ChanMACD 容器占据原 MACD+ChanMACD 高度(30%) chanMacdChartContainer = document.createElement('div'); chanMacdChartContainer.style.width = '100%'; chanMacdChartContainer.style.height = '30%'; chanMacdChartContainer.style.position = 'absolute'; chanMacdChartContainer.style.top = '40%'; chanMacdChartContainer.style.left = '0'; chanMacdChartContainer.style.right = '0'; chanMacdChartContainer.style.borderTop = '1px solid #e0e0e0'; chanMacdChartContainer.style.zIndex = '10'; // 水印:便于区分是新的 ChanMACD 子图 const chanMacdWatermark = document.createElement('div'); chanMacdWatermark.textContent = 'ChanMACD'; chanMacdWatermark.style.position = 'absolute'; chanMacdWatermark.style.top = '4px'; chanMacdWatermark.style.left = '8px'; chanMacdWatermark.style.fontSize = '11px'; chanMacdWatermark.style.color = '#888'; chanMacdWatermark.style.pointerEvents = 'none'; chanMacdChartContainer.appendChild(chanMacdWatermark); // 成交量位于 ChanMACD 之下 volumeChartContainer.style.top = '70%'; volumeChartContainer.style.height = '17.5%'; // ATR 位于最底部 atrChartContainer.style.top = '87.5%'; atrChartContainer.style.height = '12.5%'; } else { // 不显示MACD时的高度 - 主图、成交量图和ATR图分配 mainChartContainer.style.height = '55%'; // 主图占55% volumeChartContainer.style.top = '55%'; volumeChartContainer.style.height = '22.5%'; // 成交量图占22.5% atrChartContainer.style.top = '77.5%'; // ATR图从77.5%位置开始 atrChartContainer.style.height = '22.5%'; // ATR图占22.5% } container.appendChild(mainChartContainer); container.appendChild(volumeChartContainer); container.appendChild(atrChartContainer); if (showMacd) { // 只追加新的 ChanMACD 容器 container.appendChild(chanMacdChartContainer); } // 防止同步过程中的无限循环(实际同步由 bindSyncEvents 负责) // 创建统一的图表选项 const createChartOptions = (showTimeScale = true, chartType = 'main') => { // 根据图表类型确定高度 let chartHeight; if (chartType === 'main') { chartHeight = mainChartContainer.clientHeight; } else if (chartType === 'volume') { chartHeight = volumeChartContainer.clientHeight; } else if (chartType === 'atr') { chartHeight = atrChartContainer.clientHeight; } else if (chartType === 'macd') { chartHeight = macdChartContainer ? macdChartContainer.clientHeight : 0; } else if (chartType === 'chanmacd') { chartHeight = chanMacdChartContainer ? chanMacdChartContainer.clientHeight : 0; } else { chartHeight = mainChartContainer.clientHeight; } const baseOptions = { width: mainChartContainer.clientWidth, height: chartHeight, layout: { background: { color: '#ffffff' }, textColor: '#333', }, grid: { vertLines: { color: '#f0f0f0' }, horzLines: { color: '#f0f0f0' }, }, crosshair: { mode: LightweightCharts.CrosshairMode.Normal, // 添加十字线工具提示本地化配置 horzLine: { labelVisible: true, }, vertLine: { labelVisible: true, // 自定义时间格式化 labelFormatter: (time) => { const selectedTimezone = $('#timezone').val(); try { const date = new Date(time * 1000); if (symbolConfig.type === 'a_stock') { // A股使用中国时区格式 return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } else { return date.toLocaleString('zh-CN', { timeZone: selectedTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } } catch (e) { console.error('十字线时间格式化错误:', e); return new Date(time * 1000).toLocaleString(); } }, }, }, rightPriceScale: { borderColor: '#ddd', scaleMargins: { top: 0.1, bottom: 0.1, }, // 为标签留出更多空间,防止遮挡 minimumWidth: 80, }, // 添加左边距配置 leftPriceScale: { visible: false, }, // 添加本地化选项,确保所有时间显示都使用选定的时区 localization: { timeFormatter: (time) => { const selectedTimezone = $('#timezone').val(); try { const date = new Date(time * 1000); if (symbolConfig.type === 'a_stock') { // A股使用中国时区格式 return date.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } else { return date.toLocaleString('zh-CN', { timeZone: selectedTimezone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit' }); } } catch (e) { console.error('全局时间格式化错误:', e); return new Date(time * 1000).toLocaleString(); } } }, timeScale: { timeVisible: true, secondsVisible: false, visible: showTimeScale, borderColor: '#ddd', barSpacing: symbolConfig.type === 'a_stock' ? 6 : 10, // 确保所有图表使用相同的边距设置 rightOffset: 12, // 移除可能影响拖动的固定边缘设置 // fixLeftEdge: true, // fixRightEdge: true, lockVisibleTimeRangeOnResize: true, tickMarkFormatter: (time) => { const selectedTimezone = symbolConfig.type === 'a_stock' ? 'Asia/Shanghai' : $('#timezone').val(); try { // 使用完整的配置确保时区正确应用 const date = new Date(time * 1000); console.log('格式化时间:', time, '转换为:', date.toISOString(), '时区:', selectedTimezone); return date.toLocaleString('zh-CN', { timeZone: selectedTimezone, month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit', }); } catch (e) { console.error('时间格式化错误:', e); // 如果时区格式化失败,返回简单格式 return new Date(time * 1000).toLocaleString(); } }, }, }; // 根据交易对类型调整配置 return adjustChartForSymbolType(baseOptions, symbolConfig); }; // 创建主图表 const mainChart = LightweightCharts.createChart(mainChartContainer, createChartOptions(true, 'main')); // Cycle Summary 挂到主图左下角(相对 K 线主图 pane,而非整图底边) (function mountWyckoffCycleSummary() { var summaryEl = document.getElementById('wyckoffCycleSummary'); if (!summaryEl) { summaryEl = document.createElement('div'); summaryEl.id = 'wyckoffCycleSummary'; summaryEl.className = 'wyckoff-cycle-summary'; summaryEl.setAttribute('aria-live', 'polite'); } mainChartContainer.appendChild(summaryEl); if (typeof renderWyckoffCycleSummary === 'function') { try { renderWyckoffCycleSummary(); } catch (e) {} } })(); // 创建成交量图表 - 只显示底部的时间轴 const volumeChart = LightweightCharts.createChart(volumeChartContainer, createChartOptions(false, 'volume')); // 创建ATR图表 const atrChart = LightweightCharts.createChart(atrChartContainer, createChartOptions(false, 'atr')); // 创建MACD图表(如果需要):仅创建新的 ChanMACD 图 let macdChart = null; let chanMacdChart = null; if (showMacd) { chanMacdChart = LightweightCharts.createChart(chanMacdChartContainer, createChartOptions(false, 'chanmacd')); } // 创建主价格系列并设置数据(支持多种图表类型) (function(){ const klineType = ($('#klineType').val() || 'candlestick'); // 先清空旧的主系列引用 tvWidget.series.candleSeries = null; tvWidget.series.lineSeries = null; tvWidget.series.barSeries = null; tvWidget.series.areaSeries = null; tvWidget.series.baselineSeries = null; tvWidget.series.renkoSeries = null; tvWidget.series.heikinSeries = null; if (klineType === 'candlestick') { const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); series.setData(candles); tvWidget.series.candleSeries = series; } else if (klineType === 'renko') { const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); const bricks = buildRenkoFromCandles(candles); series.setData(bricks); tvWidget.series.renkoSeries = series; } else if (klineType === 'heikin') { const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); const hk = buildHeikinFromCandles(candles); series.setData(hk); tvWidget.series.heikinSeries = series; } else if (klineType === 'bar') { const series = mainChart.addBarSeries({ upColor: '#28a745', downColor: '#dc3545', thinBars: false }); series.setData(candles); tvWidget.series.barSeries = series; } else if (klineType === 'line') { const series = mainChart.addLineSeries({ color: '#2962FF', lineWidth: 2, crosshairMarkerVisible: true, lastValueVisible: true, priceLineVisible: true, }); const lineData = candles.map(c => ({ time: c.time, value: c.close })); series.setData(lineData); tvWidget.series.lineSeries = series; } else if (klineType === 'area') { const series = mainChart.addAreaSeries({ topColor: 'rgba(41, 98, 255, 0.4)', bottomColor: 'rgba(41, 98, 255, 0.0)', lineColor: '#2962FF', lineWidth: 2, }); const areaData = candles.map(c => ({ time: c.time, value: c.close })); series.setData(areaData); tvWidget.series.areaSeries = series; } else if (klineType === 'baseline') { const series = mainChart.addBaselineSeries({ baseValue: { type: 'price', price: candles.length ? candles[candles.length - 1].close : 0 }, topLineColor: '#26a69a', bottomLineColor: '#ef5350', topFillColor1: 'rgba(38, 166, 154, 0.28)', topFillColor2: 'rgba(38, 166, 154, 0.05)', bottomFillColor1: 'rgba(239, 83, 80, 0.28)', bottomFillColor2: 'rgba(239, 83, 80, 0.05)' }); const baseData = candles.map(c => ({ time: c.time, value: c.close })); series.setData(baseData); tvWidget.series.baselineSeries = series; } else if (klineType === 'klc') { // KLC显示模式 - 使用蜡烛线显示KLC数据 const series = mainChart.addCandlestickSeries({ upColor: '#28a745', downColor: '#dc3545', borderVisible: false, wickUpColor: '#28a745', wickDownColor: '#dc3545', }); // 使用KLC数据创建蜡烛图 const klcCandles = buildKLCFromAnalysis(currentData); series.setData(klcCandles); tvWidget.series.klcSeries = series; } })(); ctx.symbolConfig = symbolConfig; ctx.useSubSubPeriod = useSubSubPeriod; ctx.useElementPeriod = useElementPeriod; ctx.klinePeriodLabel = klinePeriodLabel; ctx.candles = candles; ctx.klineDataSource = klineDataSource; ctx.container = container; ctx.showMacd = showMacd; ctx.showOriginalKline = showOriginalKline; ctx.mainChartContainer = mainChartContainer; ctx.volumeChartContainer = volumeChartContainer; ctx.atrChartContainer = atrChartContainer; ctx.macdChartContainer = macdChartContainer; ctx.chanMacdChartContainer = chanMacdChartContainer; ctx.mainChart = mainChart; ctx.volumeChart = volumeChart; ctx.atrChart = atrChart; ctx.macdChart = macdChart; ctx.chanMacdChart = chanMacdChart; ctx.createChartOptions = createChartOptions; }