ohlcv-indicators 3.3.2 → 3.3.5
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/dist/ohlcv-indicators.min.js +1731 -1879
- package/index.js +170 -292
- package/package.json +1 -3
- package/src/core-functions/mainLoop.js +85 -0
- package/src/core-functions/pushToMain.js +15 -0
- package/src/moving-averages/bollingerBands.js +26 -32
- package/src/moving-averages/donchianChannel.js +31 -36
- package/src/moving-averages/macd.js +30 -16
- package/src/moving-averages/movingAverages.js +15 -129
- package/src/moving-averages/relativeVolume.js +20 -14
- package/src/oscillators/rsi.js +21 -25
- package/src/oscillators/volumeOscillator.js +20 -19
- package/src/studies/dateTime.js +22 -20
- package/src/studies/findCrosses.js +17 -17
- package/src/studies/lag.js +22 -13
- package/src/studies/scaler.js +112 -0
- package/src/utilities/assignTypes.js +39 -0
- package/src/utilities/dateUtilities.js +79 -0
- package/src/utilities/numberUtilities.js +51 -10
- package/src/utilities/precisionMultiplier.js +24 -0
- package/src/utilities/validators.js +77 -8
- package/src/utilities/verticalToHorizontal.js +30 -0
- package/src/studies/candleVectors.js +0 -121
- package/src/studies/priceVariations.js +0 -23
- package/src/utilities/classification.js +0 -75
- package/src/utilities/dataParsingUtilities.js +0 -32
- package/src/utilities/parsing-utilities.js +0 -189
- package/src/utilities/pushToMain.js +0 -63
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { validateInputParams } from "../utilities/validators.js";
|
|
2
|
+
import { rsi } from "../oscillators/rsi.js";
|
|
3
|
+
import { movingAverages } from "../moving-averages/movingAverages.js";
|
|
4
|
+
import { macd } from "../moving-averages/macd.js";
|
|
5
|
+
import { relativeVolume } from "../moving-averages/relativeVolume.js";
|
|
6
|
+
import { donchianChannels } from "../moving-averages/donchianChannel.js";
|
|
7
|
+
import { bollingerBands } from "../moving-averages/bollingerBands.js";
|
|
8
|
+
import { volumeOscillator } from "../oscillators/volumeOscillator.js";
|
|
9
|
+
import { lag } from "../studies/lag.js";
|
|
10
|
+
import { crossPairs } from "../studies/findCrosses.js";
|
|
11
|
+
import { dateTime } from "../studies/dateTime.js";
|
|
12
|
+
import { Scaler } from "../studies/scaler.js";
|
|
13
|
+
|
|
14
|
+
import { buildArray } from "../utilities/assignTypes.js";
|
|
15
|
+
import { numberFormater } from "../utilities/numberUtilities.js";
|
|
16
|
+
import { dateFormaters } from "../utilities/dateUtilities.js";
|
|
17
|
+
|
|
18
|
+
// Map indicator keys to their respective functions
|
|
19
|
+
const indicatorFunctions = {
|
|
20
|
+
dateTime,
|
|
21
|
+
rsi,
|
|
22
|
+
ema: movingAverages,
|
|
23
|
+
sma: movingAverages,
|
|
24
|
+
macd,
|
|
25
|
+
relativeVolume,
|
|
26
|
+
donchianChannels,
|
|
27
|
+
bollingerBands,
|
|
28
|
+
volumeOscillator,
|
|
29
|
+
Scaler
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
export const mainLoop = (input, main) => {
|
|
34
|
+
const { len, inputParams, priceBased, precisionMultiplier, arrayTypes, verticalOhlcv, verticalOhlcvKeyNames, inputTypes } = main;
|
|
35
|
+
|
|
36
|
+
validateInputParams({inputParams, len})
|
|
37
|
+
|
|
38
|
+
for(const key of Object.keys(inputTypes))
|
|
39
|
+
{
|
|
40
|
+
verticalOhlcv[key] = buildArray(arrayTypes[key], len)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Process each row in the input
|
|
44
|
+
for (let index = 0; index < len; index++) {
|
|
45
|
+
const curr = input[index]
|
|
46
|
+
|
|
47
|
+
for(const [key, formaterKey] of Object.entries(inputTypes))
|
|
48
|
+
{
|
|
49
|
+
const value = curr[key]
|
|
50
|
+
|
|
51
|
+
if(numberFormater.hasOwnProperty(formaterKey))
|
|
52
|
+
{
|
|
53
|
+
let formatedValue = numberFormater[formaterKey](value)
|
|
54
|
+
|
|
55
|
+
if(precisionMultiplier > 1 && priceBased.has(key))
|
|
56
|
+
{
|
|
57
|
+
formatedValue = formatedValue * precisionMultiplier
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
main.pushToMain({index, key, value: formatedValue})
|
|
61
|
+
}
|
|
62
|
+
else if(dateFormaters.hasOwnProperty(formaterKey))
|
|
63
|
+
{
|
|
64
|
+
main.pushToMain({index, key, value: dateFormaters[formaterKey](value)})
|
|
65
|
+
}
|
|
66
|
+
else
|
|
67
|
+
{
|
|
68
|
+
main.pushToMain({index, key, value})
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Run all indicator functions except for the ones processed later
|
|
73
|
+
for (const { key, params } of inputParams) {
|
|
74
|
+
if (key === 'lag' || key === 'crossPairs') continue;
|
|
75
|
+
// resolve fn on-demand, no per-item object allocation here
|
|
76
|
+
indicatorFunctions[key](main, index, ...params)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Process these indicators separately (ensuring their execution order)
|
|
80
|
+
lag(main, index)
|
|
81
|
+
crossPairs(main, index)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
verticalOhlcvKeyNames.push(...Object.keys(verticalOhlcv))
|
|
85
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const pushToMain = ({main, index, key, value}) => {
|
|
2
|
+
|
|
3
|
+
const {verticalOhlcv} = main
|
|
4
|
+
|
|
5
|
+
if(value === null || typeof value === 'undefined' || Number.isNaN(value))
|
|
6
|
+
{
|
|
7
|
+
main.invalidValueIndex = index
|
|
8
|
+
verticalOhlcv[key][index] = value
|
|
9
|
+
return false
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
verticalOhlcv[key][index] = value
|
|
13
|
+
|
|
14
|
+
return true
|
|
15
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { FasterBollingerBands } from 'trading-signals'
|
|
2
|
-
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
3
2
|
|
|
4
3
|
const defaultTarget = 'close'
|
|
5
4
|
|
|
6
|
-
export const bollingerBands = (main, index, size, stdDev, { height, range = [], target,
|
|
7
|
-
const { verticalOhlcv, instances
|
|
5
|
+
export const bollingerBands = (main, index, size, stdDev, { height, range = [], target, lag }) => {
|
|
6
|
+
const { verticalOhlcv, instances } = main;
|
|
8
7
|
const suffix = target === defaultTarget ? '' : `_${target}`;
|
|
9
8
|
const indicatorKey = `${size}_${stdDev}${suffix}`;
|
|
10
9
|
let prefix;
|
|
@@ -12,7 +11,7 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
12
11
|
// Initialization on the first call.
|
|
13
12
|
if (index === 0) {
|
|
14
13
|
|
|
15
|
-
const { priceBased, inputParams,
|
|
14
|
+
const { priceBased, inputParams, verticalOhlcv, len, arrayTypes } = main;
|
|
16
15
|
|
|
17
16
|
if (!(target in verticalOhlcv)) {
|
|
18
17
|
throw new Error(`bollingerBands could not find target "${target}"`);
|
|
@@ -46,19 +45,14 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
46
45
|
|
|
47
46
|
// Set up additional arrays for each range property.
|
|
48
47
|
for (const rangeKey of range) {
|
|
49
|
-
if (!(rangeKey in verticalOhlcv) || !priceBased.
|
|
48
|
+
if (!(rangeKey in verticalOhlcv) || !priceBased.has(rangeKey)) {
|
|
50
49
|
throw new Error(`Invalid range item value "${rangeKey}" property for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`);
|
|
51
50
|
}
|
|
52
51
|
keyNames.push(`${prefix}_range_${rangeKey}`)
|
|
53
52
|
|
|
54
|
-
if(autoMinMax)
|
|
55
|
-
{
|
|
56
|
-
main.autoMinMaxKeys.push(`${prefix}_range_${rangeKey}`)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
53
|
}
|
|
60
54
|
|
|
61
|
-
const verticalOhlcvSetup = Object.fromEntries(keyNames.map(v => [v,
|
|
55
|
+
const verticalOhlcvSetup = Object.fromEntries(keyNames.map(v => [v, new Float64Array(len).fill(NaN)]))
|
|
62
56
|
Object.assign(verticalOhlcv, {...verticalOhlcvSetup})
|
|
63
57
|
|
|
64
58
|
if(lag > 0)
|
|
@@ -66,7 +60,14 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
66
60
|
main.lag(keyNames, lag)
|
|
67
61
|
}
|
|
68
62
|
|
|
69
|
-
|
|
63
|
+
[`${prefix}_upper`, `${prefix}_middle`, `${prefix}_lower`].forEach(v => {
|
|
64
|
+
priceBased.add(v)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
for(const key of keyNames)
|
|
68
|
+
{
|
|
69
|
+
arrayTypes[key] = 'Float64Array'
|
|
70
|
+
}
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
// Derive prefix for subsequent calls if not set.
|
|
@@ -79,20 +80,20 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
79
80
|
// Update the indicator with the current value.
|
|
80
81
|
const instance = instances.bollinger_bands.settings[indicatorKey];
|
|
81
82
|
const value = verticalOhlcv[target][index];
|
|
82
|
-
instance.update(value
|
|
83
|
+
instance.update(value);
|
|
83
84
|
|
|
84
85
|
// Attempt to retrieve the result.
|
|
85
|
-
let result =
|
|
86
|
+
let result = {};
|
|
86
87
|
try {
|
|
87
88
|
result = instance.getResult();
|
|
88
89
|
} catch (err) {
|
|
89
|
-
// If not available, result stays
|
|
90
|
+
// If not available, result stays {}.
|
|
90
91
|
}
|
|
91
92
|
|
|
92
|
-
// Use
|
|
93
|
-
const upper = result?.upper ??
|
|
94
|
-
const middle = result?.middle ??
|
|
95
|
-
const lower = result?.lower ??
|
|
93
|
+
// Use NaN fallbacks for the primary values.
|
|
94
|
+
const upper = result?.upper ?? NaN;
|
|
95
|
+
const middle = result?.middle ?? NaN;
|
|
96
|
+
const lower = result?.lower ?? NaN;
|
|
96
97
|
|
|
97
98
|
// Always push the indicator outputs.
|
|
98
99
|
main.pushToMain({ index, key: `${subPrefix}_upper`, value: upper });
|
|
@@ -101,26 +102,19 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
101
102
|
|
|
102
103
|
// Process height if requested.
|
|
103
104
|
if (height) {
|
|
104
|
-
let heightValue =
|
|
105
|
-
if (
|
|
106
|
-
heightValue =
|
|
105
|
+
let heightValue = NaN;
|
|
106
|
+
if (!Number.isNaN(lower) && !Number.isNaN(upper)) {
|
|
107
|
+
heightValue = ((upper - lower) / lower)
|
|
107
108
|
}
|
|
108
109
|
main.pushToMain({ index, key: `${subPrefix}_height`, value: heightValue });
|
|
109
110
|
}
|
|
110
111
|
|
|
111
112
|
// Process each range property.
|
|
112
113
|
for (const rangeKey of range) {
|
|
113
|
-
let rangeValue =
|
|
114
|
+
let rangeValue = NaN;
|
|
114
115
|
const priceValue = verticalOhlcv[rangeKey][index];
|
|
115
|
-
if (
|
|
116
|
-
rangeValue = (
|
|
117
|
-
? Math.max(Math.min((priceValue - lower) / (upper - lower), 1), 0)
|
|
118
|
-
: (priceValue - lower) / (upper - lower)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (scale) {
|
|
122
|
-
rangeValue = calcMagnitude(rangeValue, scale);
|
|
123
|
-
}
|
|
116
|
+
if (!Number.isNaN(priceValue) && !Number.isNaN(lower) && !Number.isNaN(upper)) {
|
|
117
|
+
rangeValue = (priceValue - lower) / (upper - lower)
|
|
124
118
|
}
|
|
125
119
|
main.pushToMain({ index, key: `${subPrefix}_range_${rangeKey}`, value: rangeValue });
|
|
126
120
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { calcMagnitude } from "../utilities/numberUtilities.js"
|
|
2
1
|
|
|
3
|
-
export const donchianChannels = (main, index, size, offset, { height, range,
|
|
2
|
+
export const donchianChannels = (main, index, size, offset, { height, range, lag }) => {
|
|
4
3
|
const indicatorKey = `${size}_${offset}`;
|
|
5
4
|
const { verticalOhlcv, instances, len } = main;
|
|
6
5
|
|
|
7
6
|
// Initialization: create output arrays and indicator instance on the first call.
|
|
8
7
|
if (index === 0) {
|
|
9
|
-
const { inputParams, priceBased,
|
|
8
|
+
const { inputParams, priceBased, arrayTypes} = main;
|
|
10
9
|
const numberOfIndicators = inputParams.filter(o => o.key === 'donchianChannels').length;
|
|
11
10
|
const prefix = numberOfIndicators > 1 ? `donchian_channel_${indicatorKey}` : 'donchian_channel';
|
|
12
11
|
|
|
@@ -28,19 +27,14 @@ export const donchianChannels = (main, index, size, offset, { height, range, sca
|
|
|
28
27
|
|
|
29
28
|
// Set up additional arrays for each range property.
|
|
30
29
|
for (const rangeKey of range) {
|
|
31
|
-
if (!(rangeKey in verticalOhlcv) || !priceBased.
|
|
30
|
+
if (!(rangeKey in verticalOhlcv) || !priceBased.has(rangeKey)) {
|
|
32
31
|
throw new Error(`Invalid range item value "${rangeKey}" property for donchianChannels. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`);
|
|
33
32
|
}
|
|
34
33
|
keyNames.push(`${prefix}_range_${rangeKey}`)
|
|
35
34
|
|
|
36
|
-
if(autoMinMax)
|
|
37
|
-
{
|
|
38
|
-
main.autoMinMaxKeys.push(`${prefix}_range_${rangeKey}`)
|
|
39
|
-
}
|
|
40
|
-
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
const verticalOhlcvSetup = Object.fromEntries(keyNames.map(v => [v,
|
|
37
|
+
const verticalOhlcvSetup = Object.fromEntries(keyNames.map(v => [v, new Float64Array(len).fill(NaN)]))
|
|
44
38
|
Object.assign(verticalOhlcv, {...verticalOhlcvSetup})
|
|
45
39
|
|
|
46
40
|
if(lag > 0)
|
|
@@ -48,12 +42,20 @@ export const donchianChannels = (main, index, size, offset, { height, range, sca
|
|
|
48
42
|
main.lag(keyNames, lag)
|
|
49
43
|
}
|
|
50
44
|
|
|
51
|
-
|
|
45
|
+
|
|
46
|
+
[`${prefix}_upper`, `${prefix}_basis`, `${prefix}_lower`].forEach(v => {
|
|
47
|
+
priceBased.add(v)
|
|
48
|
+
})
|
|
52
49
|
|
|
53
50
|
instances.donchian_channel.settings[indicatorKey] = {
|
|
54
51
|
maxDeque: [], // will hold indices for highs in descending order
|
|
55
52
|
minDeque: [] // will hold indices for lows in ascending order
|
|
56
53
|
};
|
|
54
|
+
|
|
55
|
+
for(const key of keyNames)
|
|
56
|
+
{
|
|
57
|
+
arrayTypes[key] = 'Float64Array'
|
|
58
|
+
}
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
const numberOfIndicators = instances.donchian_channel.numberOfIndicators;
|
|
@@ -67,16 +69,16 @@ export const donchianChannels = (main, index, size, offset, { height, range, sca
|
|
|
67
69
|
const endIdx = currentBarIdx + 1; // currentBarIdx is inclusive
|
|
68
70
|
const startIdx = endIdx - size; // window: [startIdx, endIdx)
|
|
69
71
|
|
|
70
|
-
// If the window is not fully available, push
|
|
72
|
+
// If the window is not fully available, push NaN values for all computed outputs.
|
|
71
73
|
if (startIdx < 0 || endIdx > len) {
|
|
72
|
-
main.pushToMain({ index, key: `${subPrefix}_upper`, value:
|
|
73
|
-
main.pushToMain({ index, key: `${subPrefix}_basis`, value:
|
|
74
|
-
main.pushToMain({ index, key: `${subPrefix}_lower`, value:
|
|
74
|
+
main.pushToMain({ index, key: `${subPrefix}_upper`, value: NaN });
|
|
75
|
+
main.pushToMain({ index, key: `${subPrefix}_basis`, value: NaN });
|
|
76
|
+
main.pushToMain({ index, key: `${subPrefix}_lower`, value: NaN });
|
|
75
77
|
if (height) {
|
|
76
|
-
main.pushToMain({ index, key: `${subPrefix}_height`, value:
|
|
78
|
+
main.pushToMain({ index, key: `${subPrefix}_height`, value: NaN });
|
|
77
79
|
}
|
|
78
80
|
for (const rangeKey of range) {
|
|
79
|
-
main.pushToMain({ index, key: `${subPrefix}_range_${rangeKey}`, value:
|
|
81
|
+
main.pushToMain({ index, key: `${subPrefix}_range_${rangeKey}`, value: NaN });
|
|
80
82
|
}
|
|
81
83
|
return true;
|
|
82
84
|
}
|
|
@@ -103,9 +105,9 @@ export const donchianChannels = (main, index, size, offset, { height, range, sca
|
|
|
103
105
|
minDeque.push(currentBarIdx);
|
|
104
106
|
|
|
105
107
|
// Retrieve computed values with safety checks.
|
|
106
|
-
const upper = maxDeque.length ? highs[maxDeque[0]] :
|
|
107
|
-
const lower = minDeque.length ? lows[minDeque[0]] :
|
|
108
|
-
const basis = (
|
|
108
|
+
const upper = maxDeque.length ? highs[maxDeque[0]] : NaN;
|
|
109
|
+
const lower = minDeque.length ? lows[minDeque[0]] : NaN;
|
|
110
|
+
const basis = (!Number.isNaN(upper) && !Number.isNaN(lower)) ? (upper + lower) / 2 : NaN;
|
|
109
111
|
|
|
110
112
|
// Always push the main indicator values.
|
|
111
113
|
main.pushToMain({ index, key: `${subPrefix}_upper`, value: upper });
|
|
@@ -114,30 +116,23 @@ export const donchianChannels = (main, index, size, offset, { height, range, sca
|
|
|
114
116
|
|
|
115
117
|
// Process height if enabled.
|
|
116
118
|
if (height) {
|
|
117
|
-
let heightValue =
|
|
118
|
-
if (
|
|
119
|
-
heightValue =
|
|
119
|
+
let heightValue = NaN;
|
|
120
|
+
if (!Number.isNaN(upper) && !Number.isNaN(lower)) {
|
|
121
|
+
heightValue = ((upper - lower) / lower)
|
|
120
122
|
}
|
|
121
123
|
main.pushToMain({ index, key: `${subPrefix}_height`, value: heightValue });
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
// Process each range property.
|
|
125
127
|
for (const rangeKey of range) {
|
|
126
|
-
let rangeValue =
|
|
127
|
-
const priceValue = verticalOhlcv[rangeKey][index]
|
|
128
|
+
let rangeValue = NaN;
|
|
129
|
+
const priceValue = verticalOhlcv[rangeKey][index]
|
|
128
130
|
if (
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
(upper - lower) !== 0
|
|
131
|
+
!Number.isNaN(priceValue) &&
|
|
132
|
+
!Number.isNaN(upper) &&
|
|
133
|
+
!Number.isNaN(lower)
|
|
133
134
|
) {
|
|
134
|
-
rangeValue = (
|
|
135
|
-
? Math.max(Math.min((priceValue - lower) / (upper - lower), 1), 0)
|
|
136
|
-
: (priceValue - lower) / (upper - lower)
|
|
137
|
-
|
|
138
|
-
if (scale) {
|
|
139
|
-
rangeValue = calcMagnitude(rangeValue, scale);
|
|
140
|
-
}
|
|
135
|
+
rangeValue = (priceValue - lower) / (upper - lower)
|
|
141
136
|
}
|
|
142
137
|
main.pushToMain({ index, key: `${subPrefix}_range_${rangeKey}`, value: rangeValue });
|
|
143
138
|
}
|
|
@@ -2,16 +2,16 @@ import { FasterEMA, FasterMACD } from 'trading-signals';
|
|
|
2
2
|
|
|
3
3
|
const defaultTarget = 'close';
|
|
4
4
|
|
|
5
|
-
export const macd = (main, index, fast, slow, signal,
|
|
6
|
-
|
|
7
|
-
const { verticalOhlcv, instances
|
|
5
|
+
export const macd = (main, index, fast, slow, signal, {target, lag}) => {
|
|
6
|
+
|
|
7
|
+
const { verticalOhlcv, instances } = main;
|
|
8
8
|
|
|
9
9
|
// Create an instance key that includes the target if it isn't the default.
|
|
10
10
|
const instanceKey = `${fast}_${slow}_${signal}${target === defaultTarget ? '' : `_${target}`}`;
|
|
11
11
|
|
|
12
12
|
// Initialization on the first index.
|
|
13
13
|
if (index === 0) {
|
|
14
|
-
const { inputParams, crossPairsList,
|
|
14
|
+
const { inputParams, crossPairsList, priceBased, len , arrayTypes} = main;
|
|
15
15
|
|
|
16
16
|
if (!verticalOhlcv.hasOwnProperty(target)) {
|
|
17
17
|
throw new Error(`Target property ${target} not found in verticalOhlcv for macd.`);
|
|
@@ -42,12 +42,27 @@ export const macd = (main, index, fast, slow, signal, options) => {
|
|
|
42
42
|
);
|
|
43
43
|
|
|
44
44
|
Object.assign(verticalOhlcv, {
|
|
45
|
-
[diffKey]:
|
|
46
|
-
[deaKey]:
|
|
47
|
-
[histogramKey]:
|
|
45
|
+
[diffKey]: new Float64Array(len).fill(NaN),
|
|
46
|
+
[deaKey]: new Float64Array(len).fill(NaN),
|
|
47
|
+
[histogramKey]: new Float64Array(len).fill(NaN),
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
[diffKey, deaKey, histogramKey].forEach(v => {
|
|
51
|
+
priceBased.add(v)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const keyNames = [diffKey, deaKey, histogramKey]
|
|
55
|
+
|
|
56
|
+
if(lag > 0)
|
|
57
|
+
{
|
|
58
|
+
main.lag(keyNames, lag)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for(const key of keyNames)
|
|
62
|
+
{
|
|
63
|
+
arrayTypes[key] = 'Float64Array'
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
}
|
|
52
67
|
|
|
53
68
|
const { numberOfIndicators, settings } = instances.macd;
|
|
@@ -58,20 +73,19 @@ export const macd = (main, index, fast, slow, signal, options) => {
|
|
|
58
73
|
|
|
59
74
|
const macdInstance = settings[instanceKey];
|
|
60
75
|
const value = verticalOhlcv[target][index];
|
|
61
|
-
macdInstance.update(value
|
|
76
|
+
macdInstance.update(value);
|
|
62
77
|
|
|
63
|
-
let macdResult =
|
|
78
|
+
let macdResult = {}
|
|
64
79
|
try {
|
|
65
80
|
macdResult = macdInstance.getResult();
|
|
66
81
|
} catch (err) {
|
|
67
|
-
// If the result is unavailable, macdResult remains
|
|
68
|
-
macdResult = null;
|
|
82
|
+
// If the result is unavailable, macdResult remains NaN.
|
|
69
83
|
}
|
|
70
84
|
|
|
71
|
-
// Always push values; use
|
|
72
|
-
main.pushToMain({ index, key: diffKey, value: macdResult ? macdResult.macd :
|
|
73
|
-
main.pushToMain({ index, key: deaKey, value: macdResult ? macdResult.signal :
|
|
74
|
-
main.pushToMain({ index, key: histogramKey, value: macdResult ? macdResult.histogram :
|
|
85
|
+
// Always push values; use NaN as fallback when macdResult is missing.
|
|
86
|
+
main.pushToMain({ index, key: diffKey, value: macdResult ? macdResult.macd : NaN });
|
|
87
|
+
main.pushToMain({ index, key: deaKey, value: macdResult ? macdResult.signal : NaN });
|
|
88
|
+
main.pushToMain({ index, key: histogramKey, value: macdResult ? macdResult.histogram : NaN });
|
|
75
89
|
|
|
76
90
|
return true;
|
|
77
91
|
};
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
|
|
2
2
|
import {FasterEMA, FasterSMA, FasterBollingerBands} from 'trading-signals';
|
|
3
|
-
import { classifyBoll, calcZScore } from '../utilities/classification.js'
|
|
4
3
|
|
|
5
4
|
const indicatorClasses = {ema: FasterEMA, sma: FasterSMA}
|
|
6
5
|
|
|
7
|
-
export const movingAverages = (main, index, indicatorName, size, { target,
|
|
8
|
-
const { verticalOhlcv, instances, priceBased
|
|
6
|
+
export const movingAverages = (main, index, indicatorName, size, { target, lag }) => {
|
|
7
|
+
const { verticalOhlcv, instances, priceBased } = main;
|
|
9
8
|
let suffix =
|
|
10
9
|
typeof target === 'string' &&
|
|
11
10
|
verticalOhlcv.hasOwnProperty(target) &&
|
|
@@ -13,10 +12,9 @@ export const movingAverages = (main, index, indicatorName, size, { target, diff
|
|
|
13
12
|
? `_${target}`
|
|
14
13
|
: '';
|
|
15
14
|
const keyName = `${indicatorName}_${size}${suffix}`;
|
|
16
|
-
const diffKeyName = `${indicatorName}_${size}_diff${suffix}`;
|
|
17
15
|
|
|
18
16
|
if (index === 0) {
|
|
19
|
-
const {
|
|
17
|
+
const { len, arrayTypes } = main;
|
|
20
18
|
|
|
21
19
|
if (!verticalOhlcv.hasOwnProperty(target)) {
|
|
22
20
|
throw new Error(
|
|
@@ -29,146 +27,34 @@ export const movingAverages = (main, index, indicatorName, size, { target, diff
|
|
|
29
27
|
maInstance: new indicatorClasses[indicatorName](size)
|
|
30
28
|
};
|
|
31
29
|
|
|
32
|
-
verticalOhlcv[keyName] =
|
|
30
|
+
verticalOhlcv[keyName] = new Float64Array(len).fill(NaN)
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
if (diff !== null) {
|
|
36
|
-
instances[keyName].diffInstance = new FasterBollingerBands(
|
|
37
|
-
diff.size,
|
|
38
|
-
diff.stdDev
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const lagDiffArr = [];
|
|
42
|
-
|
|
43
|
-
for (const targetKey of diff.targets) {
|
|
44
|
-
verticalOhlcv[`${diffKeyName}_${targetKey}`] = [...nullArray];
|
|
45
|
-
|
|
46
|
-
if(diff.autoMinMax)
|
|
47
|
-
{
|
|
48
|
-
main.autoMinMaxKeys.push(`${diffKeyName}_${targetKey}`)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (diff.lag > 0) {
|
|
52
|
-
lagDiffArr.push(`${diffKeyName}_${targetKey}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
32
|
+
priceBased.add(keyName);
|
|
55
33
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
34
|
+
if(lag > 0)
|
|
35
|
+
{
|
|
36
|
+
main.lag([keyName], lag)
|
|
59
37
|
}
|
|
60
38
|
|
|
61
|
-
|
|
39
|
+
arrayTypes[keyName] = 'Float64Array'
|
|
40
|
+
|
|
62
41
|
}
|
|
63
42
|
|
|
64
43
|
// Retrieve the current price value.
|
|
65
44
|
const value = verticalOhlcv[target][index];
|
|
66
|
-
const { maInstance
|
|
45
|
+
const { maInstance } = instances[keyName];
|
|
67
46
|
|
|
68
47
|
// Update the moving average instance.
|
|
69
|
-
maInstance.update(value
|
|
70
|
-
let currMa =
|
|
48
|
+
maInstance.update(value);
|
|
49
|
+
let currMa = NaN;
|
|
71
50
|
try {
|
|
72
51
|
currMa = maInstance.getResult();
|
|
73
52
|
} catch (err) {
|
|
74
|
-
currMa =
|
|
53
|
+
currMa = NaN;
|
|
75
54
|
}
|
|
76
55
|
|
|
77
|
-
// Always push the MA value (even if
|
|
56
|
+
// Always push the MA value (even if NaN).
|
|
78
57
|
main.pushToMain({ index, key: keyName, value: currMa });
|
|
79
58
|
|
|
80
|
-
// Process the diff values if a diff is requested.
|
|
81
|
-
if (diff !== null) {
|
|
82
|
-
for (const targetKey of diff.targets) {
|
|
83
|
-
// Compute diffValue only if we have a valid MA value.
|
|
84
|
-
let diffValue = null;
|
|
85
|
-
if (currMa !== null && typeof currMa === 'number') {
|
|
86
|
-
diffValue = verticalOhlcv[targetKey][index] - currMa;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Update the diff indicator only if diffValue is a valid number.
|
|
90
|
-
if (diffValue !== null && typeof diffValue === 'number') {
|
|
91
|
-
diffInstance.update(Math.abs(diffValue), lastIndexReplace);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
let diffBoll = null;
|
|
95
|
-
try {
|
|
96
|
-
diffBoll = diffInstance.getResult();
|
|
97
|
-
} catch (err) {
|
|
98
|
-
diffBoll = null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const classified = classifyBoll(diffValue, diffBoll, diff.scale, diff.autoMinMax);
|
|
102
|
-
main.pushToMain({
|
|
103
|
-
index,
|
|
104
|
-
key: `${diffKeyName}_${targetKey}`,
|
|
105
|
-
value: classified
|
|
106
|
-
});
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
59
|
return true;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
export const getMovingAveragesParams = (indicatorName, size, options, validMagnitudeValues) => {
|
|
115
|
-
if (!options || typeof options !== 'object') {
|
|
116
|
-
throw new Error(`"options" must be an object in ${indicatorName}. eg: {target, height, range}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
let { target = 'close', diff = null } = options;
|
|
120
|
-
|
|
121
|
-
if(typeof target !== 'string')
|
|
122
|
-
{
|
|
123
|
-
throw new Error(`"target" must be a valid keyName ${indicatorName}.`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (typeof size !== 'number' || size <= 0) {
|
|
127
|
-
throw new Error(`"size" must be a positive number in ${indicatorName}.`);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
//diff can be null, true, false or an object with optional {stdDev, scale, targets, and size}
|
|
131
|
-
const optionArgs = { target, diff: null };
|
|
132
|
-
|
|
133
|
-
if (diff !== null && (typeof diff === 'object' || (typeof diff === 'boolean' && diff === true))) {
|
|
134
|
-
|
|
135
|
-
diff = (diff === true) ? ({}) : diff
|
|
136
|
-
|
|
137
|
-
const {
|
|
138
|
-
stdDev = 2,
|
|
139
|
-
scale = 0.05,
|
|
140
|
-
targets = ['close'],
|
|
141
|
-
size: diffSize = size,
|
|
142
|
-
lag = 0,
|
|
143
|
-
autoMinMax = false
|
|
144
|
-
} = diff;
|
|
145
|
-
|
|
146
|
-
if (typeof stdDev !== 'number' || stdDev <= 0) {
|
|
147
|
-
throw new Error(`"diff.stdDev" must be a number greater than 0 in ${indicatorName}.`);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (typeof scale !== 'number' || !validMagnitudeValues.includes(scale)) {
|
|
151
|
-
throw new Error(`"diff.scale" must be a number between 0.01 and 1 in ${indicatorName}.`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (typeof diffSize !== 'number' || !Number.isInteger(diffSize) || diffSize <= 0) {
|
|
155
|
-
throw new Error(`"diff.size" must be an integer greater than or equal to size in ${indicatorName}.`);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
if (!Array.isArray(targets)) {
|
|
159
|
-
throw new Error(`"diff.targest" must be an array of keyNames with at least 1 item in ${indicatorName}.`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (typeof lag !== 'number' || !Number.isInteger(lag) || lag < 0) {
|
|
163
|
-
throw new Error(`"diff.lag" must be an integer greater than 0 in ${indicatorName}.`);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (typeof autoMinMax !== 'boolean') {
|
|
167
|
-
throw new Error(`"diff.autoMinMax" must be an boolean in ${indicatorName}.`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
optionArgs.diff = { stdDev, scale, size: diffSize, targets, lag, autoMinMax};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return optionArgs;
|
|
174
60
|
}
|
|
@@ -1,39 +1,45 @@
|
|
|
1
1
|
import { FasterSMA } from 'trading-signals';
|
|
2
|
-
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
3
2
|
|
|
4
|
-
export const relativeVolume = (main, index, size
|
|
3
|
+
export const relativeVolume = (main, index, size, {lag}) => {
|
|
4
|
+
|
|
5
5
|
const key = `relative_volume_${size}`;
|
|
6
|
-
const { instances, verticalOhlcv
|
|
6
|
+
const { instances, verticalOhlcv } = main;
|
|
7
7
|
|
|
8
8
|
if (index === 0) {
|
|
9
|
-
const {
|
|
9
|
+
const { len, arrayTypes } = main;
|
|
10
|
+
|
|
10
11
|
instances[key] = {
|
|
11
12
|
instance: new FasterSMA(size),
|
|
12
|
-
prevRelativeVolumeSma:
|
|
13
|
+
prevRelativeVolumeSma: NaN
|
|
13
14
|
};
|
|
14
|
-
|
|
15
|
+
|
|
16
|
+
verticalOhlcv[key] = new Float64Array(len).fill(NaN);
|
|
17
|
+
|
|
18
|
+
if(lag > 0)
|
|
19
|
+
{
|
|
20
|
+
main.lag([key], lag)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
arrayTypes[key] = 'Float64Array'
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
const value = verticalOhlcv.volume[index];
|
|
18
27
|
const smaInstance = instances[key].instance;
|
|
19
|
-
smaInstance.update(value
|
|
28
|
+
smaInstance.update(value);
|
|
20
29
|
|
|
21
|
-
let smaValue =
|
|
30
|
+
let smaValue = NaN;
|
|
22
31
|
try {
|
|
23
32
|
smaValue = smaInstance.getResult();
|
|
24
33
|
} catch (err) {
|
|
25
|
-
|
|
34
|
+
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
const prevSma = instances[key].prevRelativeVolumeSma;
|
|
29
|
-
let currRelativeVolume =
|
|
38
|
+
let currRelativeVolume = NaN;
|
|
30
39
|
|
|
31
40
|
// Only calculate relative volume if both current SMA and previous SMA are valid numbers and prevSma is not zero.
|
|
32
|
-
if (
|
|
41
|
+
if (!Number.isNaN(smaValue) && !Number.isNaN(prevSma)) {
|
|
33
42
|
currRelativeVolume = value / prevSma;
|
|
34
|
-
if (scale) {
|
|
35
|
-
currRelativeVolume = calcMagnitude(currRelativeVolume, scale);
|
|
36
|
-
}
|
|
37
43
|
}
|
|
38
44
|
|
|
39
45
|
main.pushToMain({ index, key, value: currRelativeVolume });
|