Skip to content

Commit bf71df1

Browse files
feat(transform): add native financial indicator dataset transform
1 parent d6a812f commit bf71df1

3 files changed

Lines changed: 436 additions & 0 deletions

File tree

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
import {
21+
DataTransformOption, ExternalDataTransform, ExternalDataTransformResultItem
22+
} from '../../data/helper/transform';
23+
import { throwError } from '../../util/log';
24+
import { DimensionLoose } from '../../util/types';
25+
26+
export interface IndicatorTransformOption extends DataTransformOption {
27+
type: 'echarts:indicator';
28+
config: {
29+
indicator?: 'sma' | 'ema' | 'macd' | 'bollinger';
30+
period?: number;
31+
sourceDimension?: DimensionLoose;
32+
// MACD specific
33+
shortPeriod?: number;
34+
longPeriod?: number;
35+
signalPeriod?: number;
36+
};
37+
}
38+
39+
export const indicatorTransform: ExternalDataTransform<IndicatorTransformOption> = {
40+
41+
type: 'echarts:indicator',
42+
43+
transform: function (params) {
44+
const upstream = params.upstream;
45+
const config = params.config || {};
46+
const indicator = config.indicator || 'sma';
47+
const period = config.period || 14;
48+
49+
let sourceDimension = config.sourceDimension;
50+
// Default to the last dimension if not specified
51+
if (sourceDimension == null) {
52+
const dims = upstream.cloneAllDimensionInfo();
53+
sourceDimension = dims.length > 0 ? dims[dims.length - 1].name || dims[dims.length - 1].index : 0;
54+
}
55+
56+
const dimInfo = upstream.getDimensionInfo(sourceDimension);
57+
if (!dimInfo) {
58+
throwError('Can not find dimension info via: ' + sourceDimension);
59+
return { data: [] }; // Prevent ts error
60+
}
61+
62+
const dimIdx = dimInfo.index;
63+
64+
const resultData: any[][] = [];
65+
const upstreamCount = upstream.count();
66+
67+
const upstreamDims = upstream.cloneAllDimensionInfo();
68+
const dimensions: any[] = upstreamDims.map(d => d.name || d.index);
69+
70+
// Prepare dimensions
71+
if (indicator === 'sma' || indicator === 'ema') {
72+
dimensions.push(indicator.toUpperCase());
73+
} else if (indicator === 'bollinger') {
74+
dimensions.push('Upper', 'Lower');
75+
} else if (indicator === 'macd') {
76+
dimensions.push('MACD', 'Signal', 'Histogram');
77+
}
78+
79+
// Retrieve source values
80+
const values: number[] = [];
81+
for (let i = 0; i < upstreamCount; i++) {
82+
const val = upstream.retrieveValue(i, dimIdx);
83+
const num = parseFloat(val as string);
84+
values.push(isNaN(num) ? 0 : num);
85+
}
86+
87+
// Calculate SMA
88+
const sma = (data: number[], p: number) => {
89+
const res: (number | string)[] = [];
90+
let sum = 0;
91+
for (let i = 0; i < data.length; i++) {
92+
sum += data[i];
93+
if (i < p - 1) {
94+
res.push('-');
95+
} else {
96+
res.push(sum / p);
97+
sum -= data[i - p + 1];
98+
}
99+
}
100+
return res;
101+
};
102+
103+
// Calculate EMA
104+
const ema = (data: number[], p: number) => {
105+
const res: (number | string)[] = [];
106+
const k = 2 / (p + 1);
107+
let currentEma = 0;
108+
for (let i = 0; i < data.length; i++) {
109+
if (i < p - 1) {
110+
res.push('-');
111+
currentEma += data[i]; // Accumulate sum for the first p-1 elements
112+
} else if (i === p - 1) {
113+
currentEma += data[i];
114+
currentEma = currentEma / p; // Calculate initial SMA
115+
res.push(currentEma);
116+
} else {
117+
currentEma = (data[i] - currentEma) * k + currentEma;
118+
res.push(currentEma);
119+
}
120+
}
121+
return res;
122+
};
123+
124+
let outputCols: (number|string)[][] = [];
125+
126+
if (indicator === 'sma') {
127+
outputCols.push(sma(values, period));
128+
} else if (indicator === 'ema') {
129+
outputCols.push(ema(values, period));
130+
} else if (indicator === 'bollinger') {
131+
const smaVals = sma(values, period);
132+
const upper: (number | string)[] = [];
133+
const lower: (number | string)[] = [];
134+
for (let i = 0; i < values.length; i++) {
135+
if (i < period - 1) {
136+
upper.push('-');
137+
lower.push('-');
138+
} else {
139+
const mean = smaVals[i] as number;
140+
let sumSq = 0;
141+
for (let j = 0; j < period; j++) {
142+
sumSq += Math.pow(values[i - j] - mean, 2);
143+
}
144+
const stdDev = Math.sqrt(sumSq / period);
145+
upper.push(mean + 2 * stdDev);
146+
lower.push(mean - 2 * stdDev);
147+
}
148+
}
149+
outputCols.push(upper, lower);
150+
} else if (indicator === 'macd') {
151+
const shortP = config.shortPeriod || 12;
152+
const longP = config.longPeriod || 26;
153+
const sigP = config.signalPeriod || 9;
154+
155+
const emaShort = ema(values, shortP);
156+
const emaLong = ema(values, longP);
157+
158+
const macdVals: (number | string)[] = [];
159+
for (let i = 0; i < values.length; i++) {
160+
if (emaShort[i] === '-' || emaLong[i] === '-') {
161+
macdVals.push('-');
162+
} else {
163+
macdVals.push((emaShort[i] as number) - (emaLong[i] as number));
164+
}
165+
}
166+
167+
const validMacd: number[] = [];
168+
let firstValidIdx = -1;
169+
for (let i = 0; i < macdVals.length; i++) {
170+
if (macdVals[i] !== '-') {
171+
if (firstValidIdx === -1) firstValidIdx = i;
172+
validMacd.push(macdVals[i] as number);
173+
}
174+
}
175+
176+
const signalLine = ema(validMacd, sigP);
177+
const finalSignal: (number | string)[] = [];
178+
const histogram: (number | string)[] = [];
179+
180+
for (let i = 0; i < values.length; i++) {
181+
if (i < firstValidIdx) {
182+
finalSignal.push('-');
183+
histogram.push('-');
184+
} else {
185+
const sigVal = signalLine[i - firstValidIdx];
186+
finalSignal.push(sigVal);
187+
if (sigVal === '-' || macdVals[i] === '-') {
188+
histogram.push('-');
189+
} else {
190+
histogram.push((macdVals[i] as number) - (sigVal as number));
191+
}
192+
}
193+
}
194+
outputCols.push(macdVals, finalSignal, histogram);
195+
}
196+
197+
// Assemble rows
198+
for (let i = 0; i < upstreamCount; i++) {
199+
const row: any[] = [];
200+
for (let d = 0; d < upstreamDims.length; d++) {
201+
row.push(upstream.retrieveValue(i, upstreamDims[d].index));
202+
}
203+
// Append indicator columns
204+
for (let c = 0; c < outputCols.length; c++) {
205+
row.push(outputCols[c][i]);
206+
}
207+
resultData.push(row);
208+
}
209+
210+
return {
211+
data: resultData as ExternalDataTransformResultItem['data'],
212+
dimensions: dimensions
213+
};
214+
}
215+
};

src/component/transform/install.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@
2020
import { EChartsExtensionInstallRegisters } from '../../extension';
2121
import {filterTransform} from './filterTransform';
2222
import {sortTransform} from './sortTransform';
23+
import {indicatorTransform} from './indicatorTransform';
2324

2425
export function install(registers: EChartsExtensionInstallRegisters) {
2526
registers.registerTransform(filterTransform);
2627
registers.registerTransform(sortTransform);
28+
registers.registerTransform(indicatorTransform);
2729
}

0 commit comments

Comments
 (0)