531 lines
19 KiB
HTML
531 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<meta http-equiv="X-Content-Type-Options" content="nosniff">
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
|
||
<meta http-equiv="Pragma" content="no-cache">
|
||
<meta http-equiv="Expires" content="0">
|
||
<title>实时K线图</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
|
||
<style>
|
||
#kline-chart {
|
||
width: 100%;
|
||
height: 800px;
|
||
margin: 20px auto;
|
||
}
|
||
.symbol-selector {
|
||
margin: 20px;
|
||
text-align: center;
|
||
}
|
||
button {
|
||
margin: 10px;
|
||
padding: 10px;
|
||
cursor: pointer;
|
||
}
|
||
.active-symbol {
|
||
background-color: #e0e0e0;
|
||
}
|
||
body {
|
||
margin: 0;
|
||
padding: 0;
|
||
background-color: #f5f5f5;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="symbol-selector" id="symbol-buttons">
|
||
<!-- 动态生成按钮 -->
|
||
</div>
|
||
<div id="kline-chart"></div>
|
||
|
||
<script>
|
||
let currentSymbol = null;
|
||
const socket = io();
|
||
const symbolButtons = document.getElementById('symbol-buttons');
|
||
let chart = null;
|
||
|
||
// 初始化图表
|
||
function initChart() {
|
||
if (!chart) {
|
||
chart = echarts.init(document.getElementById('kline-chart'));
|
||
}
|
||
}
|
||
|
||
// 初始化数据
|
||
fetch('/api/data')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
updateSymbolButtons(data);
|
||
if (Object.keys(data).length > 0) {
|
||
currentSymbol = Object.keys(data)[0];
|
||
updateChart(data[currentSymbol]);
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error fetching data:', error);
|
||
});
|
||
|
||
// WebSocket事件处理
|
||
socket.on('connect', () => {
|
||
console.log('Connected to server');
|
||
});
|
||
|
||
socket.on('data_update', (data) => {
|
||
updateSymbolButtons(data);
|
||
if (currentSymbol && data[currentSymbol]) {
|
||
updateChart(data[currentSymbol]);
|
||
}
|
||
});
|
||
|
||
function updateSymbolButtons(data) {
|
||
symbolButtons.innerHTML = '';
|
||
Object.keys(data).forEach(symbol => {
|
||
const button = document.createElement('button');
|
||
button.textContent = symbol;
|
||
button.onclick = () => {
|
||
currentSymbol = symbol;
|
||
updateChart(data[symbol]);
|
||
};
|
||
if (symbol === currentSymbol) {
|
||
button.classList.add('active-symbol');
|
||
}
|
||
symbolButtons.appendChild(button);
|
||
});
|
||
}
|
||
|
||
function updateChart(data) {
|
||
initChart();
|
||
|
||
// 准备数据
|
||
const dates = data.map(item => item.datetime);
|
||
const klineData = data.map(item => [
|
||
parseFloat(item.open),
|
||
parseFloat(item.close),
|
||
parseFloat(item.low),
|
||
parseFloat(item.high)
|
||
]);
|
||
const volumes = data.map(item => parseFloat(item.volume));
|
||
const ultimateValues = data.map(item => parseFloat(item.终极平滑值));
|
||
const deltaSums = data.map(item => parseFloat(item.delta累计));
|
||
const djValues = data.map(item => parseFloat(item.dj));
|
||
const deltaValues = data.map(item => parseFloat(item.delta));
|
||
|
||
// 处理POC数据,将缺值替换为前一个有效值
|
||
let pocValues = data.map(item => item.POC);
|
||
let lastValidPoc = null;
|
||
pocValues = pocValues.map(value => {
|
||
if (value === '缺值') {
|
||
return lastValidPoc;
|
||
} else {
|
||
lastValidPoc = parseFloat(value);
|
||
return lastValidPoc;
|
||
}
|
||
});
|
||
|
||
// 计算120日均线
|
||
const closes = data.map(item => parseFloat(item.close));
|
||
const ma120 = calculateMA(closes, 120);
|
||
|
||
// 处理 delta 累计数据,用于标记箭头
|
||
const arrowMarks = [];
|
||
for (let i = 1; i < deltaSums.length; i++) {
|
||
if (deltaSums[i - 1] < 0 && deltaSums[i] > 0 && ultimateValues[i] > ma120[i]) {
|
||
// 前一个值小于0,后一个值大于0,标记向上箭头
|
||
arrowMarks.push({
|
||
coord: [dates[i], data[i].low - 0.1], // 标记在 K 线下方
|
||
symbol: 'path://M0,10 L5,0 L10,10 Z',
|
||
symbolSize: [10, 10],
|
||
symbolOffset: [0, 5],
|
||
itemStyle: {
|
||
color: 'red'
|
||
}
|
||
});
|
||
} else if (deltaSums[i - 1] > 0 && deltaSums[i] < 0 && ultimateValues[i] < ma120[i] ) {
|
||
// 前一个值大于0,后一个值小于0,标记向下箭头
|
||
arrowMarks.push({
|
||
coord: [dates[i], data[i].high + 0.1], // 标记在 K 线上方
|
||
symbol: 'path://M0,0 L5,10 L10,0 Z',
|
||
symbolSize: [10, 10],
|
||
symbolOffset: [0, -5],
|
||
itemStyle: {
|
||
color: 'green'
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 处理 dj 数据,用于标记圆
|
||
const circleMarks = [];
|
||
for (let i = 0; i < djValues.length; i++) {
|
||
let startIndex = Math.max(0, i - 119);
|
||
let recentDJValues = djValues.slice(startIndex, i + 1);
|
||
let maxDJ = Math.max(...recentDJValues);
|
||
let minDJ = Math.min(...recentDJValues);
|
||
if (djValues[i] >= Math.max(maxDJ * 0.8, 8) && ultimateValues[i] > ma120[i]) {
|
||
// * 0.8 dj 大于等于最近120个dj值的最大值的80%,标记向上的红色圆
|
||
circleMarks.push({
|
||
coord: [dates[i], data[i].low - 5.1], // 标记在 K 线下方
|
||
symbol: 'circle',
|
||
symbolSize: 10,
|
||
symbolOffset: [0, 5],
|
||
itemStyle: {
|
||
color: 'red'
|
||
}
|
||
});
|
||
} else if (djValues[i] <= Math.min(minDJ * 0.8,-8) && ultimateValues[i] < ma120[i]) {
|
||
// * 0.8 dj 小于等于最近120个dj值的最小值的80%,标记向下的绿色圆
|
||
circleMarks.push({
|
||
coord: [dates[i], data[i].high + 5.1], // 标记在 K 线上方
|
||
symbol: 'circle',
|
||
symbolSize: 10,
|
||
symbolOffset: [0, -5],
|
||
itemStyle: {
|
||
color: 'green'
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 处理 delta 值数据,用于标记方块
|
||
const squareMarks = [];
|
||
for (let i = 0; i < deltaValues.length; i++) {
|
||
let startIndex = Math.max(0, i - 119);
|
||
let recentDeltaValues = deltaValues.slice(startIndex, i + 1);
|
||
let maxDelta = Math.max(...recentDeltaValues);
|
||
let minDelta = Math.min(...recentDeltaValues);
|
||
if (deltaValues[i] >= Math.max(maxDelta * 0.8, 350) && ultimateValues[i] > ma120[i]) {
|
||
// * 0.8 delta 值大于等于最近120个delta值的最大值的80%,标记向上的红色方块
|
||
squareMarks.push({
|
||
coord: [dates[i], data[i].low - 10.1],
|
||
symbol: 'rect',
|
||
symbolSize: 10,
|
||
symbolOffset: [0, 5],
|
||
itemStyle: {
|
||
color: 'red'
|
||
}
|
||
});
|
||
} else if (deltaValues[i] <= Math.min(minDelta * 0.8,-350) && ultimateValues[i] < ma120[i]) {
|
||
// * 0.8 delta 值小于等于最近120个delta值的最小值的80%,标记向上的绿色方块
|
||
squareMarks.push({
|
||
coord: [dates[i], data[i].high + 10.1],
|
||
symbol: 'rect',
|
||
symbolSize: 10,
|
||
symbolOffset: [0, -5],
|
||
itemStyle: {
|
||
color: 'green'
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
// 合并箭头标记、圆标记和方块标记
|
||
const allMarks = arrowMarks.concat(circleMarks).concat(squareMarks);
|
||
|
||
// 配置图表选项
|
||
const option = {
|
||
title: {
|
||
text: `${currentSymbol} K线图`,
|
||
left: 'center'
|
||
},
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: {
|
||
type: 'cross'
|
||
}
|
||
},
|
||
legend: {
|
||
data: ['K线', '120日均线', '终极平滑值', 'POC', '成交量', 'Delta累计', 'DJ值', 'Delta值'],
|
||
top: 30
|
||
},
|
||
grid: [
|
||
{
|
||
left: '10%',
|
||
right: '8%',
|
||
height: '40%'
|
||
},
|
||
{
|
||
left: '10%',
|
||
right: '8%',
|
||
top: '50%',
|
||
height: '10%'
|
||
},
|
||
{
|
||
left: '10%',
|
||
right: '8%',
|
||
top: '60%',
|
||
height: '10%'
|
||
},
|
||
{
|
||
left: '10%',
|
||
right: '8%',
|
||
top: '70%',
|
||
height: '10%'
|
||
},
|
||
{
|
||
left: '10%',
|
||
right: '8%',
|
||
top: '80%',
|
||
height: '10%'
|
||
}
|
||
],
|
||
xAxis: [
|
||
{
|
||
type: 'category',
|
||
data: dates,
|
||
scale: true,
|
||
boundaryGap: false,
|
||
axisLine: {onZero: false},
|
||
splitLine: {show: false},
|
||
splitNumber: 20,
|
||
gridIndex: 0
|
||
},
|
||
{
|
||
type: 'category',
|
||
gridIndex: 1,
|
||
data: dates,
|
||
axisLabel: {show: false}
|
||
},
|
||
{
|
||
type: 'category',
|
||
gridIndex: 2,
|
||
data: dates,
|
||
axisLabel: {show: false}
|
||
},
|
||
{
|
||
type: 'category',
|
||
gridIndex: 3,
|
||
data: dates,
|
||
axisLabel: {show: false}
|
||
},
|
||
{
|
||
type: 'category',
|
||
gridIndex: 4,
|
||
data: dates,
|
||
axisLabel: {show: true}
|
||
}
|
||
],
|
||
yAxis: [
|
||
{
|
||
scale: true,
|
||
splitArea: {
|
||
show: true
|
||
},
|
||
gridIndex: 0
|
||
},
|
||
{
|
||
scale: true,
|
||
gridIndex: 1,
|
||
splitNumber: 2,
|
||
axisLabel: {show: true},
|
||
axisLine: {show: true},
|
||
splitLine: {show: false}
|
||
},
|
||
{
|
||
scale: true,
|
||
gridIndex: 2,
|
||
splitNumber: 2,
|
||
axisLabel: {show: true},
|
||
axisLine: {show: true},
|
||
splitLine: {show: false}
|
||
},
|
||
{
|
||
scale: true,
|
||
gridIndex: 3,
|
||
splitNumber: 2,
|
||
axisLabel: {show: true},
|
||
axisLine: {show: true},
|
||
splitLine: {show: false}
|
||
},
|
||
{
|
||
scale: true,
|
||
gridIndex: 4,
|
||
splitNumber: 2,
|
||
axisLabel: {show: true},
|
||
axisLine: {show: true},
|
||
splitLine: {show: false}
|
||
}
|
||
],
|
||
dataZoom: [
|
||
{
|
||
type: 'inside',
|
||
xAxisIndex: [0, 1, 2, 3, 4],
|
||
start: 50,
|
||
end: 100
|
||
},
|
||
{
|
||
show: true,
|
||
xAxisIndex: [0, 1, 2, 3, 4],
|
||
type: 'slider',
|
||
bottom: '2%',
|
||
start: 50,
|
||
end: 100
|
||
}
|
||
],
|
||
series: [
|
||
{
|
||
name: 'K线',
|
||
type: 'candlestick',
|
||
data: klineData,
|
||
itemStyle: {
|
||
color: 'none', // 空心 K 线,填充颜色设为无
|
||
color0: 'none',
|
||
borderColor: '#ef232a',
|
||
borderColor0: '#14b143',
|
||
borderWidth: 1
|
||
},
|
||
// 添加标记点
|
||
markPoint: {
|
||
data: allMarks
|
||
}
|
||
},
|
||
{
|
||
name: '120日均线',
|
||
type: 'line',
|
||
data: ma120,
|
||
smooth: true,
|
||
lineStyle: {
|
||
opacity: 0.5
|
||
}
|
||
},
|
||
{
|
||
name: '终极平滑值',
|
||
type: 'line',
|
||
data: ultimateValues,
|
||
smooth: true,
|
||
lineStyle: {
|
||
opacity: 0.5
|
||
}
|
||
},
|
||
{
|
||
name: 'POC',
|
||
type: 'line',
|
||
data: pocValues,
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#FFD700',
|
||
width: 2,
|
||
opacity: 0.8
|
||
},
|
||
symbol: 'circle',
|
||
symbolSize: 6
|
||
},
|
||
{
|
||
name: '成交量',
|
||
type: 'bar',
|
||
xAxisIndex: 1,
|
||
yAxisIndex: 1,
|
||
data: volumes
|
||
},
|
||
{
|
||
name: 'Delta累计',
|
||
type: 'line',
|
||
xAxisIndex: 2,
|
||
yAxisIndex: 2,
|
||
data: deltaSums,
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#4169E1',
|
||
width: 2,
|
||
opacity: 0.8
|
||
},
|
||
markLine: {
|
||
silent: true,
|
||
data: [
|
||
{
|
||
yAxis: 0,
|
||
lineStyle: {
|
||
color: '#999',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
},
|
||
{
|
||
name: 'DJ值',
|
||
type: 'line',
|
||
xAxisIndex: 3,
|
||
yAxisIndex: 3,
|
||
data: djValues,
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#9932CC',
|
||
width: 2,
|
||
opacity: 0.8
|
||
},
|
||
markLine: {
|
||
silent: true,
|
||
data: [
|
||
{
|
||
yAxis: 0,
|
||
lineStyle: {
|
||
color: '#999',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
},
|
||
{
|
||
name: 'Delta值',
|
||
type: 'line',
|
||
xAxisIndex: 4,
|
||
yAxisIndex: 4,
|
||
data: deltaValues,
|
||
smooth: true,
|
||
lineStyle: {
|
||
color: '#FF8C00',
|
||
width: 2,
|
||
opacity: 0.8
|
||
},
|
||
markLine: {
|
||
silent: true,
|
||
data: [
|
||
{
|
||
yAxis: 0,
|
||
lineStyle: {
|
||
color: '#999',
|
||
type: 'dashed'
|
||
}
|
||
}
|
||
]
|
||
}
|
||
}
|
||
]
|
||
};
|
||
|
||
// 使用配置项显示图表
|
||
chart.setOption(option);
|
||
}
|
||
|
||
function calculateMA(data, dayCount) {
|
||
const result = [];
|
||
for (let i = 0, len = data.length; i < len; i++) {
|
||
if (i < dayCount - 1) {
|
||
result.push('-');
|
||
continue;
|
||
}
|
||
let sum = 0;
|
||
for (let j = 0; j < dayCount; j++) {
|
||
sum += data[i - j];
|
||
}
|
||
result.push(+(sum / dayCount).toFixed(2));
|
||
}
|
||
return result;
|
||
}
|
||
|
||
// 响应窗口大小变化
|
||
window.addEventListener('resize', function() {
|
||
if (chart) {
|
||
chart.resize();
|
||
}
|
||
});
|
||
|
||
// 初始化图表
|
||
initChart();
|
||
</script>
|
||
</body>
|
||
</html> |