diff --git a/app/static/index.html b/app/static/index.html index db74328..862c8f5 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -514,7 +514,7 @@ #klineChart { width: 100%; - height: 500px; + height: 650px; } /* Log */ @@ -1881,12 +1881,16 @@ const upColor = '#ef4444'; const downColor = '#10b981'; + // 计算MACD指标 + const macdData = calculateMACD(candles); + const option = { animation: true, legend: { - bottom: 10, + top: 10, left: 'center', - data: ['K线', 'MA5', 'MA10', 'MA20', 'MA60'] + data: ['K线', 'MA5', 'MA10', 'MA20', 'MA60', 'DIF', 'DEA', 'MACD'], + textStyle: { fontSize: 11 } }, tooltip: { trigger: 'axis', @@ -1915,6 +1919,10 @@ result += `
${param.seriesName}: ${param.data}
`; } else if (param.seriesName === '成交量') { result += `
成交量: ${param.data}
`; + } else if (param.seriesName === 'DIF' || param.seriesName === 'DEA') { + result += `
${param.seriesName}: ${param.data}
`; + } else if (param.seriesName === 'MACD') { + result += `
MACD: ${param.data}
`; } }); @@ -1922,8 +1930,9 @@ } }, grid: [ - { left: 60, right: 40, height: '60%' }, - { left: 60, right: 40, top: '75%', height: '15%' } + { left: 60, right: 60, top: 50, height: '48%' }, + { left: 60, right: 60, top: '56%', height: '13%' }, + { left: 60, right: 60, top: '74%', height: '14%' } ], xAxis: [ { @@ -1938,10 +1947,17 @@ data: dates, gridIndex: 1, axisLine: { lineStyle: { color: '#8392A5' } }, + axisLabel: { show: false } + }, + { + type: 'category', + data: dates, + gridIndex: 2, + axisLine: { lineStyle: { color: '#8392A5' } }, axisLabel: { show: true, formatter: function(value) { - return value.substring(5); + return value.substring(11); } } } @@ -1962,23 +1978,33 @@ axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false } + }, + { + scale: true, + gridIndex: 2, + splitNumber: 3, + axisLine: { lineStyle: { color: '#8392A5' } }, + axisLabel: { show: true, fontSize: 11 }, + splitLine: { show: true, lineStyle: { color: '#E8EEF4', type: 'dashed' } } } ], dataZoom: [ { type: 'inside', - xAxisIndex: [0, 1], + xAxisIndex: [0, 1, 2], start: Math.max(0, 100 - 100 * 100 / candles.length), end: 100 }, { show: true, - xAxisIndex: [0, 1], + xAxisIndex: [0, 1, 2], type: 'slider', - bottom: 10, + bottom: 5, start: Math.max(0, 100 - 100 * 100 / candles.length), end: 100, - height: 20 + height: 15, + borderColor: 'transparent', + backgroundColor: '#f1f5f9' } ], series: [ @@ -1998,28 +2024,28 @@ type: 'line', data: calculateMA(candles, 5), smooth: true, - lineStyle: { width: 1 } + lineStyle: { width: 1, color: '#f59e0b' } }, { name: 'MA10', type: 'line', data: calculateMA(candles, 10), smooth: true, - lineStyle: { width: 1 } + lineStyle: { width: 1, color: '#3b82f6' } }, { name: 'MA20', type: 'line', data: calculateMA(candles, 20), smooth: true, - lineStyle: { width: 1 } + lineStyle: { width: 1, color: '#ef4444' } }, { name: 'MA60', type: 'line', data: calculateMA(candles, 60), smooth: true, - lineStyle: { width: 1 } + lineStyle: { width: 1, color: '#8b5cf6' } }, { name: '成交量', @@ -2030,7 +2056,43 @@ return { value: v.value, itemStyle: { - color: v.close >= v.open ? upColor : downColor + color: v.close >= v.open ? upColor : downColor, + opacity: 0.6 + } + }; + }) + }, + { + name: 'DIF', + type: 'line', + xAxisIndex: 2, + yAxisIndex: 2, + data: macdData.dif, + smooth: true, + lineStyle: { width: 1.5, color: '#3b82f6' }, + symbol: 'none' + }, + { + name: 'DEA', + type: 'line', + xAxisIndex: 2, + yAxisIndex: 2, + data: macdData.dea, + smooth: true, + lineStyle: { width: 1.5, color: '#f59e0b' }, + symbol: 'none' + }, + { + name: 'MACD', + type: 'bar', + xAxisIndex: 2, + yAxisIndex: 2, + data: macdData.macd.map((val, idx) => { + return { + value: val, + itemStyle: { + color: val >= 0 ? upColor : downColor, + opacity: 0.7 } }; }) @@ -2061,6 +2123,80 @@ return result; } + function calculateMACD(candles) { + const closes = candles.map(c => c.close); + const ema12 = calculateEMA(closes, 12); + const ema26 = calculateEMA(closes, 26); + + const dif = []; + for (let i = 0; i < closes.length; i++) { + if (ema12[i] !== '-' && ema26[i] !== '-') { + dif.push(parseFloat(ema12[i]) - parseFloat(ema26[i])); + } else { + dif.push(0); + } + } + + const dea = calculateEMARaw(dif, 9); + + const macd = dif.map((d, i) => 2 * (d - dea[i])); + + return { dif, dea, macd }; + } + + function calculateEMA(data, period) { + const result = []; + const multiplier = 2 / (period + 1); + + let sum = 0; + for (let i = 0; i < period - 1 && i < data.length; i++) { + result.push('-'); + sum += data[i]; + } + + if (data.length >= period) { + sum += data[period - 1]; + let ema = sum / period; + result.push(ema.toFixed(2)); + + for (let i = period; i < data.length; i++) { + ema = (data[i] - ema) * multiplier + ema; + result.push(ema.toFixed(2)); + } + } + + return result; + } + + function calculateEMARaw(data, period) { + const result = []; + const multiplier = 2 / (period + 1); + + let sum = 0; + let count = 0; + for (let i = 0; i < data.length; i++) { + if (data[i] === 0 && count < period) { + result.push(0); + continue; + } + if (count < period) { + sum += data[i]; + count++; + if (count === period) { + let ema = sum / period; + result.push(ema); + } else { + result.push(0); + } + } else { + const ema = (data[i] - result[result.length - 1]) * multiplier + result[result.length - 1]; + result.push(ema); + } + } + + return result; + } + function getPeriodLabel(period) { const map = { '5min': '5分钟',