Files

463 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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.终极平滑值));
// 处理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 deltaSums = data.map(item => parseFloat(item.delta累计));
const djValues = data.map(item => parseFloat(item.dj));
const deltaValues = data.map(item => parseFloat(item.delta));
// 新增:处理 delta 累计数据用于标记箭头
const arrowMarks = [];
for (let i = 1; i < deltaSums.length; i++) {
if (deltaSums[i - 1] < 0 && deltaSums[i] > 0) {
// 前一个值小于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) {
// 前一个值大于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'
}
});
}
}
// 配置图表选项
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: '#ef232a',
color0: '#14b143',
borderColor: '#ef232a',
borderColor0: '#14b143'
},
// 新增:添加标记点
markPoint: {
data: arrowMarks
}
},
{
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>