From ceacf3ff0399588b71af02724ef0428aee491612 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Mon, 30 Jun 2025 22:29:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=9D=87=E7=BA=BF=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/templates/index.html | 744 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 742 insertions(+), 2 deletions(-) diff --git a/web/templates/index.html b/web/templates/index.html index fadb286..ff7d37d 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -286,6 +286,186 @@ } /* 主次周期元素样式区分 */ + + /* 均线系统样式 */ + .ma-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: 300px; + box-shadow: 0 2px 8px rgba(0,0,0,0.1); + } + + .ma-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 4px 0; + font-size: 12px; + border-bottom: 1px solid #f0f0f0; + } + + .ma-item:last-child { + border-bottom: none; + } + + .ma-info { + display: flex; + align-items: center; + gap: 8px; + } + + .ma-label { + font-weight: 500; + } + + .ma-value { + color: #666; + } + + .ma-actions { + display: flex; + gap: 4px; + } + + .ma-action-btn { + background: none; + border: none; + cursor: pointer; + padding: 2px 4px; + border-radius: 3px; + font-size: 11px; + color: #666; + } + + .ma-action-btn:hover { + background-color: #f0f0f0; + color: #333; + } + + .ma-color-indicator { + width: 12px; + height: 2px; + border-radius: 1px; + margin-right: 4px; + } + + .add-ma-btn { + position: absolute; + top: 10px; + right: 120px; + z-index: 100; + background-color: #0d6efd; + color: white; + border: none; + padding: 8px 12px; + border-radius: 6px; + font-size: 13px; + cursor: pointer; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + + .add-ma-btn:hover { + background-color: #0a58ca; + } + + /* 配置弹窗样式 */ + .ma-config-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0,0,0,0.5); + z-index: 10000; + display: none; + justify-content: center; + align-items: center; + } + + .ma-config-content { + background: white; + border-radius: 8px; + padding: 20px; + width: 400px; + max-width: 90vw; + box-shadow: 0 4px 20px rgba(0,0,0,0.3); + } + + .ma-config-title { + font-size: 16px; + font-weight: 600; + margin-bottom: 20px; + color: #333; + } + + .ma-config-form { + display: grid; + gap: 16px; + } + + .ma-config-group { + display: flex; + flex-direction: column; + gap: 6px; + } + + .ma-config-label { + font-size: 14px; + font-weight: 500; + color: #555; + } + + .ma-config-input, .ma-config-select { + padding: 8px 12px; + border: 1px solid #ddd; + border-radius: 4px; + font-size: 14px; + } + + .ma-config-input:focus, .ma-config-select:focus { + outline: none; + border-color: #0d6efd; + box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.25); + } + + .ma-config-actions { + display: flex; + gap: 12px; + justify-content: flex-end; + margin-top: 20px; + } + + .ma-config-btn { + padding: 8px 16px; + border: none; + border-radius: 4px; + font-size: 14px; + cursor: pointer; + } + + .ma-config-btn-primary { + background-color: #0d6efd; + color: white; + } + + .ma-config-btn-primary:hover { + background-color: #0a58ca; + } + + .ma-config-btn-secondary { + background-color: #6c757d; + color: white; + } + + .ma-config-btn-secondary:hover { + background-color: #5c636a; + } @@ -513,6 +693,12 @@
+ + + +
@@ -1382,7 +1568,8 @@ elementUncompletedZsSeries: [], tradePointSeries: [], mainBollingerSeries: [], - elementBollingerSeries: [] + elementBollingerSeries: [], + maSeries: [] // 添加均线系列数组 }, state: { isInitialized: false, @@ -3387,7 +3574,7 @@ if (fx.fx_strength < 1.0) { // 降低阈值,让更多分型显示 displayText = fx.fx_strength >= 0.8 ? '' : '' // 0.8以上显示点,0.8以下不显示文本 } - displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("1", "").replace("2", "").replace("30", ""); + displayText = fx.fx_type.replace("TOP", "").replace("BOTTOM", "").replace("11", "").replace("21", "").replace("3", ""); // 添加标记配置 const markerConfig = { time: timestamp, @@ -3735,6 +3922,9 @@ tvWidget.macdChart = macdChart; tvWidget.state.isInitialized = true; + // 添加均线到图表 + addMovingAveragesToChart(candles); + // 绑定同步事件 bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, mainChart, volumeChart, atrChart, macdChart, showMacd); @@ -3843,6 +4033,9 @@ tvWidget.series.lineSeries.setData(lineData); } + // 更新均线数据 + addMovingAveragesToChart(candles); + // 更新成交量数据 let volumes = []; if (useElementPeriod && currentData.element_kline_data && Array.isArray(currentData.element_kline_data)) { @@ -6621,6 +6814,499 @@ document.body.removeChild(link); } + // 均线系统全局变量 + let movingAverages = []; // 存储所有均线配置 + let maIdCounter = 0; // 均线ID计数器 + + // 显示均线配置弹窗 + function showMAConfig() { + $('#maConfigModal').css('display', 'flex'); + // 重置表单 + $('#maType').val('SMA'); + $('#maLength').val(20); + $('#maSource').val('close'); + $('#maSmoothType').val('none'); + $('#maSmoothLength').val(3).prop('disabled', true); + $('#maColor').val(getRandomColor()); + } + + // 隐藏均线配置弹窗 + function hideMAConfig() { + $('#maConfigModal').css('display', 'none'); + } + + // 获取随机颜色 + function getRandomColor() { + const colors = ['#2962FF', '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', + '#FECA57', '#48CAE4', '#F38BA8', '#A8DADC', '#F1C0E8']; + return colors[Math.floor(Math.random() * colors.length)]; + } + + // 添加均线 + function addMovingAverage() { + const config = { + id: ++maIdCounter, + type: $('#maType').val(), + length: parseInt($('#maLength').val()), + source: $('#maSource').val(), + smoothType: $('#maSmoothType').val(), + smoothLength: parseInt($('#maSmoothLength').val()), + color: $('#maColor').val(), + visible: true + }; + + // 验证输入 + if (config.length < 1 || config.length > 200) { + alert('均线长度必须在1-200之间'); + return; + } + + if (config.smoothType !== 'none' && (config.smoothLength < 1 || config.smoothLength > 50)) { + alert('平滑长度必须在1-50之间'); + return; + } + + movingAverages.push(config); + hideMAConfig(); + + // 重新绘制图表以包含新均线 + if (typeof updateChart === 'function') { + updateChart(); + } + } + + // 计算均线数据 + function calculateMA(data, type, length, 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++) { + let value; + + switch(type) { + case 'SMA': + value = sourceData.slice(i - length + 1, i + 1) + .reduce((sum, val) => sum + val, 0) / length; + break; + case 'EMA': + if (i === length - 1) { + // 第一个EMA值使用SMA + value = sourceData.slice(0, length) + .reduce((sum, val) => sum + val, 0) / length; + } else { + const multiplier = 2 / (length + 1); + value = sourceData[i] * multiplier + result[result.length - 1].value * (1 - multiplier); + } + break; + case 'WMA': + let weightSum = 0; + let valueSum = 0; + for (let j = 0; j < length; j++) { + const weight = j + 1; + weightSum += weight; + valueSum += sourceData[i - length + 1 + j] * weight; + } + value = valueSum / weightSum; + break; + default: + value = sourceData[i]; + } + + result.push({ + time: data[i].time, + value: value + }); + } + + return result; + } + + // 应用平滑处理 + function applySmoothToMA(maData, smoothType, smoothLength) { + if (smoothType === 'none' || maData.length < smoothLength) { + return maData; + } + + const result = []; + + for (let i = smoothLength - 1; i < maData.length; i++) { + let value; + + switch(smoothType) { + case 'SMA': + value = maData.slice(i - smoothLength + 1, i + 1) + .reduce((sum, item) => sum + item.value, 0) / smoothLength; + break; + case 'EMA': + if (i === smoothLength - 1) { + value = maData.slice(0, smoothLength) + .reduce((sum, item) => sum + item.value, 0) / smoothLength; + } else { + const multiplier = 2 / (smoothLength + 1); + value = maData[i].value * multiplier + result[result.length - 1].value * (1 - multiplier); + } + break; + case 'WMA': + let weightSum = 0; + let valueSum = 0; + for (let j = 0; j < smoothLength; j++) { + const weight = j + 1; + weightSum += weight; + valueSum += maData[i - smoothLength + 1 + j].value * weight; + } + value = valueSum / weightSum; + break; + default: + value = maData[i].value; + } + + result.push({ + time: maData[i].time, + value: value + }); + } + + return result; + } + + // 更新均线面板 + function updateMAPanel() { + const panel = $('#maPanel'); + + if (movingAverages.length === 0) { + panel.hide(); + return; + } + + let html = ''; + let hasVisibleMA = false; + + movingAverages.forEach(ma => { + // 获取最新价格 + const latestValue = ma.data && ma.data.length > 0 ? + ma.data[ma.data.length - 1].value.toFixed(2) : '--'; + + const smoothText = ma.smoothType !== 'none' ? + `, ${ma.smoothType}(${ma.smoothLength})` : ''; + + // 显示所有均线(包括隐藏的),但用不同样式区分 + const itemStyle = ma.visible ? '' : 'opacity: 0.5;'; + const eyeIcon = ma.visible ? 'bi-eye' : 'bi-eye-slash'; + + html += ` +
+
+
+ ${ma.type}(${ma.length})${smoothText} + ${ma.visible ? latestValue : '--'} +
+
+ + + +
+
+ `; + + if (ma.visible) { + hasVisibleMA = true; + } + }); + + if (html.trim() === '') { + panel.hide(); + } else { + panel.html(html); + panel.show(); + } + } + + // 切换均线可见性 + function toggleMAVisibility(maId) { + const ma = movingAverages.find(m => m.id === maId); + if (ma) { + ma.visible = !ma.visible; + + // 如果图表已初始化,直接更新均线显示 + if (tvWidget.mainChart && currentData && currentData.kline_data) { + // 获取K线数据 + const useElementPeriod = $('#elementPeriodKline').is(':checked') && + currentData.element_kline_data && + Array.isArray(currentData.element_kline_data); + + let candles = []; + if (useElementPeriod) { + candles = currentData.element_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), + }; + }); + } else { + 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), + }; + }); + } + + // 重新添加均线到图表 + addMovingAveragesToChart(candles); + } else { + // 如果图表未初始化,重新加载整个图表 + if (typeof updateChart === 'function') { + updateChart(); + } + } + } + } + + // 配置均线 + function configMA(maId) { + const ma = movingAverages.find(m => m.id === maId); + if (ma) { + // 填充表单 + $('#maType').val(ma.type); + $('#maLength').val(ma.length); + $('#maSource').val(ma.source); + $('#maSmoothType').val(ma.smoothType); + $('#maSmoothLength').val(ma.smoothLength); + $('#maColor').val(ma.color); + + // 启用/禁用平滑长度输入 + $('#maSmoothLength').prop('disabled', ma.smoothType === 'none'); + + // 修改按钮为更新模式 + $('.ma-config-btn-primary').text('更新').off('click').on('click', function() { + updateMovingAverage(maId); + }); + + $('#maConfigModal').css('display', 'flex'); + } + } + + // 更新均线 + function updateMovingAverage(maId) { + const ma = movingAverages.find(m => m.id === maId); + if (ma) { + // 验证输入 + const newLength = parseInt($('#maLength').val()); + const newSmoothLength = parseInt($('#maSmoothLength').val()); + const newSmoothType = $('#maSmoothType').val(); + + if (newLength < 1 || newLength > 200) { + alert('均线长度必须在1-200之间'); + return; + } + + if (newSmoothType !== 'none' && (newSmoothLength < 1 || newSmoothLength > 50)) { + alert('平滑长度必须在1-50之间'); + return; + } + + // 更新配置 + ma.type = $('#maType').val(); + ma.length = newLength; + ma.source = $('#maSource').val(); + ma.smoothType = newSmoothType; + ma.smoothLength = newSmoothLength; + ma.color = $('#maColor').val(); + + hideMAConfig(); + + // 恢复按钮为添加模式 + $('.ma-config-btn-primary').text('添加').off('click').on('click', addMovingAverage); + + // 如果图表已初始化,直接更新均线显示 + if (tvWidget.mainChart && currentData && currentData.kline_data) { + const useElementPeriod = $('#elementPeriodKline').is(':checked') && + currentData.element_kline_data && + Array.isArray(currentData.element_kline_data); + + let candles = []; + if (useElementPeriod) { + candles = currentData.element_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), + }; + }); + } else { + 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), + }; + }); + } + + addMovingAveragesToChart(candles); + } else { + if (typeof updateChart === 'function') { + updateChart(); + } + } + } + } + + // 删除均线 + function deleteMA(maId) { + movingAverages = movingAverages.filter(m => m.id !== maId); + + // 如果图表已初始化,直接更新均线显示 + if (tvWidget.mainChart && currentData && currentData.kline_data) { + const useElementPeriod = $('#elementPeriodKline').is(':checked') && + currentData.element_kline_data && + Array.isArray(currentData.element_kline_data); + + let candles = []; + if (useElementPeriod) { + candles = currentData.element_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), + }; + }); + } else { + 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), + }; + }); + } + + addMovingAveragesToChart(candles); + } else { + if (typeof updateChart === 'function') { + updateChart(); + } + } + } + + // 添加均线到图表 + function addMovingAveragesToChart(candleData) { + if (!tvWidget.mainChart || !candleData || candleData.length === 0) { + return; + } + + // 清除现有的均线系列 + if (tvWidget.series.maSeries) { + tvWidget.series.maSeries.forEach(series => { + try { + tvWidget.mainChart.removeSeries(series); + } catch (e) { + console.debug('移除均线系列时出错:', e); + } + }); + } + tvWidget.series.maSeries = []; + + // 为每个均线配置创建系列 + movingAverages.forEach(maConfig => { + if (!maConfig.visible) return; + + try { + // 计算均线数据 + const maData = calculateMA(candleData, maConfig.type, maConfig.length, maConfig.source); + + // 应用平滑处理 + const smoothedData = maConfig.smoothType !== 'none' ? + applySmoothToMA(maData, maConfig.smoothType, maConfig.smoothLength) : + maData; + + // 创建线系列 + const maSeries = tvWidget.mainChart.addLineSeries({ + color: maConfig.color, + lineWidth: 2, + title: `${maConfig.type}(${maConfig.length})`, + lastValueVisible: false, + priceLineVisible: false, + crosshairMarkerVisible: true, + }); + + // 设置数据 + maSeries.setData(smoothedData); + + // 保存数据到配置中以便显示最新值 + maConfig.data = smoothedData; + + // 保存系列引用 + tvWidget.series.maSeries.push(maSeries); + + } catch (e) { + console.error('添加均线时出错:', e, maConfig); + } + }); + + // 更新均线面板 + updateMAPanel(); + } + + // 监听平滑类型变化 + $(document).on('change', '#maSmoothType', function() { + const isNone = $(this).val() === 'none'; + $('#maSmoothLength').prop('disabled', isNone); + }); + + // 点击弹窗外部关闭 + $(document).on('click', '#maConfigModal', function(e) { + if (e.target === this) { + hideMAConfig(); + } + }); + // 页面加载完成后初始化 $(document).ready(function() { // 设置默认的筛选时间(最近7天) @@ -6680,5 +7366,59 @@ }, 2000); }); + + +
+
+
添加均线
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+
\ No newline at end of file