金融 K 线系统 - ECharts 场景化最佳实践
场景描述: 构建专业的金融K线图(蜡烛图),支持技术指标(MA/MACD/KDJ/RSI)、实时数据推送、多周期切换、画线工具等功能,适用于股票、期货、外汇、数字货币等交易场景。
📋 目录
场景概述
典型应用场景
- 股票交易: A股、港股、美股实时行情
- 期货交易: 商品期货、股指期货
- 外汇市场: 主要货币对汇率走势
- 数字货币: BTC、ETH等加密货币
- 衍生品: 期权、权证定价分析
核心挑战
核心功能
K线图基础
OHLC数据结构
typescript
interface CandleData {
date: string; // 日期 '2024-01-01'
open: number; // 开盘价
high: number; // 最高价
low: number; // 最低价
close: number; // 收盘价
volume: number; // 成交量
amount?: number; // 成交额(可选)
}
// 示例数据
const klineData: CandleData[] = [
{ date: '2024-01-01', open: 100, high: 105, low: 98, close: 103, volume: 1000000 },
{ date: '2024-01-02', open: 103, high: 108, low: 102, close: 107, volume: 1200000 },
{ date: '2024-01-03', open: 107, high: 110, low: 105, close: 106, volume: 900000 },
// ... 更多数据
];1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
基础K线图
typescript
// 准备数据(ECharts需要数组格式)
const data = klineData.map(item => [
item.open,
item.close,
item.low,
item.high,
item.volume
]);
const dates = klineData.map(item => item.date);
chart.setOption({
title: {
text: '股票K线图',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross' // 十字光标
},
formatter: (params) => {
const candle = params[0];
const vol = params[1];
return `
<b>${candle.axisValue}</b><br/>
开盘: ${candle.data[1]}<br/>
收盘: ${candle.data[0]}<br/>
最低: ${candle.data[2]}<br/>
最高: ${candle.data[3]}<br/>
成交量: ${vol.data.toLocaleString()}
`;
}
},
legend: {
data: ['日K', '成交量'],
top: 30
},
grid: [
{
left: '10%',
right: '8%',
height: '50%' // K线区域
},
{
left: '10%',
right: '8%',
top: '65%',
height: '15%' // 成交量区域
}
],
xAxis: [
{
type: 'category',
data: dates,
boundaryGap: false,
axisLine: { onZero: false },
splitLine: { show: false },
min: 'dataMin',
max: 'dataMax'
},
{
type: 'category',
gridIndex: 1,
data: dates,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
min: 'dataMin',
max: 'dataMax'
}
],
yAxis: [
{
scale: true, // 不强制包含零刻度
splitArea: {
show: true
}
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
dataZoom: [
{
type: 'inside',
start: 50, // 默认显示后50%的数据
end: 100
},
{
show: true,
type: 'slider',
top: '85%',
start: 50,
end: 100
}
],
series: [
{
name: '日K',
type: 'candlestick',
data: data.map(d => [d[0], d[1], d[2], d[3]]), // [开, 收, 低, 高]
itemStyle: {
color: '#ef232a', // 阳线颜色(红)
color0: '#14b143', // 阴线颜色(绿)
borderColor: '#ef232a',
borderColor0: '#14b143'
}
},
{
name: '成交量',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
data: data.map((d, index) => {
// 根据涨跌设置颜色
const isRising = d[1] >= d[0]; // 收盘 >= 开盘
return {
value: d[4],
itemStyle: {
color: isRising ? '#ef232a' : '#14b143'
}
};
})
}
]
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
技术指标
MA移动平均线
typescript
/**
* 计算移动平均线
* @param data K线数据
* @param period 周期(5/10/20/60)
*/
function calculateMA(data: any[], period: number): (number | '-')[] {
const result: (number | '-')[] = [];
for (let i = 0; i < data.length; i++) {
if (i < period - 1) {
result.push('-'); // 数据不足
continue;
}
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[i - j][1]; // 收盘价
}
result.push(parseFloat((sum / period).toFixed(2)));
}
return result;
}
// 计算多条MA
const ma5Data = calculateMA(data, 5);
const ma10Data = calculateMA(data, 10);
const ma20Data = calculateMA(data, 20);
const ma60Data = calculateMA(data, 60);
// 添加到图表
chart.setOption({
legend: {
data: ['日K', 'MA5', 'MA10', 'MA20', 'MA60', '成交量']
},
series: [
{
name: '日K',
type: 'candlestick',
data: data.map(d => [d[0], d[1], d[2], d[3]])
},
{
name: 'MA5',
type: 'line',
data: ma5Data,
smooth: true,
lineStyle: {
opacity: 0.8,
width: 1,
color: '#f6c659' // 黄色
},
symbol: 'none'
},
{
name: 'MA10',
type: 'line',
data: ma10Data,
smooth: true,
lineStyle: {
opacity: 0.8,
width: 1,
color: '#67cdf2' // 蓝色
},
symbol: 'none'
},
{
name: 'MA20',
type: 'line',
data: ma20Data,
smooth: true,
lineStyle: {
opacity: 0.8,
width: 1,
color: '#ef232a' // 红色
},
symbol: 'none'
},
{
name: 'MA60',
type: 'line',
data: ma60Data,
smooth: true,
lineStyle: {
opacity: 0.8,
width: 1,
color: '#14b143' // 绿色
},
symbol: 'none'
}
]
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
MACD指标
typescript
/**
* 计算MACD指标
* @param data K线数据
* @param fastPeriod 快线周期(12)
* @param slowPeriod 慢线周期(26)
* @param signalPeriod 信号线周期(9)
*/
function calculateMACD(
data: any[],
fastPeriod: number = 12,
slowPeriod: number = 26,
signalPeriod: number = 9
): { dif: number[]; dea: number[]; macd: number[] } {
const closes = data.map(d => d[1]);
// 计算EMA
const emaFast = calculateEMA(closes, fastPeriod);
const emaSlow = calculateEMA(closes, slowPeriod);
// 计算DIF(快线-慢线)
const dif = emaFast.map((fast, i) => {
if (fast === '-' || emaSlow[i] === '-') return 0;
return parseFloat((fast - emaSlow[i]).toFixed(4));
});
// 计算DEA(DIF的EMA)
const dea = calculateEMA(dif.filter(v => v !== 0), signalPeriod);
// 补齐DEA长度
while (dea.length < dif.length) {
dea.unshift(0);
}
// 计算MACD柱状图(DIF - DEA) × 2
const macd = dif.map((d, i) => {
return parseFloat(((d - dea[i]) * 2).toFixed(4));
});
return { dif, dea, macd };
}
function calculateEMA(data: number[], period: number): (number | '-')[] {
const result: (number | '-')[] = [];
let ema = data[0];
const multiplier = 2 / (period + 1);
for (let i = 0; i < data.length; i++) {
if (i < period - 1) {
result.push('-');
continue;
}
if (i === period - 1) {
// 第一个EMA值是简单平均
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[j];
}
ema = sum / period;
} else {
ema = (data[i] - ema) * multiplier + ema;
}
result.push(parseFloat(ema.toFixed(4)));
}
return result;
}
// 使用
const { dif, dea, macd } = calculateMACD(data);
// 添加MACD子图
chart.setOption({
grid: [
{ left: '10%', right: '8%', height: '50%' }, // K线
{ left: '10%', right: '8%', top: '65%', height: '15%' }, // 成交量
{ left: '10%', right: '8%', top: '82%', height: '15%' } // MACD
],
xAxis: [
{ /* K线X轴 */ },
{ /* 成交量X轴 */ },
{
type: 'category',
gridIndex: 2,
data: dates,
boundaryGap: false,
axisLine: { onZero: false },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false }
}
],
yAxis: [
{ /* K线Y轴 */ },
{ /* 成交量Y轴 */ },
{
scale: true,
gridIndex: 2,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
series: [
// ... K线和成交量系列
{
name: 'MACD',
type: 'bar',
xAxisIndex: 2,
yAxisIndex: 2,
data: macd.map(val => ({
value: val,
itemStyle: {
color: val >= 0 ? '#ef232a' : '#14b143'
}
}))
},
{
name: 'DIF',
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: dif,
lineStyle: {
width: 1,
color: '#f6c659'
},
symbol: 'none'
},
{
name: 'DEA',
type: 'line',
xAxisIndex: 2,
yAxisIndex: 2,
data: dea,
lineStyle: {
width: 1,
color: '#67cdf2'
},
symbol: 'none'
}
]
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
其他技术指标
typescript
/**
* KDJ随机指标
*/
function calculateKDJ(data: any[], period: number = 9): { k: number[]; d: number[]; j: number[] } {
const k: number[] = [];
const d: number[] = [];
const j: number[] = [];
for (let i = 0; i < data.length; i++) {
if (i < period - 1) {
k.push(50);
d.push(50);
j.push(50);
continue;
}
// 计算RSV
let lowestLow = Infinity;
let highestHigh = -Infinity;
for (let j = 0; j < period; j++) {
const low = data[i - j][2];
const high = data[i - j][3];
lowestLow = Math.min(lowestLow, low);
highestHigh = Math.max(highestHigh, high);
}
const close = data[i][1];
const rsv = ((close - lowestLow) / (highestHigh - lowestLow)) * 100;
// 计算K、D、J
const prevK = k.length > 0 ? k[k.length - 1] : 50;
const prevD = d.length > 0 ? d[d.length - 1] : 50;
const kVal = (2 / 3) * prevK + (1 / 3) * rsv;
const dVal = (2 / 3) * prevD + (1 / 3) * kVal;
const jVal = 3 * kVal - 2 * dVal;
k.push(parseFloat(kVal.toFixed(2)));
d.push(parseFloat(dVal.toFixed(2)));
j.push(parseFloat(jVal.toFixed(2)));
}
return { k, d, j };
}
/**
* RSI相对强弱指标
*/
function calculateRSI(data: any[], period: number = 14): number[] {
const rsi: number[] = [];
for (let i = 0; i < data.length; i++) {
if (i < period) {
rsi.push(50);
continue;
}
let gainSum = 0;
let lossSum = 0;
for (let j = 0; j < period; j++) {
const change = data[i - j][1] - data[i - j - 1][1];
if (change > 0) {
gainSum += change;
} else {
lossSum -= change;
}
}
const avgGain = gainSum / period;
const avgLoss = lossSum / period;
if (avgLoss === 0) {
rsi.push(100);
} else {
const rs = avgGain / avgLoss;
const rsiVal = 100 - (100 / (1 + rs));
rsi.push(parseFloat(rsiVal.toFixed(2)));
}
}
return rsi;
}
/**
* 布林带BOLL
*/
function calculateBOLL(data: any[], period: number = 20): { upper: number[]; middle: number[]; lower: number[] } {
const upper: number[] = [];
const middle: number[] = [];
const lower: number[] = [];
for (let i = 0; i < data.length; i++) {
if (i < period - 1) {
upper.push('-');
middle.push('-');
lower.push('-');
continue;
}
// 计算中轨(MA)
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[i - j][1];
}
const ma = sum / period;
middle.push(parseFloat(ma.toFixed(2)));
// 计算标准差
let varianceSum = 0;
for (let j = 0; j < period; j++) {
const diff = data[i - j][1] - ma;
varianceSum += diff * diff;
}
const stdDev = Math.sqrt(varianceSum / period);
// 上轨 = 中轨 + 2×标准差
// 下轨 = 中轨 - 2×标准差
upper.push(parseFloat((ma + 2 * stdDev).toFixed(2)));
lower.push(parseFloat((ma - 2 * stdDev).toFixed(2)));
}
return { upper, middle, lower };
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
技术实现
实时数据推送
typescript
class RealTimeKLine {
private chart: echarts.ECharts;
private ws: WebSocket;
private currentData: any[] = [];
private currentBar: CandleData | null = null;
constructor(chart: echarts.ECharts, symbol: string) {
this.chart = chart;
this.ws = new WebSocket(`wss://market.example.com/ws/${symbol}`);
}
connect() {
this.ws.onmessage = (event) => {
const tick = JSON.parse(event.data);
this.handleTick(tick);
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
this.ws.onclose = () => {
console.warn('连接关闭,3秒后重连...');
setTimeout(() => this.connect(), 3000);
};
}
handleTick(tick: any) {
const { timestamp, price, volume } = tick;
const date = new Date(timestamp).toISOString().substring(0, 19);
if (!this.currentBar) {
// 第一笔数据
this.currentBar = {
date,
open: price,
high: price,
low: price,
close: price,
volume
};
return;
}
// 判断是否是新K线(简化:每分钟一根K线)
const isNewBar = !date.startsWith(this.currentBar.date.substring(0, 16));
if (isNewBar) {
// 保存当前K线
this.currentData.push(this.currentBar);
// 限制数据量(保留最近1000根)
if (this.currentData.length > 1000) {
this.currentData.shift();
}
// 开启新K线
this.currentBar = {
date,
open: price,
high: price,
low: price,
close: price,
volume
};
} else {
// 更新当前K线
this.currentBar.high = Math.max(this.currentBar.high, price);
this.currentBar.low = Math.min(this.currentBar.low, price);
this.currentBar.close = price;
this.currentBar.volume += volume;
}
// 更新图表
this.updateChart();
}
updateChart() {
const allData = [...this.currentData];
if (this.currentBar) {
allData.push(this.currentBar);
}
const chartData = allData.map(d => [d.open, d.close, d.low, d.high, d.volume]);
const dates = allData.map(d => d.date);
this.chart.setOption({
xAxis: [{ data: dates }, { data: dates }],
series: [
{ data: chartData.map(d => [d[0], d[1], d[2], d[3]]) },
{ data: chartData.map(d => d[4]) }
]
});
}
}
// 使用
const kline = new RealTimeKLine(chart, 'BTC-USDT');
kline.connect();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
多周期切换
typescript
class MultiTimeframeKLine {
private chart: echarts.ECharts;
private currentTimeframe: string = '1D';
constructor(chart: echarts.ECharts) {
this.chart = chart;
this.initTimeframeSelector();
}
private initTimeframeSelector() {
const timeframes = ['1M', '5M', '15M', '30M', '1H', '4H', '1D', '1W'];
timeframes.forEach(tf => {
const btn = document.createElement('button');
btn.textContent = tf;
btn.className = tf === this.currentTimeframe ? 'active' : '';
btn.onclick = () => this.switchTimeframe(tf);
document.getElementById('timeframe-buttons')?.appendChild(btn);
});
}
async switchTimeframe(timeframe: string) {
console.log(`切换到${timeframe}周期`);
// 更新按钮状态
document.querySelectorAll('#timeframe-buttons button').forEach(btn => {
btn.className = btn.textContent === timeframe ? 'active' : '';
});
this.currentTimeframe = timeframe;
// 加载对应周期的数据
const data = await this.loadKLineData(timeframe);
// 重新渲染
this.render(data);
}
private async loadKLineData(timeframe: string): Promise<any[]> {
const response = await fetch(`/api/kline?symbol=BTC-USDT&interval=${timeframe}&limit=500`);
return response.json();
}
private render(data: any[]) {
const chartData = data.map(d => [d.open, d.close, d.low, d.high]);
const dates = data.map(d => d.date);
this.chart.setOption({
xAxis: [{ data: dates }],
series: [{
type: 'candlestick',
data: chartData
}]
});
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
性能优化
大数据量渲染优化
typescript
// 加载10000+根K线时的优化
chart.setOption({
series: [{
type: 'candlestick',
data: largeDataset,
// ✅ 性能优化配置
progressive: 5000, // 渐进式渲染
progressiveThreshold: 2000, // 超过2000启用
large: true, // 大规模模式
largeThreshold: 1000, // 超过1000启用
// ✅ 简化视觉效果
itemStyle: {
borderWidth: 1 // 减小边框宽度
},
// ✅ 禁用不必要的动画
animation: false
}]
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
内存管理
typescript
class KLineMemoryManager {
private maxBars: number = 1000; // 最多保留1000根K线
private dataBuffer: any[] = [];
addBar(bar: CandleData) {
this.dataBuffer.push(bar);
// 超出限制,移除旧数据
if (this.dataBuffer.length > this.maxBars) {
this.dataBuffer = this.dataBuffer.slice(-this.maxBars);
}
}
getData(): any[] {
return this.dataBuffer;
}
clear() {
this.dataBuffer = [];
}
}
// 使用
const memoryManager = new KLineMemoryManager();
// 实时数据推送时
ws.onmessage = (event) => {
const bar = parseData(event.data);
memoryManager.addBar(bar);
// 只渲染管理器中的数据
chart.setOption({
series: [{ data: memoryManager.getData() }]
});
};1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
完整案例
专业股票交易系统
typescript
class StockTradingSystem {
private chart: echarts.ECharts;
private symbol: string;
private indicators: Set<string> = new Set(['MA']);
constructor(symbol: string) {
this.symbol = symbol;
this.chart = echarts.init(document.getElementById('kline-chart'));
}
async init() {
// 1. 加载历史数据
const historyData = await this.loadHistoryData();
// 2. 初始化图表
this.initChart(historyData);
// 3. 连接实时数据
this.connectRealtimeData();
// 4. 绑定交互事件
this.bindEvents();
console.log('交易系统初始化完成');
}
private initChart(data: any[]) {
const chartData = data.map(d => [d.open, d.close, d.low, d.high]);
const dates = data.map(d => d.date);
const volumes = data.map(d => ({
value: d.volume,
itemStyle: {
color: d.close >= d.open ? '#ef232a' : '#14b143'
}
}));
// 计算技术指标
const ma5 = this.calculateMA(data, 5);
const ma10 = this.calculateMA(data, 10);
const ma20 = this.calculateMA(data, 20);
this.chart.setOption({
backgroundColor: '#0d1117',
title: {
text: `${this.symbol} K线图`,
textStyle: { color: '#c9d1d9' }
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: { color: '#8b949e' }
},
backgroundColor: 'rgba(13, 17, 23, 0.9)',
borderColor: '#30363d',
textStyle: { color: '#c9d1d9' }
},
legend: {
data: ['日K', 'MA5', 'MA10', 'MA20', '成交量'],
textStyle: { color: '#8b949e' },
top: 30
},
grid: [
{ left: '10%', right: '8%', height: '50%' },
{ left: '10%', right: '8%', top: '65%', height: '15%' }
],
xAxis: [
{
type: 'category',
data: dates,
boundaryGap: false,
axisLine: { lineStyle: { color: '#30363d' } },
axisLabel: { color: '#8b949e' },
splitLine: { show: false }
},
{
type: 'category',
gridIndex: 1,
data: dates,
boundaryGap: false,
axisLine: { lineStyle: { color: '#30363d' } },
axisLabel: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
yAxis: [
{
scale: true,
axisLine: { lineStyle: { color: '#30363d' } },
axisLabel: { color: '#8b949e' },
splitLine: { lineStyle: { color: '#21262d' } },
splitArea: { areaStyle: { color: ['rgba(255,255,255,0.02)', 'rgba(255,255,255,0.05)'] } }
},
{
scale: true,
gridIndex: 1,
splitNumber: 2,
axisLabel: { show: false },
axisLine: { show: false },
axisTick: { show: false },
splitLine: { show: false }
}
],
dataZoom: [
{ type: 'inside', start: 50, end: 100 },
{
show: true,
type: 'slider',
top: '85%',
start: 50,
end: 100,
textStyle: { color: '#8b949e' }
}
],
series: [
{
name: '日K',
type: 'candlestick',
data: chartData,
itemStyle: {
color: '#ef232a',
color0: '#14b143',
borderColor: '#ef232a',
borderColor0: '#14b143'
}
},
{
name: 'MA5',
type: 'line',
data: ma5,
smooth: true,
lineStyle: { width: 1, color: '#f6c659' },
symbol: 'none'
},
{
name: 'MA10',
type: 'line',
data: ma10,
smooth: true,
lineStyle: { width: 1, color: '#67cdf2' },
symbol: 'none'
},
{
name: 'MA20',
type: 'line',
data: ma20,
smooth: true,
lineStyle: { width: 1, color: '#ef232a' },
symbol: 'none'
},
{
name: '成交量',
type: 'bar',
xAxisIndex: 1,
yAxisIndex: 1,
data: volumes
}
]
});
}
private connectRealtimeData() {
const ws = new WebSocket(`wss://market.example.com/ws/${this.symbol}`);
ws.onmessage = (event) => {
const tick = JSON.parse(event.data);
this.updateLastCandle(tick);
};
}
private updateLastCandle(tick: any) {
const option = this.chart.getOption();
const lastData = option.series[0].data;
if (lastData && lastData.length > 0) {
const lastIndex = lastData.length - 1;
const lastCandle = lastData[lastIndex];
// 更新最新K线
lastCandle[1] = tick.price; // 收盘价
lastCandle[3] = Math.max(lastCandle[3], tick.price); // 最高价
lastCandle[2] = Math.min(lastCandle[2], tick.price); // 最低价
this.chart.setOption({
series: [{ data: lastData }]
});
}
}
private bindEvents() {
// 十字光标联动
this.chart.on('updateAxisPointer', (event) => {
const axisInfo = event.axesInfo[0];
if (axisInfo) {
this.showCrosshairInfo(axisInfo);
}
});
}
private showCrosshairInfo(axisInfo: any) {
const dataIndex = axisInfo.value;
const option = this.chart.getOption();
const klineData = option.series[0].data[dataIndex];
if (klineData) {
const infoPanel = document.getElementById('info-panel');
if (infoPanel) {
infoPanel.innerHTML = `
<div>开盘: ${klineData[0]}</div>
<div>收盘: ${klineData[1]}</div>
<div>最低: ${klineData[2]}</div>
<div>最高: ${klineData[3]}</div>
`;
}
}
}
private calculateMA(data: any[], period: number): (number | '-')[] {
const result: (number | '-')[] = [];
for (let i = 0; i < data.length; i++) {
if (i < period - 1) {
result.push('-');
continue;
}
let sum = 0;
for (let j = 0; j < period; j++) {
sum += data[i - j][1];
}
result.push(parseFloat((sum / period).toFixed(2)));
}
return result;
}
private async loadHistoryData(): Promise<any[]> {
const response = await fetch(`/api/kline?symbol=${this.symbol}&interval=1D&limit=500`);
return response.json();
}
}
// 启动交易系统
const tradingSystem = new StockTradingSystem('AAPL');
tradingSystem.init();1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
最佳实践总结
🎯 核心技术要点
| 功能 | 关键技术 | 注意事项 |
|---|---|---|
| K线渲染 | candlestick系列 | 数据格式[开,收,低,高] |
| 成交量 | bar系列+颜色区分 | 根据涨跌设置颜色 |
| MA均线 | 自定义计算+line系列 | 多周期组合(5/10/20/60) |
| MACD | EMA算法+子图展示 | 三个子系列(DIF/DEA/MACD) |
| 实时推送 | WebSocket+增量更新 | 控制数据量,及时清理 |
| 多周期 | 动态加载+重新渲染 | 缓存常用周期数据 |
| 大数据量 | progressive+large | 超过1000点启用优化 |
📊 技术指标速查
typescript
// MA(移动平均): trend = sum(close, period) / period
// MACD: DIF = EMA(12) - EMA(26), DEA = EMA(DIF, 9), MACD = (DIF - DEA) × 2
// KDJ: RSV = (C - L9) / (H9 - L9) × 100, K = 2/3×K + 1/3×RSV
// RSI: RSI = 100 - 100/(1 + RS), RS = avgGain / avgLoss
// BOLL: 中轨=MA(20), 上轨=中轨+2×STD, 下轨=中轨-2×STD1
2
3
4
5
2
3
4
5
⚡ 性能优化清单
- [ ] 超过1000根K线开启
large: true - [ ] 超过2000根开启
progressive - [ ] 禁用动画(
animation: false) - [ ] 限制最大数据量(1000-2000根)
- [ ] WebSocket增量更新
- [ ] 定期清理旧数据
- [ ] 多周期数据缓存
🔧 常见问题
Q1: K线颜色不对?
A: 检查数据顺序:
typescript
data: [open, close, low, high] // 必须是这个顺序!1
Q2: 实时数据不更新?
A: 检查是否正确调用:
typescript
chart.setOption({
series: [{ data: newData }]
});1
2
3
2
3
Q3: 缩放卡顿?
A: 启用优化:
typescript
{
progressive: 5000,
large: true,
animation: false
}1
2
3
4
5
2
3
4
5
延伸阅读
- ECharts K线图API
- ECharts 实时数据更新
- 技术分析基础
- WebSocket实时通信
总结: 金融K线系统的核心是数据准确性、实时更新能力和专业的技术指标。通过合理的性能优化,可以实现万级K线的流畅展示和毫秒级数据更新。记住:在金融交易中,每一毫秒的延迟都可能意味着巨大的盈亏,性能和准确性同等重要。
