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分钟',