Files

531 lines
19 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.终极平滑值));
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] >= maxDJ * 0.8 && ultimateValues[i] > ma120[i]) {
// 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] <= minDJ * 0.8 && ultimateValues[i] < ma120[i]) {
// 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] >= maxDelta * 0.8 && ultimateValues[i] > ma120[i]) {
// 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] <= minDelta * 0.8 && ultimateValues[i] < ma120[i]) {
// 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>