ohlcv-indicators 2.0.5 → 2.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +52 -43
- package/package.json +1 -1
- package/src/moving-averages/bollingerBands.js +116 -63
- package/src/moving-averages/donchianChannel.js +104 -83
- package/src/moving-averages/ema.js +1 -1
- package/src/moving-averages/macd.js +41 -11
- package/src/moving-averages/relativeVolume.js +16 -12
- package/src/moving-averages/sma.js +1 -1
- package/src/oscillators/rsi.js +2 -2
- package/src/oscillators/volumeOscillator.js +7 -5
- package/src/studies/candleStudies.js +35 -26
- package/src/studies/dateTime.js +8 -9
- package/src/studies/findCrosses.js +1 -1
- package/src/studies/lag.js +14 -1
- package/src/studies/priceVariations.js +5 -4
- package/src/utilities/parsing-utilities.js +157 -93
package/index.js
CHANGED
|
@@ -12,14 +12,15 @@ export default class OHLCV_INDICATORS {
|
|
|
12
12
|
if(!input[0].hasOwnProperty('close')) throw Error('input OHLCV array objects require at least close property: ' + ticker)
|
|
13
13
|
|
|
14
14
|
this.lastComputedIndex = 0
|
|
15
|
-
|
|
16
15
|
this.input = input
|
|
16
|
+
this.inputTypes = {open: '', high: '', low: '', close: '', volume: ''}
|
|
17
17
|
this.priceBased = ['open', 'high', 'low', 'close']
|
|
18
18
|
this.len = input.length
|
|
19
19
|
this.lastIndexReplace = false
|
|
20
20
|
this.instances = {}
|
|
21
21
|
this.crossPairsList = []
|
|
22
22
|
this.verticalOhlcv = {}
|
|
23
|
+
this.horizontalOhlcv = new Array(this.len).fill({})
|
|
23
24
|
|
|
24
25
|
this.inputParams = []
|
|
25
26
|
|
|
@@ -28,59 +29,52 @@ export default class OHLCV_INDICATORS {
|
|
|
28
29
|
correlation
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
this.precisionMultiplier = 0
|
|
32
33
|
this.setIndicatorsFromInputParams = setIndicatorsFromInputParams
|
|
33
34
|
|
|
34
35
|
return this
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
pushToMain({index, key, value})
|
|
39
|
+
{
|
|
40
|
+
this.verticalOhlcv[key][index] = value
|
|
41
|
+
this.horizontalOhlcv[index][key] = value
|
|
42
|
+
}
|
|
37
43
|
|
|
38
|
-
|
|
44
|
+
getDataAsCols(){
|
|
39
45
|
|
|
40
|
-
//getData method returns the last object (row) of the new OHLCV with indicators: {open, high, low, close, rsi_14, bollinger_bands_upper}
|
|
41
46
|
this.compute()
|
|
42
|
-
|
|
43
|
-
const {verticalOhlcv} = this
|
|
44
|
-
const keys = Object.keys(verticalOhlcv);
|
|
45
|
-
const len = verticalOhlcv[keys[0]].length;
|
|
46
|
-
const keysLength = keys.length;
|
|
47
|
-
|
|
48
|
-
// Pre-allocate array to improve memory efficiency
|
|
49
|
-
const result = [];
|
|
50
|
-
|
|
51
|
-
// Iterate over the rows
|
|
52
|
-
for (let i = 0; i < len; i++) {
|
|
53
|
-
const row = {};
|
|
54
|
-
let isInvalidValue = false; // Flag to track if any null value is found
|
|
55
|
-
|
|
56
|
-
// Loop over keys to fill row data
|
|
57
|
-
for (let x = 0; x < keysLength; x++) {
|
|
58
|
-
const header = keys[x];
|
|
59
47
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
row[header] = value
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// If no null values were found, add row to result
|
|
72
|
-
if (!isInvalidValue) {
|
|
73
|
-
result.push(row);
|
|
48
|
+
const {precisionMultiplier} = this
|
|
49
|
+
const verticalClone = {...this.verticalOhlcv}
|
|
50
|
+
|
|
51
|
+
for(const [key, arr] of Object.entries(verticalClone))
|
|
52
|
+
{
|
|
53
|
+
if(this.priceBased.includes(key))
|
|
54
|
+
{
|
|
55
|
+
verticalClone[key] = arr.map(v => v / precisionMultiplier)
|
|
74
56
|
}
|
|
75
57
|
}
|
|
76
|
-
|
|
77
|
-
return
|
|
58
|
+
|
|
59
|
+
return verticalClone
|
|
60
|
+
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getData() {
|
|
64
|
+
|
|
65
|
+
//getData method returns the last object (row) of the new OHLCV with indicators: {open, high, low, close, rsi_14, bollinger_bands_upper}
|
|
66
|
+
this.compute()
|
|
67
|
+
return this.horizontalOhlcv.filter(row =>
|
|
68
|
+
!Object.values(row).some(value => value === undefined || value === null)
|
|
69
|
+
)
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
|
|
81
73
|
getLastValues(){
|
|
82
74
|
|
|
83
|
-
|
|
75
|
+
this.compute()
|
|
76
|
+
|
|
77
|
+
return this.horizontalOhlcv[this.horizontalOhlcv.length - 1]
|
|
84
78
|
|
|
85
79
|
}
|
|
86
80
|
|
|
@@ -222,7 +216,7 @@ export default class OHLCV_INDICATORS {
|
|
|
222
216
|
|
|
223
217
|
return this
|
|
224
218
|
}
|
|
225
|
-
macd(fast = 12, slow = 26, signal = 9) {
|
|
219
|
+
macd(fast = 12, slow = 26, signal = 9, options = {}) {
|
|
226
220
|
|
|
227
221
|
isAlreadyComputed(this)
|
|
228
222
|
|
|
@@ -236,14 +230,20 @@ export default class OHLCV_INDICATORS {
|
|
|
236
230
|
throw new Error('"signal" must be a positive number in macd.');
|
|
237
231
|
}
|
|
238
232
|
|
|
239
|
-
|
|
233
|
+
if(typeof options !== 'object')
|
|
234
|
+
{
|
|
235
|
+
throw new Error('"options" must be an object in macd. eg: {target}');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const {target = 'close'} = options
|
|
239
|
+
|
|
240
|
+
this.inputParams.push({key: 'macd', params: [fast, slow, signal, {target}]})
|
|
240
241
|
|
|
241
242
|
return this
|
|
242
243
|
|
|
243
244
|
}
|
|
244
245
|
bollingerBands(size = 20, stdDev = 2, options = {}) {
|
|
245
246
|
|
|
246
|
-
|
|
247
247
|
isAlreadyComputed(this)
|
|
248
248
|
|
|
249
249
|
if(!options || typeof options !== 'object')
|
|
@@ -251,7 +251,7 @@ export default class OHLCV_INDICATORS {
|
|
|
251
251
|
throw new Error('"options" must be an object in bollingerBands. eg: {target, height, range}');
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
-
const {target = 'close', height =
|
|
254
|
+
const {target = 'close', height = 0, range = [], zScore = []} = options
|
|
255
255
|
|
|
256
256
|
// Validate size and times
|
|
257
257
|
if (typeof size !== 'number' || size <= 0) {
|
|
@@ -260,8 +260,17 @@ export default class OHLCV_INDICATORS {
|
|
|
260
260
|
if (typeof stdDev !== 'number' || stdDev <= 0) {
|
|
261
261
|
throw new Error('"stdDev" must be a positive number in bollingerBands.');
|
|
262
262
|
}
|
|
263
|
+
if (!Array.isArray(range)) {
|
|
264
|
+
throw new Error('If set, "range" must be a array of column names in bollingerBands.');
|
|
265
|
+
}
|
|
266
|
+
if (!Array.isArray(zScore)) {
|
|
267
|
+
throw new Error('If set, "zScore" must be a array of column names in bollingerBands.');
|
|
268
|
+
}
|
|
269
|
+
if (typeof height !== 'number' || height < size) {
|
|
270
|
+
throw new Error('"height" must be a number greater than or equal to "size" in bollingerBands.');
|
|
271
|
+
}
|
|
263
272
|
|
|
264
|
-
this.inputParams.push({key: 'bollingerBands', params: [size, stdDev, {target, height, range}]});
|
|
273
|
+
this.inputParams.push({key: 'bollingerBands', params: [size, stdDev, {target, height, range, zScore}]});
|
|
265
274
|
this.priceBased.push('bollinger_bands_middle', 'bollinger_bands_upper', 'bollinger_bands_lower');
|
|
266
275
|
|
|
267
276
|
return this;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ohlcv-indicators",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.6",
|
|
4
4
|
"description": "A JavaScript library featuring optimized trading indicators for OHLCV arrays [{open, high, low, close, value}]. This library is optimized for backtesting and trading bot development.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -1,79 +1,132 @@
|
|
|
1
|
-
import {FasterBollingerBands, FasterSMA} from 'trading-signals';
|
|
1
|
+
import { FasterBollingerBands, FasterSMA } from 'trading-signals';
|
|
2
2
|
import { classifySize } from '../utilities/classification.js';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
export const bollingerBands = (main, index, size, times, options) => {
|
|
7
|
-
|
|
8
|
-
const {height, range, target} = options
|
|
9
|
-
let suffix = (target === defaultTarget) ? '' : `_${target}`
|
|
10
|
-
|
|
11
|
-
if (index === 0) {
|
|
12
|
-
|
|
13
|
-
if(!main.verticalOhlcv.hasOwnProperty(target))
|
|
14
|
-
{
|
|
15
|
-
throw new Error(`bollingerBands could not find target "${target}"`)
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
Object.assign(main.instances, {
|
|
19
|
-
[`bollinger_bands${suffix}`]: {
|
|
20
|
-
instance: new FasterBollingerBands(size, times),
|
|
21
|
-
heightInstance: height ? new FasterSMA(size) : null
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
Object.assign(main.verticalOhlcv, {
|
|
26
|
-
[`bollinger_bands_upper${suffix}`]: [...main.nullArray],
|
|
27
|
-
[`bollinger_bands_middle${suffix}`]: [...main.nullArray],
|
|
28
|
-
[`bollinger_bands_lower${suffix}`]: [...main.nullArray],
|
|
29
|
-
...(height && {
|
|
30
|
-
[`bollinger_bands_height${suffix}`]: [...main.nullArray]
|
|
31
|
-
}),
|
|
32
|
-
...(range && {
|
|
33
|
-
[`bollinger_bands_range${suffix}`]: [...main.nullArray],
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
}
|
|
4
|
+
const defaultTarget = 'close';
|
|
37
5
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
6
|
+
export const bollingerBands = (main, index, size, stdDev, options) => {
|
|
7
|
+
// Destructure options; ensure range and zScore default to empty arrays if not provided.
|
|
8
|
+
const { height, range = [], zScore = [], target } = options;
|
|
9
|
+
const suffix = target === defaultTarget ? '' : `_${target}`;
|
|
10
|
+
const indicatorKey = `${size}_${stdDev}${suffix}`;
|
|
11
|
+
let prefix; // Will be computed during initialization
|
|
42
12
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//do nothing
|
|
13
|
+
// Initialize indicator instances and output arrays on the first call.
|
|
14
|
+
if (index === 0) {
|
|
15
|
+
if (!(target in main.verticalOhlcv)) {
|
|
16
|
+
throw new Error(`bollingerBands could not find target "${target}"`);
|
|
48
17
|
}
|
|
49
18
|
|
|
50
|
-
|
|
19
|
+
const numberOfIndicators = main.inputParams.filter(o => o.key === 'bollingerBands').length;
|
|
20
|
+
// Choose a prefix based on the number of indicators.
|
|
21
|
+
prefix = numberOfIndicators > 1
|
|
22
|
+
? `bollinger_bands_${indicatorKey}`
|
|
23
|
+
: `bollinger_bands${suffix}`;
|
|
24
|
+
|
|
25
|
+
// Ensure the container exists.
|
|
26
|
+
if (!main.instances.bollinger_bands) {
|
|
27
|
+
main.instances.bollinger_bands = {
|
|
28
|
+
numberOfIndicators,
|
|
29
|
+
settings: {}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
51
32
|
|
|
52
|
-
|
|
53
|
-
main.
|
|
54
|
-
|
|
55
|
-
|
|
33
|
+
// Create the Bollinger Bands (and optional height) indicator instance.
|
|
34
|
+
main.instances.bollinger_bands.settings[indicatorKey] = {
|
|
35
|
+
instance: new FasterBollingerBands(size, stdDev),
|
|
36
|
+
heightInstance: height >= size ? new FasterSMA(height) : null
|
|
37
|
+
};
|
|
56
38
|
|
|
57
|
-
|
|
39
|
+
// Build the base output arrays.
|
|
40
|
+
const ohlcvSetup = {
|
|
41
|
+
[`${prefix}_upper`]: [...main.nullArray],
|
|
42
|
+
[`${prefix}_middle`]: [...main.nullArray],
|
|
43
|
+
[`${prefix}_lower`]: [...main.nullArray],
|
|
44
|
+
...(height >= size && { [`${prefix}_height`]: [...main.nullArray] })
|
|
45
|
+
};
|
|
58
46
|
|
|
59
|
-
let heightMean;
|
|
60
|
-
const rangeValue = (value - lower) / (upper - lower);
|
|
61
|
-
const heightValue = upper - lower;
|
|
62
47
|
|
|
63
|
-
heightInstance.update(heightValue, main.lastIndexReplace);
|
|
64
48
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
49
|
+
|
|
50
|
+
// Set up additional arrays for each range property.
|
|
51
|
+
for (const rangeKey of range) {
|
|
52
|
+
if (!(rangeKey in main.verticalOhlcv) || !main.priceBased.includes(rangeKey)) {
|
|
53
|
+
throw new Error(`Invalid range item value "${rangeKey}" property for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(main.priceBased)}`);
|
|
54
|
+
}
|
|
55
|
+
ohlcvSetup[`${prefix}_range_${rangeKey}`] = [...main.nullArray];
|
|
69
56
|
}
|
|
70
57
|
|
|
71
|
-
|
|
58
|
+
// Set up additional arrays for each zScore property.
|
|
59
|
+
for (const zScoreKey of zScore) {
|
|
60
|
+
if (!(zScoreKey in main.verticalOhlcv) || !main.priceBased.includes(zScoreKey)) {
|
|
61
|
+
throw new Error(`Invalid zScore item value "${zScoreKey}" for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(main.priceBased)}`);
|
|
62
|
+
}
|
|
63
|
+
ohlcvSetup[`${prefix}_zscore_${zScoreKey}`] = [...main.nullArray];
|
|
64
|
+
}
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
66
|
+
// Merge the new arrays into verticalOhlcv.
|
|
67
|
+
Object.assign(main.verticalOhlcv, ohlcvSetup);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// For subsequent calls, if prefix wasn’t computed (i.e. index > 0), derive it.
|
|
71
|
+
if (!prefix) {
|
|
72
|
+
const num = main.instances.bollinger_bands.numberOfIndicators;
|
|
73
|
+
prefix = num > 1 ? `bollinger_bands_${indicatorKey}` : `bollinger_bands${suffix}`;
|
|
74
|
+
}
|
|
75
|
+
// Use the same prefix for output keys.
|
|
76
|
+
const subPrefix = prefix;
|
|
77
|
+
|
|
78
|
+
// Retrieve the indicator instance(s) and update with the current value.
|
|
79
|
+
const { instance, heightInstance } = main.instances.bollinger_bands.settings[indicatorKey];
|
|
80
|
+
const value = main.verticalOhlcv[target][index];
|
|
81
|
+
instance.update(value, main.lastIndexReplace);
|
|
82
|
+
|
|
83
|
+
let result;
|
|
84
|
+
try {
|
|
85
|
+
result = instance.getResult();
|
|
86
|
+
} catch (err) {
|
|
87
|
+
// If the result is not available yet, simply exit.
|
|
88
|
+
}
|
|
89
|
+
if (!result) return true;
|
|
90
|
+
|
|
91
|
+
const { upper, middle, lower } = result;
|
|
92
|
+
main.pushToMain({index, key: `${subPrefix}_upper`, value: upper})
|
|
93
|
+
main.pushToMain({index, key: `${subPrefix}_middle`, value: middle})
|
|
94
|
+
main.pushToMain({index, key: `${subPrefix}_lower`, value: lower})
|
|
95
|
+
|
|
96
|
+
// Exit early if neither height nor range are requested.
|
|
97
|
+
if (!height && !range) return true;
|
|
98
|
+
|
|
99
|
+
// Process height if a height instance exists.
|
|
100
|
+
if (heightInstance) {
|
|
101
|
+
const heightValue = upper - lower;
|
|
102
|
+
heightInstance.update(heightValue, main.lastIndexReplace);
|
|
103
|
+
let heightMean;
|
|
104
|
+
try {
|
|
105
|
+
heightMean = heightInstance.getResult();
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Swallow error if the height result is not ready.
|
|
108
|
+
}
|
|
109
|
+
if (heightMean) {
|
|
110
|
+
main.pushToMain({index, key: `${subPrefix}_height`, value: classifySize(heightValue, heightMean, 0.5, 7)})
|
|
76
111
|
}
|
|
77
|
-
|
|
78
|
-
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Process each range property.
|
|
115
|
+
for (const rangeKey of range) {
|
|
116
|
+
const rangeValue = (main.verticalOhlcv[rangeKey][index] - lower) / (upper - lower);
|
|
117
|
+
main.pushToMain({index, key: `${subPrefix}_range_${rangeKey}`, value: rangeValue})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Process each zScore property.
|
|
121
|
+
for (const zScoreKey of zScore) {
|
|
122
|
+
const denominator = upper - lower;
|
|
123
|
+
const zScoreValue = denominator !== 0
|
|
124
|
+
? (2 * stdDev * (main.verticalOhlcv[zScoreKey][index] - middle)) / denominator
|
|
125
|
+
: 0;
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
main.pushToMain({index, key: `${subPrefix}_zscore_${zScoreKey}`, value: zScoreValue})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return true;
|
|
79
132
|
};
|
|
@@ -1,91 +1,112 @@
|
|
|
1
|
-
import {FasterSMA} from 'trading-signals'
|
|
2
|
-
import { classifySize } from '../utilities/classification.js'
|
|
3
|
-
|
|
1
|
+
import { FasterSMA } from 'trading-signals';
|
|
2
|
+
import { classifySize } from '../utilities/classification.js';
|
|
4
3
|
|
|
5
4
|
export const donchianChannels = (main, index, size, offset, options) => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
})
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
Object.assign(main.instances, {
|
|
28
|
-
['donchian_channel']: {
|
|
29
|
-
heightInstance: height ? new FasterSMA((typeof height === 'number' && height > 0) ? height : size) : null
|
|
30
|
-
}
|
|
31
|
-
})
|
|
5
|
+
const { height, range } = options;
|
|
6
|
+
const indicatorKey = `${size}_${offset}`;
|
|
7
|
+
|
|
8
|
+
// Initialization: create output arrays and indicator instance on the first call.
|
|
9
|
+
if (index === 0) {
|
|
10
|
+
const numberOfIndicators = main.inputParams.filter(o => o.key === 'donchianChannels').length;
|
|
11
|
+
const prefix = numberOfIndicators > 1 ? `donchian_channel_${indicatorKey}` : 'donchian_channel';
|
|
12
|
+
|
|
13
|
+
Object.assign(main.verticalOhlcv, {
|
|
14
|
+
[`${prefix}_upper`]: [...main.nullArray],
|
|
15
|
+
[`${prefix}_basis`]: [...main.nullArray],
|
|
16
|
+
[`${prefix}_lower`]: [...main.nullArray],
|
|
17
|
+
...(height && { [`${prefix}_height`]: [...main.nullArray] }),
|
|
18
|
+
...(range && { [`${prefix}_range`]: [...main.nullArray] })
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!main.instances.hasOwnProperty('donchian_channel')) {
|
|
22
|
+
main.instances.donchian_channel = { numberOfIndicators, settings: {} };
|
|
32
23
|
}
|
|
33
24
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
25
|
+
// In addition to your heightInstance, also initialize deques for highs and lows.
|
|
26
|
+
main.instances.donchian_channel.settings[indicatorKey] = {
|
|
27
|
+
heightInstance: height
|
|
28
|
+
? new FasterSMA(typeof height === 'number' && height > 0 ? height : size)
|
|
29
|
+
: null,
|
|
30
|
+
maxDeque: [], // will hold indices for highs in descending order
|
|
31
|
+
minDeque: [] // will hold indices for lows in ascending order
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const numberOfIndicators = main.instances.donchian_channel.numberOfIndicators;
|
|
36
|
+
const subPrefix = numberOfIndicators > 1 ? `donchian_channel_${indicatorKey}` : 'donchian_channel';
|
|
37
|
+
const state = main.instances.donchian_channel.settings[indicatorKey];
|
|
38
|
+
const { heightInstance, maxDeque, minDeque } = state;
|
|
39
|
+
|
|
40
|
+
// Compute the “current bar index” for the window.
|
|
41
|
+
// Note: We use index - offset so that the window is built on the proper part of the array.
|
|
42
|
+
const currentBarIdx = index - offset;
|
|
43
|
+
const endIdx = currentBarIdx + 1; // currentBarIdx is inclusive
|
|
44
|
+
const startIdx = endIdx - size; // window: [startIdx, endIdx)
|
|
45
|
+
|
|
46
|
+
// If the window is not fully available, exit early.
|
|
47
|
+
if (startIdx < 0 || endIdx > main.len) return true;
|
|
48
|
+
|
|
49
|
+
const highs = main.verticalOhlcv.high;
|
|
50
|
+
const lows = main.verticalOhlcv.low;
|
|
51
|
+
|
|
52
|
+
// **Update the maximum deque:**
|
|
53
|
+
// Remove indices that are out of the current window.
|
|
54
|
+
while (maxDeque.length && maxDeque[0] < startIdx) {
|
|
55
|
+
maxDeque.shift();
|
|
56
|
+
}
|
|
57
|
+
// Remove indices whose corresponding high value is less than or equal to the current high.
|
|
58
|
+
while (maxDeque.length && highs[maxDeque[maxDeque.length - 1]] <= highs[currentBarIdx]) {
|
|
59
|
+
maxDeque.pop();
|
|
60
|
+
}
|
|
61
|
+
// Add the current index.
|
|
62
|
+
maxDeque.push(currentBarIdx);
|
|
63
|
+
|
|
64
|
+
// **Update the minimum deque:**
|
|
65
|
+
// Remove indices that are out of the current window.
|
|
66
|
+
while (minDeque.length && minDeque[0] < startIdx) {
|
|
67
|
+
minDeque.shift();
|
|
68
|
+
}
|
|
69
|
+
// Remove indices whose corresponding low value is greater than or equal to the current low.
|
|
70
|
+
while (minDeque.length && lows[minDeque[minDeque.length - 1]] >= lows[currentBarIdx]) {
|
|
71
|
+
minDeque.pop();
|
|
72
|
+
}
|
|
73
|
+
// Add the current index.
|
|
74
|
+
minDeque.push(currentBarIdx);
|
|
75
|
+
|
|
76
|
+
// Now the maximum high is at the front of maxDeque and the minimum low is at the front of minDeque.
|
|
77
|
+
const upper = highs[maxDeque[0]];
|
|
78
|
+
const lower = lows[minDeque[0]];
|
|
79
|
+
const basis = (upper + lower) / 2;
|
|
80
|
+
const close = main.verticalOhlcv.close[index];
|
|
81
|
+
const rangeValue = (close - lower) / (upper - lower);
|
|
82
|
+
const heightValue = upper - lower;
|
|
83
|
+
|
|
84
|
+
// Update the height indicator if requested.
|
|
85
|
+
if (height && heightInstance) {
|
|
86
|
+
heightInstance.update(heightValue, main.lastIndexReplace);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Update the computed values in the output arrays.
|
|
90
|
+
|
|
91
|
+
main.pushToMain({index, key: `${subPrefix}_upper`, value: upper})
|
|
92
|
+
main.pushToMain({index, key: `${subPrefix}_basis`, value: basis})
|
|
93
|
+
main.pushToMain({index, key: `${subPrefix}_lower`, value: lower})
|
|
94
|
+
|
|
95
|
+
if (range) {
|
|
96
|
+
main.pushToMain({index, key: `${subPrefix}_range`, value: rangeValue})
|
|
97
|
+
}
|
|
98
|
+
if (height && heightInstance) {
|
|
99
|
+
let heightMean;
|
|
100
|
+
try {
|
|
101
|
+
heightMean = heightInstance.getResult();
|
|
102
|
+
} catch (err) {
|
|
103
|
+
// Ignore errors if the result isn't ready.
|
|
58
104
|
}
|
|
105
|
+
if (heightMean) {
|
|
59
106
|
|
|
60
|
-
|
|
61
|
-
const placementIndex = index
|
|
62
|
-
|
|
63
|
-
if (placementIndex >= 0 && placementIndex < main.len) {
|
|
64
|
-
|
|
65
|
-
main.verticalOhlcv['donchian_channel_upper'][placementIndex] = upper
|
|
66
|
-
main.verticalOhlcv['donchian_channel_basis'][placementIndex] = basis
|
|
67
|
-
main.verticalOhlcv['donchian_channel_lower'][placementIndex] = lower
|
|
68
|
-
|
|
69
|
-
if(range)
|
|
70
|
-
{
|
|
71
|
-
main.verticalOhlcv['donchian_channel_range'][placementIndex] = rangeValue
|
|
72
|
-
}
|
|
73
|
-
if(height)
|
|
74
|
-
{
|
|
75
|
-
let heightMean
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
heightMean = heightInstance.getResult();
|
|
79
|
-
} catch (err) {
|
|
80
|
-
//do nothing
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if(heightMean)
|
|
84
|
-
{
|
|
85
|
-
main.verticalOhlcv['donchian_channel_height'][placementIndex] = classifySize(heightValue, heightMean, 1.5)
|
|
86
|
-
}
|
|
87
|
-
}
|
|
107
|
+
main.pushToMain({index, key: `${subPrefix}_height`, value: classifySize(heightValue, heightMean, 1.5)})
|
|
88
108
|
}
|
|
109
|
+
}
|
|
89
110
|
|
|
90
|
-
|
|
91
|
-
}
|
|
111
|
+
return true;
|
|
112
|
+
};
|
|
@@ -1,27 +1,56 @@
|
|
|
1
1
|
import {FasterEMA, FasterMACD} from 'trading-signals';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const defaultTarget = 'close'
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
export const macd = (main, index, fast, slow, signal, options) => {
|
|
6
|
+
|
|
7
|
+
const {target} = options
|
|
8
|
+
const suffix = target === defaultTarget ? '' : `_${target}`;
|
|
9
|
+
const indicatorKey = `${fast}_${slow}_${signal}${suffix}`;
|
|
6
10
|
|
|
7
11
|
if (index === 0) {
|
|
8
12
|
|
|
9
|
-
main.
|
|
13
|
+
if(!main.verticalOhlcv.hasOwnProperty(target))
|
|
14
|
+
{
|
|
15
|
+
throw new Error(`Target property ${target} not found in verticalOhlcv for macd.`)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const numberOfIndicators = main.inputParams.filter(o => o.key === 'macd').length;
|
|
19
|
+
const prefix =
|
|
20
|
+
numberOfIndicators > 1
|
|
21
|
+
? `macd_${indicatorKey}`
|
|
22
|
+
: `macd${suffix}`;
|
|
23
|
+
|
|
24
|
+
main.crossPairsList.push({fast: `${prefix}_diff`, slow: `${prefix}_dea`, isDefault: true});
|
|
25
|
+
|
|
26
|
+
if(!main.instances.hasOwnProperty('macd'))
|
|
27
|
+
{
|
|
28
|
+
main.instances.macd = {
|
|
29
|
+
numberOfIndicators,
|
|
30
|
+
settings: {}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
10
33
|
|
|
11
|
-
main.instances[
|
|
34
|
+
main.instances.macd.settings[indicatorKey] = new FasterMACD(
|
|
12
35
|
new FasterEMA(fast),
|
|
13
36
|
new FasterEMA(slow),
|
|
14
37
|
new FasterEMA(signal)
|
|
15
38
|
);
|
|
16
39
|
|
|
17
40
|
Object.assign(main.verticalOhlcv, {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
41
|
+
[`${prefix}_diff`]: [...main.nullArray],
|
|
42
|
+
[`${prefix}_dea`]: [...main.nullArray],
|
|
43
|
+
[`${prefix}_histogram`]: [...main.nullArray],
|
|
21
44
|
})
|
|
22
45
|
}
|
|
23
46
|
|
|
24
|
-
|
|
47
|
+
|
|
48
|
+
const subPrefix = main.instances.macd.numberOfIndicators > 1
|
|
49
|
+
? `macd_${indicatorKey}`
|
|
50
|
+
: `macd${suffix}`;
|
|
51
|
+
|
|
52
|
+
const macdInstance = main.instances.macd.settings[indicatorKey];
|
|
53
|
+
const value = main.verticalOhlcv[target][index]
|
|
25
54
|
macdInstance.update(value, main.lastIndexReplace);
|
|
26
55
|
|
|
27
56
|
let macdResult;
|
|
@@ -34,9 +63,10 @@ export const macd = (main, index, fast, slow, signal) => {
|
|
|
34
63
|
|
|
35
64
|
|
|
36
65
|
if (macdResult) {
|
|
37
|
-
|
|
38
|
-
main.
|
|
39
|
-
main.
|
|
66
|
+
|
|
67
|
+
main.pushToMain({index, key: `${subPrefix}_diff`, value: macdResult.macd})
|
|
68
|
+
main.pushToMain({index, key: `${subPrefix}_dea`, value: macdResult.signal})
|
|
69
|
+
main.pushToMain({index, key: `${subPrefix}_histogram`, value: macdResult.histogram})
|
|
40
70
|
}
|
|
41
71
|
|
|
42
72
|
return true;
|
|
@@ -2,39 +2,43 @@ import {FasterSMA} from 'trading-signals';
|
|
|
2
2
|
|
|
3
3
|
export const relativeVolume = (main, index, size = 10) => {
|
|
4
4
|
|
|
5
|
-
const
|
|
5
|
+
const key = `relative_volume_${size}`
|
|
6
6
|
|
|
7
7
|
if (index === 0) {
|
|
8
|
-
main.instances[
|
|
8
|
+
main.instances[key] = {
|
|
9
9
|
instance: new FasterSMA(size),
|
|
10
10
|
prevRelativeVolumeSma: null
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
Object.assign(main.verticalOhlcv, {
|
|
14
|
-
[
|
|
14
|
+
[key]: [...main.nullArray],
|
|
15
15
|
})
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
|
|
19
|
+
const value = main.verticalOhlcv.volume[index]
|
|
20
|
+
|
|
21
|
+
const smaInstance = main.instances[key].instance
|
|
19
22
|
smaInstance.update(value, main.lastIndexReplace)
|
|
20
23
|
|
|
21
24
|
let smaValue;
|
|
22
25
|
try {
|
|
23
26
|
smaValue = smaInstance.getResult()
|
|
27
|
+
|
|
28
|
+
|
|
24
29
|
} catch (err) {
|
|
25
30
|
//do nothing
|
|
26
31
|
}
|
|
27
32
|
|
|
28
|
-
const {prevRelativeVolumeSma} = main.instances[
|
|
29
|
-
|
|
30
|
-
if (smaValue !== null && prevRelativeVolumeSma !== null) {
|
|
31
|
-
main.verticalOhlcv[`relative_volume_${size}`][index] = value / prevRelativeVolumeSma;
|
|
32
|
-
} else {
|
|
33
|
-
main.verticalOhlcv[`relative_volume_${size}`][index] = null;
|
|
34
|
-
}
|
|
33
|
+
const {prevRelativeVolumeSma} = main.instances[key]
|
|
35
34
|
|
|
35
|
+
main.pushToMain({
|
|
36
|
+
index,
|
|
37
|
+
key,
|
|
38
|
+
value: (smaValue !== null && prevRelativeVolumeSma !== null) ? value / prevRelativeVolumeSma : null
|
|
39
|
+
})
|
|
36
40
|
|
|
37
|
-
main.instances[
|
|
41
|
+
main.instances[key].prevRelativeVolumeSma = smaValue
|
|
38
42
|
|
|
39
43
|
return true;
|
|
40
44
|
};
|
package/src/oscillators/rsi.js
CHANGED
|
@@ -35,7 +35,7 @@ export const rsi = (main, index, size) => {
|
|
|
35
35
|
|
|
36
36
|
if(currentRsi)
|
|
37
37
|
{
|
|
38
|
-
main.
|
|
38
|
+
main.pushToMain({index, key: `rsi_${size}`, value: currentRsi})
|
|
39
39
|
main.instances[`rsi_sma_${size}`].update(currentRsi, main.lastIndexReplace)
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -49,7 +49,7 @@ export const rsi = (main, index, size) => {
|
|
|
49
49
|
|
|
50
50
|
if(smoothedRsi)
|
|
51
51
|
{
|
|
52
|
-
main.
|
|
52
|
+
main.pushToMain({index, key: `rsi_sma_${size}`, value: smoothedRsi})
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
return true
|
|
@@ -3,6 +3,7 @@ import { FasterEMA } from 'trading-signals';
|
|
|
3
3
|
export const volumeOscillator = (main, index, fast, slow) => {
|
|
4
4
|
|
|
5
5
|
const value = main.verticalOhlcv.volume[index]
|
|
6
|
+
const key = 'volume_oscillator'
|
|
6
7
|
|
|
7
8
|
if (index === 0) {
|
|
8
9
|
|
|
@@ -13,11 +14,11 @@ export const volumeOscillator = (main, index, fast, slow) => {
|
|
|
13
14
|
}
|
|
14
15
|
})
|
|
15
16
|
|
|
16
|
-
main.verticalOhlcv[
|
|
17
|
-
main.crossPairsList.push({ fast:
|
|
17
|
+
main.verticalOhlcv[key] = [...main.nullArray];
|
|
18
|
+
main.crossPairsList.push({ fast: key, slow: 0, isDefault: true });
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
const { fastEMA, slowEMA } = main.instances[
|
|
21
|
+
const { fastEMA, slowEMA } = main.instances[key];
|
|
21
22
|
|
|
22
23
|
fastEMA.update(value, main.lastIndexReplace);
|
|
23
24
|
slowEMA.update(value, main.lastIndexReplace);
|
|
@@ -38,9 +39,10 @@ export const volumeOscillator = (main, index, fast, slow) => {
|
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
if (typeof fastValue === 'number' && typeof slowValue === 'number' && slowValue !== 0) {
|
|
41
|
-
|
|
42
|
+
|
|
43
|
+
main.pushToMain({index, key, value: 100 * (fastValue - slowValue) / slowValue})
|
|
42
44
|
} else {
|
|
43
|
-
main.
|
|
45
|
+
main.pushToMain({index, key, value: null})
|
|
44
46
|
}
|
|
45
47
|
|
|
46
48
|
return true;
|
|
@@ -112,45 +112,54 @@ export const candleStudies = (main, index, size, classify = true, classification
|
|
|
112
112
|
// If there's not enough data, means remain null
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
-
main.
|
|
116
|
-
|
|
117
|
-
:
|
|
115
|
+
main.pushToMain({
|
|
116
|
+
index,
|
|
117
|
+
key: 'candle_body_size',
|
|
118
|
+
value: isUp
|
|
119
|
+
? classifySize(candleBodySize, bodySizeMean, 2, changeLevel)
|
|
120
|
+
: -Math.abs(classifySize(candleBodySize, bodySizeMean, 2, changeLevel)) || 0
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
|
|
118
124
|
|
|
119
|
-
main.
|
|
125
|
+
main.pushToMain({index, key: 'candle_gap_size', value: isUp
|
|
120
126
|
? classifySize(gapSize, gapMean, 2, changeLevel)
|
|
121
|
-
: -Math.abs(classifySize(gapSize, gapMean, 2, changeLevel)) || 0
|
|
127
|
+
: -Math.abs(classifySize(gapSize, gapMean, 2, changeLevel)) || 0})
|
|
128
|
+
|
|
122
129
|
|
|
123
|
-
main.
|
|
130
|
+
main.pushToMain({index, key: 'candle_close_direction', value: isUp
|
|
124
131
|
? classifySize(closeDirection, closeDirectionMean, 2, changeLevel)
|
|
125
|
-
: -Math.abs(classifySize(closeDirection, closeDirectionMean, 2, changeLevel)) || 0
|
|
132
|
+
: -Math.abs(classifySize(closeDirection, closeDirectionMean, 2, changeLevel)) || 0})
|
|
126
133
|
|
|
127
|
-
|
|
134
|
+
|
|
135
|
+
main.pushToMain({index, key: 'candle_high_direction', value: isUp
|
|
128
136
|
? classifySize(highDirection, highDirectionMean, 2, changeLevel)
|
|
129
|
-
: -Math.abs(classifySize(highDirection, highDirectionMean, 2, changeLevel)) || 0
|
|
137
|
+
: -Math.abs(classifySize(highDirection, highDirectionMean, 2, changeLevel)) || 0})
|
|
130
138
|
|
|
131
|
-
main.
|
|
139
|
+
main.pushToMain({index, key: 'candle_low_direction', value: isUp
|
|
132
140
|
? classifySize(lowDirection, lowDirectionMean, 2, changeLevel)
|
|
133
|
-
: -Math.abs(classifySize(lowDirection, lowDirectionMean, 2, changeLevel)) || 0
|
|
141
|
+
: -Math.abs(classifySize(lowDirection, lowDirectionMean, 2, changeLevel)) || 0})
|
|
134
142
|
|
|
135
|
-
main.
|
|
143
|
+
main.pushToMain({index, key: 'candle_open_direction', value: isUp
|
|
136
144
|
? classifySize(openDirection, openDirectionMean, 2, changeLevel)
|
|
137
|
-
: -Math.abs(classifySize(openDirection, openDirectionMean, 2, changeLevel)) || 0
|
|
145
|
+
: -Math.abs(classifySize(openDirection, openDirectionMean, 2, changeLevel)) || 0})
|
|
146
|
+
|
|
147
|
+
main.pushToMain({index, key: 'candle_top_size', value: classifySize(topSize, topSizeMean, 0.5, sizeLevel)})
|
|
148
|
+
main.pushToMain({index, key: 'candle_bottom_size', value: classifySize(bottomSize, bottomSizeMean, 0.5, sizeLevel)})
|
|
149
|
+
main.pushToMain({index, key: 'candle_shadow_size', value: classifySize(shadowSize, shadowSizeMean, 0.5, sizeLevel)})
|
|
138
150
|
|
|
139
|
-
main.verticalOhlcv.candle_top_size[index] = classifySize(topSize, topSizeMean, 0.5, sizeLevel);
|
|
140
|
-
main.verticalOhlcv.candle_bottom_size[index] = classifySize(bottomSize, bottomSizeMean, 0.5, sizeLevel);
|
|
141
|
-
main.verticalOhlcv.candle_shadow_size[index] = classifySize(shadowSize, shadowSizeMean, 0.5, sizeLevel);
|
|
142
151
|
} else {
|
|
143
152
|
// Without classification, just store raw values
|
|
144
|
-
|
|
145
|
-
main.
|
|
146
|
-
main.
|
|
147
|
-
main.
|
|
148
|
-
main.
|
|
149
|
-
main.
|
|
150
|
-
|
|
151
|
-
main.
|
|
152
|
-
main.
|
|
153
|
-
main.
|
|
153
|
+
|
|
154
|
+
main.pushToMain({index, key: 'candle_body_size', value: isUp ? candleBodySize : -candleBodySize})
|
|
155
|
+
main.pushToMain({index, key: 'candle_gap_size', value: isUp ? gapSize : -gapSize})
|
|
156
|
+
main.pushToMain({index, key: 'candle_close_direction', value: isUp ? closeDirection : -closeDirection})
|
|
157
|
+
main.pushToMain({index, key: 'candle_high_direction', value: isUp ? highDirection : -highDirection})
|
|
158
|
+
main.pushToMain({index, key: 'candle_low_direction', value: isUp ? lowDirection : -lowDirection})
|
|
159
|
+
main.pushToMain({index, key: 'candle_open_direction', value: isUp ? openDirection : -openDirection})
|
|
160
|
+
main.pushToMain({index, key: 'candle_top_size', value: topSize})
|
|
161
|
+
main.pushToMain({index, key: 'candle_bottom_size', value: bottomSize})
|
|
162
|
+
main.pushToMain({index, key: 'candle_shadow_size', value: shadowSize})
|
|
154
163
|
}
|
|
155
164
|
|
|
156
165
|
return true;
|
package/src/studies/dateTime.js
CHANGED
|
@@ -48,15 +48,14 @@ export const dateTime = (main, index) => {
|
|
|
48
48
|
main.instances.dateTime.sessionIntradayIndexCount = 0
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
main.
|
|
53
|
-
main.
|
|
54
|
-
main.
|
|
55
|
-
main.
|
|
56
|
-
main.
|
|
57
|
-
main.
|
|
58
|
-
main.
|
|
59
|
-
main.verticalOhlcv.hour[index] = hour
|
|
51
|
+
main.pushToMain({index, key: 'session_daily_index', value: main.instances.dateTime.sessionDailyIndexCount})
|
|
52
|
+
main.pushToMain({index, key: 'session_intraday_index', value: main.instances.dateTime.sessionIntradayIndexCount})
|
|
53
|
+
main.pushToMain({index, key: 'day_of_the_week', value: day_of_the_week})
|
|
54
|
+
main.pushToMain({index, key: 'day_of_the_month', value: day_of_the_month})
|
|
55
|
+
main.pushToMain({index, key: 'week_of_the_month', value: week_of_the_month})
|
|
56
|
+
main.pushToMain({index, key: 'month', value: month})
|
|
57
|
+
main.pushToMain({index, key: 'year', value: year})
|
|
58
|
+
main.pushToMain({index, key: 'hour', value: hour})
|
|
60
59
|
|
|
61
60
|
}
|
|
62
61
|
|
|
@@ -188,7 +188,7 @@ export const crossPairs = (main, index) => {
|
|
|
188
188
|
main.instances[crossName].update({fast: fastValue, slow: slowValue})
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
main.
|
|
191
|
+
main.pushToMain({index, key: crossName, value: main.instances[crossName].getResult()})
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
}
|
package/src/studies/lag.js
CHANGED
|
@@ -8,6 +8,19 @@ export const lag = (main, index) => {
|
|
|
8
8
|
|
|
9
9
|
const params = findParams.map(o => o.params)
|
|
10
10
|
|
|
11
|
+
for (const [colKeys, lags] of params)
|
|
12
|
+
{
|
|
13
|
+
for (const colKey of colKeys)
|
|
14
|
+
{
|
|
15
|
+
if(main.priceBased.includes(colKey))
|
|
16
|
+
{
|
|
17
|
+
for (let lag = 1; lag <= lags; lag++) {
|
|
18
|
+
main.priceBased.push(`${colKey}_lag_${lag}`)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
11
24
|
main.instances.lag = {
|
|
12
25
|
lagParams: params
|
|
13
26
|
}
|
|
@@ -33,7 +46,7 @@ export const lag = (main, index) => {
|
|
|
33
46
|
|
|
34
47
|
if(laggedIndex <= 0) break
|
|
35
48
|
|
|
36
|
-
main.
|
|
49
|
+
main.pushToMain({index, key, value: currentColumn[laggedIndex]})
|
|
37
50
|
}
|
|
38
51
|
}
|
|
39
52
|
}
|
|
@@ -9,14 +9,15 @@ export const priceVariations = (main, index) => {
|
|
|
9
9
|
mid_price_open_close: [...main.nullArray],
|
|
10
10
|
mid_price_high_low: [...main.nullArray]
|
|
11
11
|
})
|
|
12
|
+
|
|
13
|
+
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const open = main.verticalOhlcv.open[index]
|
|
15
17
|
const high = main.verticalOhlcv.high[index]
|
|
16
18
|
const low = main.verticalOhlcv.low[index]
|
|
17
19
|
const close = main.verticalOhlcv.close[index]
|
|
18
|
-
|
|
19
|
-
main.
|
|
20
|
-
main.
|
|
21
|
-
|
|
20
|
+
|
|
21
|
+
main.pushToMain({index, key: 'mid_price_open_close', value: (open + close) / 2})
|
|
22
|
+
main.pushToMain({index, key: 'mid_price_high_low', value: (high + low) / 2})
|
|
22
23
|
}
|
|
@@ -1,110 +1,174 @@
|
|
|
1
|
-
import { validateInputParams } from "./validators.js"
|
|
2
|
-
import { rsi } from "../oscillators/rsi.js"
|
|
3
|
-
import { sma } from "../moving-averages/sma.js"
|
|
4
|
-
import { ema } from "../moving-averages/ema.js"
|
|
5
|
-
import { macd } from "../moving-averages/macd.js"
|
|
6
|
-
import { relativeVolume } from "../moving-averages/relativeVolume.js"
|
|
7
|
-
import { donchianChannels } from "../moving-averages/donchianChannel.js"
|
|
8
|
-
import { bollingerBands } from "../moving-averages/bollingerBands.js"
|
|
9
|
-
import { volumeOscillator } from "../oscillators/volumeOscillator.js"
|
|
10
|
-
import { candleStudies } from "../studies/candleStudies.js"
|
|
11
|
-
import { lag } from "../studies/lag.js"
|
|
12
|
-
import {crossPairs} from "../studies/findCrosses.js"
|
|
13
|
-
import { dateTime } from "../studies/dateTime.js"
|
|
14
|
-
import { priceVariations } from "../studies/priceVariations.js"
|
|
15
|
-
|
|
16
|
-
|
|
1
|
+
import { validateInputParams } from "./validators.js";
|
|
2
|
+
import { rsi } from "../oscillators/rsi.js";
|
|
3
|
+
import { sma } from "../moving-averages/sma.js";
|
|
4
|
+
import { ema } from "../moving-averages/ema.js";
|
|
5
|
+
import { macd } from "../moving-averages/macd.js";
|
|
6
|
+
import { relativeVolume } from "../moving-averages/relativeVolume.js";
|
|
7
|
+
import { donchianChannels } from "../moving-averages/donchianChannel.js";
|
|
8
|
+
import { bollingerBands } from "../moving-averages/bollingerBands.js";
|
|
9
|
+
import { volumeOscillator } from "../oscillators/volumeOscillator.js";
|
|
10
|
+
import { candleStudies } from "../studies/candleStudies.js";
|
|
11
|
+
import { lag } from "../studies/lag.js";
|
|
12
|
+
import { crossPairs } from "../studies/findCrosses.js";
|
|
13
|
+
import { dateTime } from "../studies/dateTime.js";
|
|
14
|
+
import { priceVariations } from "../studies/priceVariations.js";
|
|
15
|
+
|
|
16
|
+
// Helper to clean non-numeric characters (except "-" at the start and decimal point)
|
|
17
|
+
const cleanNumStr = str => str.replace(/(?!^-)[^0-9.]/g, '');
|
|
18
|
+
|
|
19
|
+
// Map indicator keys to their respective functions
|
|
17
20
|
const indicatorFunctions = {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
export const parseOhlcvToVertical = (input, main, startIndex = 0) => {
|
|
33
|
-
const { len, inputParams } = main
|
|
34
|
-
|
|
35
|
-
main.nullArray = new Array(len).fill(null)
|
|
36
|
-
|
|
37
|
-
if (startIndex === 0) {
|
|
38
|
-
|
|
39
|
-
validateInputParams(main)
|
|
40
|
-
|
|
41
|
-
const baseKeys = ['open', 'high', 'low', 'close', 'volume'];
|
|
42
|
-
const baseKeysSet = new Set(baseKeys);
|
|
21
|
+
dateTime,
|
|
22
|
+
priceVariations,
|
|
23
|
+
rsi,
|
|
24
|
+
sma,
|
|
25
|
+
ema,
|
|
26
|
+
macd,
|
|
27
|
+
relativeVolume,
|
|
28
|
+
donchianChannels,
|
|
29
|
+
bollingerBands,
|
|
30
|
+
volumeOscillator,
|
|
31
|
+
candleStudies
|
|
32
|
+
};
|
|
43
33
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
// Processes the inputParams into a list of indicator calls
|
|
35
|
+
const processIndicatorCalls = inputParams =>
|
|
36
|
+
Array.isArray(inputParams)
|
|
37
|
+
? inputParams.map(({ key, params }) => ({
|
|
38
|
+
key,
|
|
39
|
+
fn: indicatorFunctions[key],
|
|
40
|
+
args: params
|
|
41
|
+
}))
|
|
42
|
+
: [];
|
|
43
|
+
|
|
44
|
+
// Sets the input types for each key based on the first row of data
|
|
45
|
+
const setInputTypes = (row0, main) => {
|
|
46
|
+
const isValidNumberString = str => /^(\d+(\.\d*)?|\.\d+)$/.test(str);
|
|
47
|
+
|
|
48
|
+
for (const key of Object.keys(main.inputTypes)) {
|
|
49
|
+
if (!(key in row0)) {
|
|
50
|
+
throw new Error(`Property "${key}" not found in ohlcv array. ${main.ticker}`);
|
|
51
|
+
}
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
const otherKeys = [];
|
|
51
|
-
for (let i = 0; i < inputKeys.length; i++) {
|
|
52
|
-
const key = inputKeys[i]
|
|
53
|
-
if (!baseKeysSet.has(key)) {
|
|
54
|
-
main.verticalOhlcv[key] = [...main.nullArray]
|
|
55
|
-
otherKeys.push(key)
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
main.otherKeys = otherKeys;
|
|
53
|
+
let value = row0[key];
|
|
59
54
|
|
|
55
|
+
|
|
60
56
|
|
|
57
|
+
if (typeof value === 'number' && value > 0) {
|
|
58
|
+
main.inputTypes[key] = 'number';
|
|
59
|
+
} else if (typeof value === 'string' && isValidNumberString(value)) {
|
|
60
|
+
main.inputTypes[key] = 'string';
|
|
61
|
+
value = cleanNumStr(value);
|
|
61
62
|
} else {
|
|
62
|
-
|
|
63
|
-
throw new Error("otherKeys not found in main. Ensure you run with startIndex=0 first to initialize.");
|
|
64
|
-
}
|
|
63
|
+
throw new Error(`Invalid property "${key}" in ohlcv array. ${main.ticker}`);
|
|
65
64
|
}
|
|
66
65
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
main.verticalOhlcv.open[x] = current.open;
|
|
74
|
-
main.verticalOhlcv.high[x] = current.high;
|
|
75
|
-
main.verticalOhlcv.low[x] = current.low;
|
|
76
|
-
main.verticalOhlcv.close[x] = current.close;
|
|
77
|
-
main.verticalOhlcv.volume[x] = current.volume;
|
|
78
|
-
|
|
79
|
-
for (let i = 0; i < main.otherKeys.length; i++) {
|
|
80
|
-
const k = main.otherKeys[i];
|
|
81
|
-
main.verticalOhlcv[k][x] = current[k];
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
for (let i = 0; i < indicatorCalls.length; i++) {
|
|
85
|
-
const { fn, args, key } = indicatorCalls[i];
|
|
86
|
-
if (key === 'crossPairs' || key === 'lag') continue;
|
|
87
|
-
|
|
88
|
-
fn(main, x, ...args)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
lag(main, x);
|
|
92
|
-
crossPairs(main, x);
|
|
93
|
-
main.lastComputedIndex++;
|
|
66
|
+
// Determine precisionMultiplier if not already set
|
|
67
|
+
if (main.precisionMultiplier === 0) {
|
|
68
|
+
const [, decimals = ''] = String(value).split('.');
|
|
69
|
+
const decimalPrecision = Math.max(4, decimals.length);
|
|
70
|
+
main.precisionMultiplier = decimalPrecision > 1 ? Math.pow(10, decimalPrecision - 1) : 1;
|
|
94
71
|
}
|
|
72
|
+
}
|
|
95
73
|
};
|
|
96
74
|
|
|
75
|
+
// Parses a number from either a numeric or string input, applying a multiplier to fix precision issues
|
|
76
|
+
const parseNumber = (num, type, precisionMultiplier) => {
|
|
77
|
+
const addMultiplier = cNum =>
|
|
78
|
+
precisionMultiplier > 1 ? cNum * precisionMultiplier : cNum;
|
|
79
|
+
|
|
80
|
+
if (type === 'number') {
|
|
81
|
+
return addMultiplier(num);
|
|
82
|
+
} else if (type === 'string') {
|
|
83
|
+
const cleanedStr = cleanNumStr(num);
|
|
84
|
+
return addMultiplier(Number(cleanedStr));
|
|
85
|
+
} else
|
|
86
|
+
{
|
|
87
|
+
return 0 // Optionally: throw an error if type is not recognized
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const parseVolume = (volume, type) => {
|
|
97
93
|
|
|
98
|
-
|
|
99
|
-
|
|
94
|
+
if(type === 'number') return volume
|
|
95
|
+
else if(type === 'string'){
|
|
96
|
+
return Number(cleanNumStr(volume))
|
|
97
|
+
}
|
|
98
|
+
else
|
|
99
|
+
{
|
|
100
|
+
return 0 // Optionally: throw an error if type is not recognized
|
|
101
|
+
}
|
|
102
|
+
}
|
|
100
103
|
|
|
101
|
-
|
|
104
|
+
export const parseOhlcvToVertical = (input, main, startIndex = 0) => {
|
|
105
|
+
const { len, inputParams } = main;
|
|
106
|
+
main.nullArray = new Array(len).fill(null);
|
|
107
|
+
|
|
108
|
+
const BASE_KEYS = ['open', 'high', 'low', 'close', 'volume'];
|
|
109
|
+
const baseKeysSet = new Set(BASE_KEYS);
|
|
110
|
+
|
|
111
|
+
if (startIndex === 0) {
|
|
112
|
+
// Validate parameters and initialize base keys in verticalOhlcv
|
|
113
|
+
validateInputParams(main);
|
|
114
|
+
BASE_KEYS.forEach(key => {
|
|
115
|
+
main.verticalOhlcv[key] = [...main.nullArray];
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Identify any additional keys from the first input row and initialize them
|
|
119
|
+
const inputKeys = Object.keys(input[0]);
|
|
120
|
+
const otherKeys = inputKeys.filter(key => !baseKeysSet.has(key));
|
|
121
|
+
otherKeys.forEach(key => {
|
|
122
|
+
main.verticalOhlcv[key] = [...main.nullArray];
|
|
123
|
+
});
|
|
124
|
+
main.otherKeys = otherKeys;
|
|
125
|
+
|
|
126
|
+
// Set the input types once using the first row
|
|
127
|
+
setInputTypes(input[0], main);
|
|
128
|
+
} else if (!main.otherKeys) {
|
|
129
|
+
throw new Error(
|
|
130
|
+
"otherKeys not found in main. Ensure you run with startIndex=0 first to initialize."
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Prepare the list of indicator function calls
|
|
135
|
+
const indicatorCalls = processIndicatorCalls(inputParams);
|
|
136
|
+
|
|
137
|
+
// Process each row in the input
|
|
138
|
+
for (let x = startIndex; x < len; x++) {
|
|
139
|
+
const current = input[x];
|
|
140
|
+
// Destructure the base keys and use the rest for other properties
|
|
141
|
+
const { open, high, low, close, volume, ...rest } = current
|
|
142
|
+
const {precisionMultiplier} = main
|
|
143
|
+
|
|
144
|
+
const parsedOpen = parseNumber(open, main.inputTypes.open, precisionMultiplier)
|
|
145
|
+
const parsedHigh = parseNumber(open, main.inputTypes.high, precisionMultiplier)
|
|
146
|
+
const parsedLow = parseNumber(open, main.inputTypes.low, precisionMultiplier)
|
|
147
|
+
const parsedClose = parseNumber(open, main.inputTypes.close, precisionMultiplier)
|
|
148
|
+
const parsedVolume = parseVolume (volume, main.inputTypes.volume)
|
|
149
|
+
|
|
150
|
+
main.pushToMain({index: x, key: 'open', value: parsedOpen})
|
|
151
|
+
main.pushToMain({index: x, key: 'high', value: parsedHigh})
|
|
152
|
+
main.pushToMain({index: x, key: 'low', value: parsedLow})
|
|
153
|
+
main.pushToMain({index: x, key: 'close', value: parsedClose})
|
|
154
|
+
main.pushToMain({index: x, key: 'volume', value: parsedVolume})
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
// Populate any extra keys identified during initialization
|
|
158
|
+
for (const key of main.otherKeys) {
|
|
159
|
+
main.pushToMain({index: x, key, value: rest[key]})
|
|
160
|
+
}
|
|
102
161
|
|
|
103
|
-
|
|
104
|
-
{
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
// Run all indicator functions except for the ones processed later
|
|
163
|
+
for (const { key, fn, args } of indicatorCalls) {
|
|
164
|
+
if (['crossPairs', 'lag'].includes(key)) continue;
|
|
165
|
+
fn(main, x, ...args);
|
|
107
166
|
}
|
|
108
167
|
|
|
109
|
-
|
|
168
|
+
// Process these indicators separately (ensuring their execution order)
|
|
169
|
+
lag(main, x);
|
|
170
|
+
crossPairs(main, x);
|
|
171
|
+
|
|
172
|
+
main.lastComputedIndex++;
|
|
173
|
+
}
|
|
110
174
|
};
|