From c6d02d78021e6aa60e561cf3a02e2026398bcfef Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Wed, 23 Jul 2025 01:53:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=B8=83=E6=9E=97=E5=B8=A6?= =?UTF-8?q?=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/app.py | 2 +- web/templates/index.html | 585 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 576 insertions(+), 11 deletions(-) diff --git a/web/app.py b/web/app.py index c22ff99..a7b4aa9 100644 --- a/web/app.py +++ b/web/app.py @@ -370,7 +370,7 @@ def analyze_chan(df, timeframe='1d'): "divergence_rate": float("inf"), "bsp2_follow_1": False, "bsp3_follow_1": False, - "min_zs_cnt": 1, + "min_zs_cnt": 2, "bs1_peak": False, "macd_algo": "peak", "bs_type": '1,2,3a,1p,2s,3b', diff --git a/web/templates/index.html b/web/templates/index.html index 8717337..705f0db 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -371,6 +371,18 @@ .ma-indicator-value { font-weight: bold; } + + /* 布林带指标样式 */ + .bb-indicator-colors { + display: flex; + align-items: center; + margin-right: 8px; + } + + .bb-values { + font-size: 11px; + line-height: 1.2; + } @@ -496,14 +508,13 @@ -
- - -
-
+
+ @@ -891,6 +902,10 @@ let movingAverages = []; let maCounter = 0; + // 布林带系统相关变量 + let bollingerBands = []; + let bbCounter = 0; + // 均线计算函数 function calculateMovingAverage(data, period, type = 'SMA') { if (!data || data.length < period) return []; @@ -1007,6 +1022,39 @@ return calculateMovingAverage(data, smoothingPeriod, smoothingType); } + // 布林带计算函数 + function calculateBollingerBands(data, period, multiplier = 2) { + if (!data || data.length < period) return { middle: [], upper: [], lower: [] }; + + const middle = []; + const upper = []; + const lower = []; + + for (let i = period - 1; i < data.length; i++) { + // 计算移动平均线(中线) + const sum = data.slice(i - period + 1, i + 1).reduce((acc, val) => acc + val.value, 0); + const ma = sum / period; + + // 计算标准差 + const variance = data.slice(i - period + 1, i + 1).reduce((acc, val) => { + return acc + Math.pow(val.value - ma, 2); + }, 0) / period; + const stdDev = Math.sqrt(variance); + + // 计算上下轨 + const upperValue = ma + (multiplier * stdDev); + const lowerValue = ma - (multiplier * stdDev); + + const timestamp = data[i].time; + + middle.push({ time: timestamp, value: ma }); + upper.push({ time: timestamp, value: upperValue }); + lower.push({ time: timestamp, value: lowerValue }); + } + + return { middle, upper, lower }; + } + // 添加均线到图表 function addMovingAverageToChart(config) { const ma = { @@ -1108,19 +1156,20 @@ // 更新均线指标显示 function updateMovingAverageIndicators() { - console.log('更新均线指标显示,当前均线数量:', movingAverages.length); + console.log('更新指标显示,当前均线数量:', movingAverages.length, '布林带数量:', bollingerBands.length); try { const container = $('#movingAverageIndicators'); const list = $('#maIndicatorsList'); - if (movingAverages.length === 0) { + if (movingAverages.length === 0 && bollingerBands.length === 0) { container.hide(); - console.log('没有均线,隐藏指标容器'); + console.log('没有指标,隐藏指标容器'); return; } list.empty(); + // 显示均线 movingAverages.forEach((ma, index) => { console.log(`创建均线指标 ${index + 1}:`, ma.id, ma.type, ma.period); const latestValue = ma.data && ma.data.length > 0 ? ma.data[ma.data.length - 1].value : 0; @@ -1148,10 +1197,47 @@ list.append(indicator); }); + // 显示布林带 + bollingerBands.forEach((bb, index) => { + console.log(`创建布林带指标 ${index + 1}:`, bb.id, bb.period, bb.multiplier); + const upperValue = bb.data && bb.data.upper && bb.data.upper.length > 0 ? bb.data.upper[bb.data.upper.length - 1].value : 0; + const middleValue = bb.data && bb.data.middle && bb.data.middle.length > 0 ? bb.data.middle[bb.data.middle.length - 1].value : 0; + const lowerValue = bb.data && bb.data.lower && bb.data.lower.length > 0 ? bb.data.lower[bb.data.lower.length - 1].value : 0; + + const indicator = $(` +
+
+
+
+
+
+ BOLL(${bb.period},${bb.multiplier}) +
+ 上: ${upperValue.toFixed(2)} + 中: ${middleValue.toFixed(2)} + 下: ${lowerValue.toFixed(2)} +
+
+ + + +
+
+ `); + + list.append(indicator); + }); + container.show(); - console.log('均线指标显示更新完成'); + console.log('指标显示更新完成'); } catch (error) { - console.error('更新均线指标显示时出错:', error); + console.error('更新指标显示时出错:', error); } } @@ -1258,10 +1344,260 @@ updateMovingAverageIndicators(); } + // 添加布林带到图表 + function addBollingerBandToChart(config) { + const bb = { + id: `bb_${++bbCounter}`, + ...config, + series: { + upper: null, + middle: null, + lower: null + }, + visible: true + }; + + if (!currentData) { + console.error('没有数据,无法添加布林带'); + return null; + } + + // 获取价格数据 + const priceData = getPriceData(null, config.source); + + // 计算布林带 + const bbData = calculateBollingerBands(priceData, config.period, config.multiplier); + + // 添加到图表 + const upperSeries = tvWidget.mainChart.addLineSeries({ + color: config.upperColor, + lineWidth: config.lineWidth, + lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0, + title: `BOLL上轨(${config.period},${config.multiplier})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + const middleSeries = tvWidget.mainChart.addLineSeries({ + color: config.middleColor, + lineWidth: config.lineWidth, + lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0, + title: `BOLL中轨(${config.period})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + const lowerSeries = tvWidget.mainChart.addLineSeries({ + color: config.lowerColor, + lineWidth: config.lineWidth, + lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0, + title: `BOLL下轨(${config.period},${config.multiplier})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + upperSeries.setData(bbData.upper); + middleSeries.setData(bbData.middle); + lowerSeries.setData(bbData.lower); + + bb.series.upper = upperSeries; + bb.series.middle = middleSeries; + bb.series.lower = lowerSeries; + bb.data = bbData; + + bollingerBands.push(bb); + tvWidget.series.movingAverageSeries.push(upperSeries, middleSeries, lowerSeries); + + // 更新显示 + updateMovingAverageIndicators(); + + return bb; + } + + // 删除布林带 + function removeBollingerBand(bbId) { + console.log('删除布林带:', bbId); + try { + const index = bollingerBands.findIndex(bb => bb.id === bbId); + if (index !== -1) { + const bb = bollingerBands[index]; + console.log('找到要删除的布林带:', bb); + + if (bb.series && tvWidget.mainChart) { + // 删除三条线 + tvWidget.mainChart.removeSeries(bb.series.upper); + tvWidget.mainChart.removeSeries(bb.series.middle); + tvWidget.mainChart.removeSeries(bb.series.lower); + + // 从数组中移除 + const removeFromArray = (arr, series) => { + const seriesIndex = arr.indexOf(series); + if (seriesIndex !== -1) { + arr.splice(seriesIndex, 1); + } + }; + + removeFromArray(tvWidget.series.movingAverageSeries, bb.series.upper); + removeFromArray(tvWidget.series.movingAverageSeries, bb.series.middle); + removeFromArray(tvWidget.series.movingAverageSeries, bb.series.lower); + } + bollingerBands.splice(index, 1); + updateMovingAverageIndicators(); + console.log('布林带删除成功'); + } else { + console.warn('未找到要删除的布林带:', bbId); + } + } catch (error) { + console.error('删除布林带时出错:', error); + } + } + + // 切换布林带显示/隐藏 + function toggleBollingerBand(bbId) { + console.log('切换布林带显示状态:', bbId); + try { + const bb = bollingerBands.find(bb => bb.id === bbId); + if (bb && bb.series) { + bb.visible = !bb.visible; + console.log('布林带新状态:', bb.visible ? '显示' : '隐藏'); + bb.series.upper.applyOptions({ visible: bb.visible }); + bb.series.middle.applyOptions({ visible: bb.visible }); + bb.series.lower.applyOptions({ visible: bb.visible }); + updateMovingAverageIndicators(); + console.log('布林带状态切换成功'); + } else { + console.warn('未找到布林带或series不存在:', bbId, bb); + } + } catch (error) { + console.error('切换布林带显示状态时出错:', error); + } + } + + // 编辑布林带配置 + function editBollingerBand(bbId) { + console.log('编辑布林带配置:', bbId); + try { + const bb = bollingerBands.find(bb => bb.id === bbId); + if (!bb) { + console.warn('未找到要编辑的布林带:', bbId); + return; + } + + console.log('找到布林带配置:', bb); + + // 填充表单 + $('#bbPeriod').val(bb.period); + $('#bbMultiplier').val(bb.multiplier); + $('#bbSource').val(bb.source); + $('#bbUpperColor').val(bb.upperColor); + $('#bbMiddleColor').val(bb.middleColor); + $('#bbLowerColor').val(bb.lowerColor); + $('#bbLineWidth').val(bb.lineWidth); + $('#bbLineWidthDisplay').text(bb.lineWidth); + $('#bbStyle').val(bb.style); + + // 更新确认按钮 + $('#addBollingerBandConfirm').text('更新布林带').data('editId', bbId); + + // 显示对话框 + const modal = new bootstrap.Modal(document.getElementById('bollingerBandModal')); + modal.show(); + console.log('编辑对话框已显示'); + } catch (error) { + console.error('编辑布林带配置时出错:', error); + } + } + + // 更新所有布林带数据 + function updateAllBollingerBands() { + if (!currentData) return; + + console.log('更新所有布林带数据,布林带数量:', bollingerBands.length); + + bollingerBands.forEach(bb => { + const priceData = getPriceData(null, bb.source); + + if (priceData.length === 0) { + console.warn('布林带数据为空,跳过:', bb.id); + return; + } + + const bbData = calculateBollingerBands(priceData, bb.period, bb.multiplier); + bb.data = bbData; + + // 如果序列不存在或无效,重新创建 + if ((!bb.series.upper || !bb.series.middle || !bb.series.lower) && tvWidget.mainChart) { + console.log('重新创建布林带序列:', bb.period, bb.multiplier); + try { + bb.series.upper = tvWidget.mainChart.addLineSeries({ + color: bb.upperColor, + lineWidth: bb.lineWidth, + lineStyle: bb.style === 'solid' ? 0 : bb.style === 'dotted' ? 1 : bb.style === 'dashed' ? 2 : 0, + title: `BOLL上轨(${bb.period},${bb.multiplier})`, + visible: bb.visible !== false, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + bb.series.middle = tvWidget.mainChart.addLineSeries({ + color: bb.middleColor, + lineWidth: bb.lineWidth, + lineStyle: bb.style === 'solid' ? 0 : bb.style === 'dotted' ? 1 : bb.style === 'dashed' ? 2 : 0, + title: `BOLL中轨(${bb.period})`, + visible: bb.visible !== false, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + bb.series.lower = tvWidget.mainChart.addLineSeries({ + color: bb.lowerColor, + lineWidth: bb.lineWidth, + lineStyle: bb.style === 'solid' ? 0 : bb.style === 'dotted' ? 1 : bb.style === 'dashed' ? 2 : 0, + title: `BOLL下轨(${bb.period},${bb.multiplier})`, + visible: bb.visible !== false, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + if (!tvWidget.series.movingAverageSeries) { + tvWidget.series.movingAverageSeries = []; + } + tvWidget.series.movingAverageSeries.push(bb.series.upper, bb.series.middle, bb.series.lower); + console.log('布林带序列创建成功:', bb.id); + } catch (error) { + console.error('创建布林带序列失败:', error); + return; + } + } else if (!tvWidget.mainChart) { + console.warn('主图表不存在,跳过布林带更新:', bb.id); + return; + } + + // 设置数据 + if (bb.series.upper && bb.series.middle && bb.series.lower) { + bb.series.upper.setData(bbData.upper); + bb.series.middle.setData(bbData.middle); + bb.series.lower.setData(bbData.lower); + console.log(`布林带 BOLL(${bb.period},${bb.multiplier}) 数据已更新,数据点数:`, bbData.upper.length); + } + }); + + updateMovingAverageIndicators(); + } + // 确保函数在全局作用域中可用 window.removeMovingAverage = removeMovingAverage; window.toggleMovingAverage = toggleMovingAverage; window.editMovingAverage = editMovingAverage; + window.removeBollingerBand = removeBollingerBand; + window.toggleBollingerBand = toggleBollingerBand; + window.editBollingerBand = editBollingerBand; // 测试函数 - 用于验证均线系统是否工作 window.testMovingAverageSystem = function() { @@ -1358,6 +1694,28 @@ modal.show(); }); + // 布林带按钮点击事件 + $('#addBollingerBandBtn').click(function() { + // 重置表单 + $('#bollingerBandForm')[0].reset(); + $('#bbPeriod').val(20); + $('#bbMultiplier').val(2); + $('#bbSource').val('close'); + $('#bbUpperColor').val('#FF6B6B'); + $('#bbMiddleColor').val('#4ECDC4'); + $('#bbLowerColor').val('#45B7D1'); + $('#bbLineWidth').val(1); + $('#bbLineWidthDisplay').text('1'); + $('#bbStyle').val('solid'); + + // 重置确认按钮 + $('#addBollingerBandConfirm').text('添加布林带').removeData('editId'); + + // 显示对话框 + const modal = new bootstrap.Modal(document.getElementById('bollingerBandModal')); + modal.show(); + }); + // 平滑算法变更事件 $('#maSmoothing').change(function() { const smoothing = $(this).val(); @@ -1369,6 +1727,11 @@ $('#lineWidthDisplay').text($(this).val()); }); + // 布林带线宽滑块变更事件 + $('#bbLineWidth').on('input', function() { + $('#bbLineWidthDisplay').text($(this).val()); + }); + // 确认添加/更新均线 $('#addMovingAverageConfirm').click(function() { const form = $('#movingAverageForm')[0]; @@ -1440,6 +1803,101 @@ const modal = bootstrap.Modal.getInstance(document.getElementById('movingAverageModal')); modal.hide(); }); + + // 确认添加/更新布林带 + $('#addBollingerBandConfirm').click(function() { + const editId = $(this).data('editId'); + const config = { + period: parseInt($('#bbPeriod').val()), + multiplier: parseFloat($('#bbMultiplier').val()), + source: $('#bbSource').val(), + upperColor: $('#bbUpperColor').val(), + middleColor: $('#bbMiddleColor').val(), + lowerColor: $('#bbLowerColor').val(), + lineWidth: parseInt($('#bbLineWidth').val()), + style: $('#bbStyle').val() + }; + + if (editId) { + // 更新现有布林带 + const bb = bollingerBands.find(bb => bb.id === editId); + if (bb) { + // 删除旧系列 + if (bb.series.upper && bb.series.middle && bb.series.lower) { + tvWidget.mainChart.removeSeries(bb.series.upper); + tvWidget.mainChart.removeSeries(bb.series.middle); + tvWidget.mainChart.removeSeries(bb.series.lower); + + const removeFromArray = (arr, series) => { + const seriesIndex = arr.indexOf(series); + if (seriesIndex !== -1) { + arr.splice(seriesIndex, 1); + } + }; + + removeFromArray(tvWidget.series.movingAverageSeries, bb.series.upper); + removeFromArray(tvWidget.series.movingAverageSeries, bb.series.middle); + removeFromArray(tvWidget.series.movingAverageSeries, bb.series.lower); + } + + // 更新配置 + Object.assign(bb, config); + + // 重新计算和添加 + const priceData = getPriceData(null, config.source); + const bbData = calculateBollingerBands(priceData, config.period, config.multiplier); + + const upperSeries = tvWidget.mainChart.addLineSeries({ + color: config.upperColor, + lineWidth: config.lineWidth, + lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0, + title: `BOLL上轨(${config.period},${config.multiplier})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + const middleSeries = tvWidget.mainChart.addLineSeries({ + color: config.middleColor, + lineWidth: config.lineWidth, + lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0, + title: `BOLL中轨(${config.period})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + const lowerSeries = tvWidget.mainChart.addLineSeries({ + color: config.lowerColor, + lineWidth: config.lineWidth, + lineStyle: config.style === 'dashed' ? 1 : config.style === 'dotted' ? 3 : 0, + title: `BOLL下轨(${config.period},${config.multiplier})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true + }); + + upperSeries.setData(bbData.upper); + middleSeries.setData(bbData.middle); + lowerSeries.setData(bbData.lower); + + bb.series.upper = upperSeries; + bb.series.middle = middleSeries; + bb.series.lower = lowerSeries; + bb.data = bbData; + + tvWidget.series.movingAverageSeries.push(upperSeries, middleSeries, lowerSeries); + updateMovingAverageIndicators(); + } + } else { + // 添加新布林带 + addBollingerBandToChart(config); + } + + // 关闭对话框 + const modal = bootstrap.Modal.getInstance(document.getElementById('bollingerBandModal')); + modal.hide(); + }); } // 买卖点样式定义 - 根据desc字段直接显示 @@ -2063,6 +2521,16 @@ }); console.log('已清空均线序列引用,配置保留:', movingAverages.length); + // 清空所有布林带的序列引用 + bollingerBands.forEach(bb => { + bb.series = { + upper: null, + middle: null, + lower: null + }; + }); + console.log('已清空布林带序列引用,配置保留:', bollingerBands.length); + tvWidget = { mainChart: null, volumeChart: null, @@ -4385,6 +4853,14 @@ }, 200); } + // 如果有布林带配置,用新数据重新计算和显示布林带 + if (bollingerBands.length > 0) { + console.log('图表重新初始化后,更新布林带数据,布林带数量:', bollingerBands.length); + setTimeout(() => { + updateAllBollingerBands(); + }, 200); + } + // 窗口大小变化时重绘图表 window.addEventListener('resize', () => { // 调整主图大小 @@ -6870,5 +7346,94 @@
+ + + \ No newline at end of file