diff --git a/ChanKLU.py b/ChanKLU.py index 8a91bdf..2056857 100644 --- a/ChanKLU.py +++ b/ChanKLU.py @@ -22,7 +22,10 @@ class ChanKLU: self.ma250 = 0 self.rsi = 0 self.volume_ratio = 0 - + self.bbp120 = 0 + self.bbp365 = 0 + self.bb120 = 0 + self.bb365 = 0 # === 新增:K线类型 === self.kline_type = None # K线类型:大阳线、大阴线、小阳线、小阴线 @@ -575,7 +578,10 @@ class ChanKLU: self.rsi = float(item['rsi']) if 'rsi' in item and item['rsi'] else 0 self.volume_ratio = float(item['volume_ratio']) if 'volume_ratio' in item and item['volume_ratio'] else 0 - + self.bbp120 = float(item['bbp120']) if 'bbp120' in item and item['bbp120'] else 0 + self.bbp365 = float(item['bbp365']) if 'bbp365' in item and item['bbp365'] else 0 + self.bb120 = float(item['bb120']) if 'bb120' in item and item['bb120'] else 0 + self.bb365 = float(item['bb365']) if 'bb365' in item and item['bb365'] else 0 # 设置指标后更新实时分析 self.update_realtime_analysis() diff --git a/ChanLun.py b/ChanLun.py index 77f0087..f7d5d1e 100644 --- a/ChanLun.py +++ b/ChanLun.py @@ -106,10 +106,10 @@ class ChanLun(): klc_index += 1 if klc.klc_fx_type == Chan_KLC_FX.TOP3: state_list.append("10") - print(klc.start_time, klc.end_time, klc.klc_fx_type) + #print(klc.start_time, klc.end_time, klc.klc_fx_type) elif klc.klc_fx_type == Chan_KLC_FX.BOTTOM3: state_list.append("-10") - print(klc.start_time, klc.end_time, klc.klc_fx_type) + #print(klc.start_time, klc.end_time, klc.klc_fx_type) else: state_list.append("00") else: diff --git a/strategies/ChanLun_BTC_30.py b/strategies/ChanLun_BTC_30.py index 432d278..6eade46 100644 --- a/strategies/ChanLun_BTC_30.py +++ b/strategies/ChanLun_BTC_30.py @@ -115,7 +115,7 @@ class ChanLun_BTC_30(IStrategy): state_list = self.chan.get_klc_state_list(dataframe_15) dataframe_15['state'] = state_list dataframe_15['fx'] = state_list - + state_list = self.chan.get_klc_state_list(dataframe_30) #bi_list_1 = self.chan.get_bi_list(dataframe) #bi_list_5 = self.chan.get_bi_list(dataframe_5) #bi_list_15 = self.chan.get_bi_list(dataframe_15) @@ -154,12 +154,17 @@ class ChanLun_BTC_30(IStrategy): period = 6 macd = ta.MACD(df, fastperiod=fast, slowperiod=slow, signalperiod=period) bb365 = ta.BBANDS(df, timeperiod=365, nbdevup=3.0, nbdevdn=3.0, matype=0) - bbp365 = ta.BBP(df, timeperiod=365) bb120 = ta.BBANDS(df, timeperiod=120, nbdevup=3.0, nbdevdn=3.0, matype=0) - bbp120 = ta.BBP(df, timeperiod=120) + + # 手动计算布林带 %B 指标 (BBP) + # %B = (Price - Lower Band) / (Upper Band - Lower Band) + bbp365 = (df['close'] - bb365['lowerband']) / (bb365['upperband'] - bb365['lowerband']) + bbp120 = (df['close'] - bb120['lowerband']) / (bb120['upperband'] - bb120['lowerband']) + df['bb365'] = bb365['upperband'] df['bbp365'] = bbp365 df['bb120'] = bb120['upperband'] + df['bbp120'] = bbp120 df['macd'] = macd['macd'] df['macdsignal'] = macd['macdsignal'] df['macdhist'] = macd['macdhist'] @@ -279,8 +284,8 @@ class ChanLun_BTC_30(IStrategy): ema10 = 'resample_{}_ema10'.format(self.get_ticker_indicator()*self.time30) ema26 = 'resample_{}_ema26'.format(self.get_ticker_indicator()*self.time30) ema52 = 'resample_{}_ema52'.format(self.get_ticker_indicator()*self.time30) - print(last_candle[ema5], last_candle[ema10], last_candle[ema26], last_candle[ema52]) - print(last_candle['close']) + #print(last_candle[ema5], last_candle[ema10], last_candle[ema26], last_candle[ema52]) + #print(last_candle['close']) klc_list = self.chan.get_klc_list(resample_to_interval(dataframe, self.get_ticker_indicator() * self.time30)) bi_list = self.chan.cal_bi_list(klc_list) if self.last_order is None: diff --git a/web/templates/index.html b/web/templates/index.html index af531e0..e4bce9f 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -48,7 +48,7 @@ } .chart-container { width: 100%; - height: 700px; + height: 900px; margin-top: 10px; border: 1px solid #e9ecef; border-radius: 8px; @@ -355,11 +355,14 @@ margin-right: 4px; } - .add-ma-btn { + .indicator-dropdown { position: absolute; top: 10px; right: 120px; z-index: 100; + } + + .add-indicator-btn { background-color: #0d6efd; color: white; border: none; @@ -368,12 +371,134 @@ font-size: 13px; cursor: pointer; box-shadow: 0 2px 4px rgba(0,0,0,0.1); + display: flex; + align-items: center; + gap: 5px; } - .add-ma-btn:hover { + .add-indicator-btn:hover { background-color: #0a58ca; } + .add-indicator-btn:focus { + box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.25); + } + + .dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + min-width: 160px; + padding: 0.5rem 0; + margin: 0.125rem 0 0; + font-size: 0.875rem; + color: #212529; + text-align: left; + background-color: #fff; + background-clip: padding-box; + border: 1px solid rgba(0,0,0,.15); + border-radius: 0.375rem; + box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15); + } + + .dropdown-menu.show { + display: block; + } + + .dropdown-item { + display: block; + width: 100%; + padding: 0.375rem 1rem; + clear: both; + font-weight: 400; + color: #212529; + text-align: inherit; + text-decoration: none; + white-space: nowrap; + background-color: transparent; + border: 0; + cursor: pointer; + } + + .dropdown-item:hover, + .dropdown-item:focus { + background-color: #f8f9fa; + color: #16181b; + } + + .dropdown-item i { + margin-right: 8px; + } + + .indicator-panel { + position: absolute; + top: 10px; + left: 10px; + z-index: 1000; + background: rgba(255, 255, 255, 0.95); + border: 1px solid #e0e0e0; + border-radius: 8px; + padding: 8px; + max-width: 350px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + + .indicator-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 0; + font-size: 12px; + border-bottom: 1px solid #f0f0f0; + } + + .indicator-item:last-child { + border-bottom: none; + } + + .indicator-info { + display: flex; + align-items: center; + gap: 8px; + } + + .indicator-label { + font-weight: 500; + } + + .indicator-value { + color: #666; + } + + .indicator-actions { + display: flex; + gap: 4px; + } + + .indicator-action-btn { + background: none; + border: none; + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + font-size: 11px; + color: #666; + } + + .indicator-action-btn:hover { + background-color: #f0f0f0; + color: #333; + } + + .indicator-color-indicator { + width: 12px; + height: 2px; + border-radius: 1px; + margin-right: 4px; + } + /* 配置弹窗样式 */ .ma-config-modal { position: fixed; @@ -483,6 +608,190 @@ width: 80%; height: 20px; } + + /* 布林带面板样式 */ + .bb-panel { + position: absolute; + bottom: 20px; + left: 20px; + background: rgba(255, 255, 255, 0.95); + border: 1px solid #ddd; + border-radius: 8px; + padding: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + min-width: 200px; + max-width: 300px; + z-index: 50; + } + + .bb-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 0; + border-bottom: 1px solid #f0f0f0; + font-size: 12px; + } + + .bb-item:last-child { + border-bottom: none; + } + + .bb-info { + flex: 1; + margin-right: 10px; + } + + .bb-label { + font-weight: 600; + color: #333; + } + + .bb-value { + color: #666; + font-size: 11px; + } + + .bb-actions { + display: flex; + gap: 6px; + } + + .bb-action-btn { + width: 20px; + height: 20px; + border: none; + background: #f8f9fa; + border-radius: 3px; + cursor: pointer; + font-size: 10px; + color: #666; + } + + .bb-action-btn:hover { + background: #e9ecef; + color: #333; + } + + .bb-color-indicator { + width: 12px; + height: 12px; + border-radius: 2px; + margin-right: 4px; + display: inline-block; + } + + + + .bb-config-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: none; + justify-content: center; + align-items: center; + z-index: 1000; + } + + .bb-config-content { + background: white; + border-radius: 8px; + padding: 16px; + width: 400px; + max-width: 90vw; + max-height: 85vh; + overflow-y: auto; + } + + .bb-config-title { + font-size: 18px; + font-weight: 600; + margin-bottom: 20px; + color: #333; + } + + .bb-config-form { + display: flex; + flex-direction: column; + gap: 16px; + } + + .bb-config-group { + display: flex; + flex-direction: column; + gap: 6px; + } + + .bb-config-label { + font-weight: 500; + color: #555; + font-size: 14px; + } + + .bb-config-input, .bb-config-select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 6px; + font-size: 14px; + transition: border-color 0.2s; + } + + .bb-config-input:focus, .bb-config-select:focus { + outline: none; + border-color: #667eea; + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); + } + + .bb-config-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 20px; + } + + .bb-config-btn { + padding: 10px 20px; + border: none; + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: all 0.2s; + } + + .bb-config-btn-primary { + background: #667eea; + color: white; + } + + .bb-config-btn-primary:hover { + background: #5a67d8; + } + + .bb-config-btn-secondary { + background: #f8f9fa; + color: #6c757d; + } + + .bb-config-btn-secondary:hover { + background: #e9ecef; + } + + .bb-line-preview { + height: 30px; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 8px; + background: linear-gradient(to right, #f8f9fa, #fff); + position: relative; + } + + .bb-line-preview svg { + width: 100%; + height: 100%; + } @@ -710,12 +1019,22 @@
- - - - + +
+ +
@@ -962,7 +1281,9 @@ elementUncompletedZsSeries: [], tradePointSeries: [], mainBollingerSeries: [], - elementBollingerSeries: [] + elementBollingerSeries: [], + maSeries: [], // 添加均线系列 + bbSeries: [] // 添加布林带系列 }, state: { isInitialized: false, @@ -1586,7 +1907,8 @@ tradePointSeries: [], mainBollingerSeries: [], elementBollingerSeries: [], - maSeries: [] // 添加均线系列数组 + maSeries: [], // 添加均线系列数组 + bbSeries: [] // 添加布林带系列数组 }, state: { isInitialized: false, @@ -3975,8 +4297,11 @@ // 添加均线到图表 addMovingAveragesToChart(candles); - // 更新均线面板显示 - updateMAPanel(); + // 添加布林带到图表 + addBollingerBandsToChart(candles); + + // 更新技术指标面板显示 + updateIndicatorPanel(); // 绑定同步事件 bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, mainChart, volumeChart, atrChart, macdChart, showMacd); @@ -4089,6 +4414,9 @@ // 更新均线数据 addMovingAveragesToChart(candles); + // 更新布林带数据 + addBollingerBandsToChart(candles); + // 更新成交量数据 let volumes = []; if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) { @@ -5318,7 +5646,9 @@ elementUncompletedZsSeries: [], tradePointSeries: [], mainBollingerSeries: [], - elementBollingerSeries: [] + elementBollingerSeries: [], + maSeries: [], // 添加均线系列 + bbSeries: [] // 添加布林带系列 }; } catch (e) { console.error('销毁图表错误:', e); @@ -6803,74 +7133,52 @@ }); } - // 获取分型强度对应的CSS类 - function getStrengthClass(strength) { - if (strength >= 2.0) return 'text-danger fw-bold'; - else if (strength >= 1.5) return 'text-warning fw-bold'; - else if (strength >= 1.0) return 'text-info'; - else return ''; - } - - // 分析特定股票 - function analyzeStock(symbol) { - // 切换到主分析页面 - $('#stock-filter-tab').removeClass('active'); - $('#kline-tab').addClass('active'); - $('#stock-filter').removeClass('show active'); - $('#kline').addClass('show active'); - - // 切换数据源为A股 - $('#dataSource').val('a_stock'); - $('#dataSource').trigger('change'); - - // 等待数据源切换完成后设置股票代码 - setTimeout(() => { - $('#astockSymbol').val(symbol); - // 触发分析 - updateChart(); - }, 500); - } - - // 导出筛选结果 - function exportFilterResults() { - const tbody = $('#stockFilterTable tbody tr'); - - if (tbody.length === 0 || (tbody.length === 1 && tbody.find('td').length === 1)) { - alert('没有可导出的数据'); - return; - } - - // 创建CSV内容 - const headers = ['股票代码', '股票名称', '分型时间', '分型类型', '分型强度', '分型价格', '当前价格', '涨跌幅(%)']; - let csvContent = headers.join(',') + '\n'; - - tbody.each(function() { - const cells = $(this).find('td'); - if (cells.length > 1) { // 排除"没有数据"的行 - const row = []; - cells.slice(0, 8).each(function() { // 只取前8列,排除操作列 - row.push($(this).text().trim()); - }); - csvContent += row.join(',') + '\n'; - } - }); - - // 创建下载链接 - const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' }); - const link = document.createElement('a'); - const url = URL.createObjectURL(blob); - link.setAttribute('href', url); - link.setAttribute('download', `股票筛选结果_${new Date().toISOString().slice(0, 10)}.csv`); - link.style.visibility = 'hidden'; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - // 均线系统全局变量 let movingAverages = []; // 存储所有均线配置 let maIdCounter = 0; // 均线ID计数器 + // 布林带系统全局变量 + let bollingerBands = []; // 存储所有布林带配置 + let bbIdCounter = 0; // 布林带ID计数器 + + // 获取随机颜色 + function getRandomColor() { + const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', + '#FECA57', '#48CAE4', '#F38BA8', '#A8DADC', '#F1C0E8']; + return colors[Math.floor(Math.random() * colors.length)]; + } + + // 显示布林带配置弹窗 + function showBBConfig() { + console.log('📂 显示布林带配置弹窗,设置为添加模式'); + $('#bbConfigModal').css('display', 'flex'); + + // 重置表单 + $('#bbType').val('Bollinger Bands'); + $('#bbLength').val(20); + $('#bbStdDev').val(2); + $('#bbSource').val('close'); + $('#bbLineWidth').val(2); + $('#bbLineStyle').val(0); + $('#bbColor').val(getRandomColor()); + + // 移除所有现有的点击事件,避免事件冲突 + $('#bbConfigSubmitBtn').off('click'); + + // 设置按钮为添加模式 + $('#bbConfigSubmitBtn').text('添加').on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('🖱️ 点击了添加按钮(来自showBBConfig),准备添加新布林带'); + addBollingerBand(); + }); + + console.log('🔄 按钮已设置为添加模式'); + + // 更新预览 + setTimeout(updateBBLinePreview, 50); + } + // 显示均线配置弹窗 function showMAConfig() { console.log('📂 显示均线配置弹窗,设置为添加模式'); @@ -6917,13 +7225,6 @@ console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加均线"按钮'); } - // 获取随机颜色 - function getRandomColor() { - const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', - '#FECA57', '#48CAE4', '#F38BA8', '#A8DADC', '#F1C0E8']; - return colors[Math.floor(Math.random() * colors.length)]; - } - // 添加均线 function addMovingAverage() { console.log('🚨🚨🚨 ➕ 警告:addMovingAverage函数被调用了!!!'); @@ -7086,18 +7387,19 @@ return result; } - // 更新均线面板 - function updateMAPanel() { - const panel = $('#maPanel'); + // 更新技术指标面板(统一面板) + function updateIndicatorPanel() { + const panel = $('#indicatorPanel'); - if (movingAverages.length === 0) { + if (movingAverages.length === 0 && bollingerBands.length === 0) { panel.hide(); return; } let html = ''; - let hasVisibleMA = false; + let hasVisibleIndicator = false; + // 添加均线指标 movingAverages.forEach(ma => { // 获取最新价格 const latestValue = ma.data && ma.data.length > 0 ? @@ -7116,20 +7418,20 @@ const eyeIcon = ma.visible ? 'bi-eye' : 'bi-eye-slash'; html += ` -
-
-
- ${ma.type}(${ma.length})${smoothText}${styleText} - ${ma.visible ? latestValue : '--'} +
+
+
+ ${ma.type}(${ma.length})${smoothText}${styleText} + ${ma.visible ? latestValue : '--'}
-
- - -
@@ -7137,18 +7439,74 @@ `; if (ma.visible) { - hasVisibleMA = true; + hasVisibleIndicator = true; + } + }); + + // 添加布林带指标 + bollingerBands.forEach(bb => { + // 获取最新价格 + const latestValue = bb.data && bb.data.length > 0 ? + bb.data[bb.data.length - 1].middle.toFixed(2) : '--'; + + // 获取线条样式描述 + const lineStyleNames = ['实线', '点线', '虚线', '大虚线']; + const styleText = bb.lineWidth && bb.lineStyle !== undefined ? + ` ${bb.lineWidth}px ${lineStyleNames[bb.lineStyle] || '实线'}` : ''; + + // 显示所有布林带(包括隐藏的),但用不同样式区分 + const itemStyle = bb.visible ? '' : 'opacity: 0.5;'; + const eyeIcon = bb.visible ? 'bi-eye' : 'bi-eye-slash'; + + // 创建渐变颜色指示器显示三种颜色 + const colorIndicatorStyle = ` + background: linear-gradient(90deg, + ${bb.upperColor || '#ff6b6b'} 0%, + ${bb.middleColor || '#667eea'} 50%, + ${bb.lowerColor || '#4ecdc4'} 100%); + border-radius: 2px; + `; + + html += ` +
+
+
+ ${bb.type}(${bb.length}, ${bb.upperMultiplier || 2}, ${bb.lowerMultiplier || 2})${styleText} + ${bb.visible ? latestValue : '--'} +
+
+ + + +
+
+ `; + + if (bb.visible) { + hasVisibleIndicator = true; } }); - if (html.trim() === '') { - panel.hide(); - } else { panel.html(html); + + if (hasVisibleIndicator || movingAverages.length > 0 || bollingerBands.length > 0) { panel.show(); + } else { + panel.hide(); } } + // 更新均线面板(兼容旧代码) + function updateMAPanel() { + updateIndicatorPanel(); + } + // 切换均线可见性 function toggleMAVisibility(maId) { console.log('👁️ 切换均线可见性,ID:', maId, 'Type:', typeof maId); @@ -7416,8 +7774,8 @@ console.log(`🎯 均线添加完成,共添加 ${addedCount} 条均线`); console.log('📊 当前图表中的均线系列数量:', tvWidget.series.maSeries.length); - // 更新均线面板 - updateMAPanel(); + // 更新技术指标面板 + updateIndicatorPanel(); } // 监听平滑类型变化 @@ -7426,8 +7784,427 @@ $('#maSmoothLength').prop('disabled', isNone); }); - // 更新线条预览 + // 显示布林带配置弹窗 + function showBBConfig() { + console.log('📂 显示布林带配置弹窗,设置为添加模式'); + $('#bbConfigModal').css('display', 'flex'); + + // 重置表单 + $('#bbType').val('Bollinger Bands'); + $('#bbLength').val(20); + $('#bbStdDev').val(2); + $('#bbSource').val('close'); + $('#bbLineWidth').val(2); + $('#bbLineStyle').val(0); + $('#bbColor').val(getRandomColor()); + + // 移除所有现有的点击事件,避免事件冲突 + $('#bbConfigSubmitBtn').off('click'); + + // 设置按钮为添加模式 + $('#bbConfigSubmitBtn').text('添加').on('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('🖱️ 点击了添加按钮(来自showBBConfig),准备添加新布林带'); + addBollingerBand(); + }); + + console.log('🔄 按钮已设置为添加模式'); + + // 更新预览 + setTimeout(updateBBLinePreview, 50); + } + + // 隐藏布林带配置弹窗 + function hideBBConfig() { + console.log('🔒 关闭布林带配置弹窗'); + $('#bbConfigModal').css('display', 'none'); + + // 移除所有现有的点击事件 + $('#bbConfigSubmitBtn').off('click'); + + // 恢复按钮为添加模式 - 但不立即绑定事件,防止意外触发 + $('#bbConfigSubmitBtn').text('添加'); + + console.log('🔄 按钮已重置为添加模式,等待用户主动点击"添加布林带"按钮'); + } + + // 添加布林带 + function addBollingerBand() { + console.log('🚨🚨🚨 ➕ 警告:addBollingerBand函数被调用了!!!'); + console.log('📊 添加前布林带配置数量:', bollingerBands.length); + console.log('🔍 调用堆栈:', new Error().stack); + + const config = { + id: ++bbIdCounter, + type: $('#bbType').val(), + length: parseInt($('#bbLength').val()), + upperMultiplier: parseFloat($('#bbUpperMultiplier').val()), + lowerMultiplier: parseFloat($('#bbLowerMultiplier').val()), + source: $('#bbSource').val(), + lineWidth: parseInt($('#bbLineWidth').val()), + lineStyle: parseInt($('#bbLineStyle').val()), + upperColor: $('#bbUpperColor').val(), + middleColor: $('#bbMiddleColor').val(), + lowerColor: $('#bbLowerColor').val(), + visible: true + }; + + console.log('📝 新布林带配置:', { + id: config.id, + type: config.type, + length: config.length, + upperMultiplier: config.upperMultiplier, + lowerMultiplier: config.lowerMultiplier, + source: config.source, + colors: { + upper: config.upperColor, + middle: config.middleColor, + lower: config.lowerColor + } + }); + + // 验证输入 + if (config.length < 1) { + alert('布林带长度必须大于0'); + return; + } + + if (config.upperMultiplier < 0.1) { + alert('上轨倍数必须大于0.1'); + return; + } + + if (config.lowerMultiplier < 0.1) { + alert('下轨倍数必须大于0.1'); + return; + } + + bollingerBands.push(config); + + console.log('📊 添加后布林带配置数量:', bollingerBands.length); + + hideBBConfig(); + + // 直接使用前端数据计算和添加布林带,不请求后台 + if (tvWidget.mainChart && currentData && currentData.kline_data) { + const candles = getCurrentCandleData(); + addBollingerBandsToChart(candles); + addMovingAveragesToChart(candles); // 同时更新均线 + } + } + + // 计算布林带数据 + function calculateBB(data, length, upperMultiplier, lowerMultiplier, source) { + if (!data || data.length < length) return []; + + const sourceData = data.map(candle => { + switch(source) { + case 'open': return candle.open; + case 'high': return candle.high; + case 'low': return candle.low; + case 'close': return candle.close; + case 'hl2': return (candle.high + candle.low) / 2; + case 'hlc3': return (candle.high + candle.low + candle.close) / 3; + case 'ohlc4': return (candle.open + candle.high + candle.low + candle.close) / 4; + default: return candle.close; + } + }); + + const result = []; + + for (let i = length - 1; i < sourceData.length; i++) { + const start = Math.max(0, i - length + 1); + const slice = sourceData.slice(start, i + 1); + const avg = slice.reduce((sum, val) => sum + val, 0) / length; + const std = Math.sqrt(slice.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / length); + + result.push({ + time: data[i].time, + upper: avg + upperMultiplier * std, + middle: avg, + lower: avg - lowerMultiplier * std + }); + } + + return result; + } + + // 更新布林带面板(兼容旧代码) + function updateBBPanel() { + updateIndicatorPanel(); + } + + // 切换布林带可见性 + function toggleBBVisibility(bbId) { + console.log('👁️ 切换布林带可见性,ID:', bbId, 'Type:', typeof bbId); + + const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换 + if (bb) { + console.log('📝 找到布林带,当前可见性:', bb.visible); + bb.visible = !bb.visible; + console.log('✅ 切换后可见性:', bb.visible); + + // 直接使用前端数据重新计算布林带 + if (tvWidget.mainChart && currentData && currentData.kline_data) { + const candles = getCurrentCandleData(); + addBollingerBandsToChart(candles); + addMovingAveragesToChart(candles); // 同时更新均线 + } + } else { + console.error('❌ 未找到要切换可见性的布林带,ID:', bbId); + } + } + + // 配置布林带 + function configBB(bbId) { + console.log('⚙️ 开始配置布林带,ID:', bbId, 'Type:', typeof bbId); + console.log('📊 当前所有布林带ID:', bollingerBands.map(b => ({id: b.id, type: typeof b.id}))); + + const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换 + if (!bb) { + console.error('❌ 未找到要配置的布林带,ID:', bbId); + return; + } + + console.log('📝 找到要配置的布林带:', bb.type, bb.length, bb.upperMultiplier, bb.lowerMultiplier, bb.upperColor, bb.middleColor, bb.lowerColor); + + // 填充表单 + $('#bbType').val(bb.type); + $('#bbLength').val(bb.length); + $('#bbUpperMultiplier').val(bb.upperMultiplier || 2); + $('#bbLowerMultiplier').val(bb.lowerMultiplier || 2); + $('#bbSource').val(bb.source); + $('#bbLineWidth').val(bb.lineWidth || 2); + $('#bbLineStyle').val(bb.lineStyle || 0); + $('#bbUpperColor').val(bb.upperColor || '#ff6b6b'); + $('#bbMiddleColor').val(bb.middleColor || '#667eea'); + $('#bbLowerColor').val(bb.lowerColor || '#4ecdc4'); + + // 移除所有现有的点击事件,避免事件冲突 + $('#bbConfigSubmitBtn').off('click'); + + // 修改按钮为更新模式 + $('#bbConfigSubmitBtn').text('更新').on('click', (function(capturedBbId) { + return function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('🖱️ 点击了更新按钮,准备更新ID:', capturedBbId); + console.log('🔍 捕获的ID类型:', typeof capturedBbId); + updateBollingerBand(capturedBbId); + }; + })(bbId)); + + console.log('🔄 按钮已切换为更新模式'); + + $('#bbConfigModal').css('display', 'flex'); + + // 更新预览 + setTimeout(updateBBLinePreview, 50); + } + + // 更新布林带 + function updateBollingerBand(bbId) { + console.log('🔄 开始更新布林带,ID:', bbId, 'Type:', typeof bbId); + console.log('📊 更新前布林带配置数量:', bollingerBands.length); + console.log('📊 当前所有布林带:', bollingerBands.map(b => ({id: b.id, type: typeof b.id, bbType: b.type, length: b.length, upperMultiplier: b.upperMultiplier, lowerMultiplier: b.lowerMultiplier, upperColor: b.upperColor, middleColor: b.middleColor, lowerColor: b.lowerColor}))); + + const bb = bollingerBands.find(b => b.id == bbId); // 使用==而不是===来兼容类型转换 + if (!bb) { + console.error('❌ 未找到要更新的布林带配置,ID:', bbId); + console.error('❌ 可用的布林带ID:', bollingerBands.map(b => b.id)); + alert('错误:未找到要更新的布林带配置!'); + return; + } + + console.log('📝 找到要更新的布林带:', bb.type, bb.length, bb.upperMultiplier, bb.lowerMultiplier, bb.upperColor, bb.middleColor, bb.lowerColor); + + // 验证输入 + const newLength = parseInt($('#bbLength').val()); + const newUpperMultiplier = parseFloat($('#bbUpperMultiplier').val()); + const newLowerMultiplier = parseFloat($('#bbLowerMultiplier').val()); + + if (newLength < 1) { + alert('布林带长度必须大于0'); + return; + } + + if (newUpperMultiplier < 0.1) { + alert('上轨倍数必须大于0.1'); + return; + } + + if (newLowerMultiplier < 0.1) { + alert('下轨倍数必须大于0.1'); + return; + } + + // 记录更新前的配置 + console.log('🔧 更新前配置:', { + type: bb.type, + length: bb.length, + upperMultiplier: bb.upperMultiplier, + lowerMultiplier: bb.lowerMultiplier, + source: bb.source, + colors: { + upper: bb.upperColor, + middle: bb.middleColor, + lower: bb.lowerColor + } + }); + + // 更新配置 + bb.type = $('#bbType').val(); + bb.length = newLength; + bb.upperMultiplier = newUpperMultiplier; + bb.lowerMultiplier = newLowerMultiplier; + bb.source = $('#bbSource').val(); + bb.lineWidth = parseInt($('#bbLineWidth').val()); + bb.lineStyle = parseInt($('#bbLineStyle').val()); + bb.upperColor = $('#bbUpperColor').val(); + bb.middleColor = $('#bbMiddleColor').val(); + bb.lowerColor = $('#bbLowerColor').val(); + + // 记录更新后的配置 + console.log('✅ 更新后配置:', { + type: bb.type, + length: bb.length, + upperMultiplier: bb.upperMultiplier, + lowerMultiplier: bb.lowerMultiplier, + source: bb.source, + colors: { + upper: bb.upperColor, + middle: bb.middleColor, + lower: bb.lowerColor + } + }); + console.log('📊 更新后布林带配置数量:', bollingerBands.length); + + hideBBConfig(); + + // 直接使用前端数据重新计算布林带 + if (tvWidget.mainChart && currentData && currentData.kline_data) { + const candles = getCurrentCandleData(); + addBollingerBandsToChart(candles); + } + } + + // 删除布林带 + function deleteBB(bbId) { + console.log('🗑️ 删除布林带,ID:', bbId, 'Type:', typeof bbId); + console.log('📊 删除前布林带配置数量:', bollingerBands.length); + + const beforeCount = bollingerBands.length; + bollingerBands = bollingerBands.filter(b => b.id != bbId); // 使用!=而不是!==来兼容类型转换 + + console.log('📊 删除后布林带配置数量:', bollingerBands.length); + console.log('✅ 是否成功删除:', beforeCount > bollingerBands.length); + + // 直接使用前端数据重新计算布林带 + if (tvWidget.mainChart && currentData && currentData.kline_data) { + const candles = getCurrentCandleData(); + addBollingerBandsToChart(candles); + } + } + + // 添加布林带到图表 + function addBollingerBandsToChart(candleData) { + if (!tvWidget.mainChart || !candleData || candleData.length === 0) { + return; + } + + console.log('🔄 重新绘制布林带,当前布林带配置数量:', bollingerBands.length); + + // 强制清除现有的布林带系列 + if (tvWidget.series.bbSeries && tvWidget.series.bbSeries.length > 0) { + console.log('🗑️ 清除现有布林带系列:', tvWidget.series.bbSeries.length); + tvWidget.series.bbSeries.forEach((series, index) => { + try { + tvWidget.mainChart.removeSeries(series); + console.log('✅ 移除布林带系列:', index); + } catch (e) { + console.error('❌ 移除布林带系列失败:', index, e); + } + }); + } + // 重置布林带系列数组 + tvWidget.series.bbSeries = []; + + // 为每个布林带配置创建系列 + let addedCount = 0; + bollingerBands.forEach((bbConfig, index) => { + if (!bbConfig.visible) { + console.log(`⏭️ 跳过隐藏的布林带 [${index}]:`, bbConfig.type, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier); + return; + } + + try { + console.log(`📈 开始创建布林带 [${index}]:`, bbConfig.type, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.upperColor, bbConfig.middleColor, bbConfig.lowerColor); + + // 计算布林带数据 + const bbData = calculateBB(candleData, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, bbConfig.source); + + // 创建线系列 + const upperSeries = tvWidget.mainChart.addLineSeries({ + color: bbConfig.upperColor, + lineWidth: bbConfig.lineWidth || 2, + lineStyle: bbConfig.lineStyle || 0, // 0=实线, 1=点线, 2=虚线, 3=大虚线 + title: `${bbConfig.type}(${bbConfig.length}, ${bbConfig.upperMultiplier}, ${bbConfig.lowerMultiplier}) Upper`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true, + }); + + const middleSeries = tvWidget.mainChart.addLineSeries({ + color: bbConfig.middleColor, + lineWidth: bbConfig.lineWidth || 2, + lineStyle: bbConfig.lineStyle || 0, // 0=实线, 1=点线, 2=虚线, 3=大虚线 + title: `${bbConfig.type}(${bbConfig.length}, ${bbConfig.upperMultiplier}, ${bbConfig.lowerMultiplier}) Middle`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true, + }); + + const lowerSeries = tvWidget.mainChart.addLineSeries({ + color: bbConfig.lowerColor, + lineWidth: bbConfig.lineWidth || 2, + lineStyle: bbConfig.lineStyle || 0, // 0=实线, 1=点线, 2=虚线, 3=大虚线 + title: `${bbConfig.type}(${bbConfig.length}, ${bbConfig.upperMultiplier}, ${bbConfig.lowerMultiplier}) Lower`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true, + }); + + // 设置数据 + upperSeries.setData(bbData.map(item => ({ time: item.time, value: item.upper }))); + middleSeries.setData(bbData.map(item => ({ time: item.time, value: item.middle }))); + lowerSeries.setData(bbData.map(item => ({ time: item.time, value: item.lower }))); + + // 保存数据到配置中以便显示最新值 + bbConfig.data = bbData; + + // 保存系列引用 + tvWidget.series.bbSeries.push(upperSeries, middleSeries, lowerSeries); + addedCount++; + + console.log(`✅ 布林带创建成功 [${index}]:`, bbConfig.type, bbConfig.length, bbConfig.upperMultiplier, bbConfig.lowerMultiplier, '数据点:', bbData.length); + + } catch (e) { + console.error('❌ 添加布林带时出错:', e, bbConfig); + } + }); + + console.log(`🎯 布林带添加完成,共添加 ${addedCount} 条布林带`); + console.log('📊 当前图表中的布林带系列数量:', tvWidget.series.bbSeries.length); + + // 更新技术指标面板 + updateIndicatorPanel(); + } + + // 更新均线预览 function updateLinePreview() { + // 检查是否在均线配置窗口 + if ($('#maConfigModal').is(':visible')) { const color = $('#maColor').val(); const width = $('#maLineWidth').val(); const style = $('#maLineStyle').val(); @@ -7450,21 +8227,97 @@ case 3: // 大虚线 line.attr('stroke-dasharray', '10,5'); break; - } - } + } + } + } + + // 更新布林带预览 + function updateBBLinePreview() { + const upperColor = $('#bbUpperColor').val(); + const middleColor = $('#bbMiddleColor').val(); + const lowerColor = $('#bbLowerColor').val(); + const width = $('#bbLineWidth').val(); + const style = $('#bbLineStyle').val(); + + const upperLine = $('#bbPreviewUpper'); + const middleLine = $('#bbPreviewMiddle'); + const lowerLine = $('#bbPreviewLower'); + + // 设置各条线的颜色 + upperLine.attr('stroke', upperColor); + middleLine.attr('stroke', middleColor); + lowerLine.attr('stroke', lowerColor); + + // 设置线条宽度和样式 + [upperLine, middleLine, lowerLine].forEach(line => { + line.attr('stroke-width', width); + + // 设置线条样式 + switch(parseInt(style)) { + case 0: // 实线 + line.attr('stroke-dasharray', 'none'); + break; + case 1: // 点线 + line.attr('stroke-dasharray', '2,3'); + break; + case 2: // 虚线 + line.attr('stroke-dasharray', '5,5'); + break; + case 3: // 大虚线 + line.attr('stroke-dasharray', '10,5'); + break; + } + }); + } // 监听配置变化以更新预览 $(document).on('change', '#maColor, #maLineWidth, #maLineStyle', updateLinePreview); + $(document).on('change', '#bbUpperColor, #bbMiddleColor, #bbLowerColor, #bbLineWidth, #bbLineStyle', updateBBLinePreview); // 点击弹窗外部关闭 - $(document).on('click', '#maConfigModal', function(e) { + $(document).on('click', '#bbConfigModal', function(e) { if (e.target === this) { - hideMAConfig(); + hideBBConfig(); } }); + // 初始化技术指标下拉菜单 + function initIndicatorDropdown() { + // 确保Bootstrap下拉菜单正常工作 + try { + // 如果Bootstrap没有正确加载,添加手动下拉菜单功能 + $('#indicatorDropdown').off('click').on('click', function(e) { + e.preventDefault(); + const $menu = $(this).next('.dropdown-menu'); + + // 切换菜单显示状态 + if ($menu.hasClass('show')) { + $menu.removeClass('show'); + } else { + // 隐藏其他所有下拉菜单 + $('.dropdown-menu').removeClass('show'); + $menu.addClass('show'); + } + }); + + // 点击菜单外部关闭下拉菜单 + $(document).on('click', function(e) { + if (!$(e.target).closest('.indicator-dropdown').length) { + $('.dropdown-menu').removeClass('show'); + } + }); + + console.log('✅ 技术指标下拉菜单初始化完成'); + } catch (error) { + console.error('❌ 技术指标下拉菜单初始化失败:', error); + } + } + // 页面加载完成后初始化 $(document).ready(function() { + // 初始化技术指标下拉菜单 + initIndicatorDropdown(); + // 设置默认的筛选时间(最近7天) const now = new Date(); const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); @@ -7603,5 +8456,103 @@
+ + +
+
+
添加布林带
+
+
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ +
+ + + + + +
+
+
+
+ + +
+
+
\ No newline at end of file