ohlcv-indicators 3.5.4 → 3.5.42

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.
@@ -1,39 +1,36 @@
1
- import { roundDecimalPlaces } from "../utilities/numberUtilities.js";
2
-
3
- /* General scaler: supports "minmax" and "zscore" types */
4
-
1
+ import { normalizeMinMax, normalizeZScore } from "./ml-utilities.js";
2
+
3
+ export const scaler = (main, index, size, colKeys, {type, group, range, lag, precomputed, secondaryLoop}) => {
5
4
 
5
+
6
6
 
7
- const normalizeMinMax = (value, min, max, [validMin, validMax]) => {
8
- const clamped = Math.min(Math.max(value, min), max);
9
- return ((clamped - min) / (max - min)) * (validMax - validMin) + validMin;
10
- };
11
-
12
- const normalizeZScore = (value, mean, std) => {
13
- return std === 0 ? 0 : (value - mean) / std;
14
- };
15
-
16
- export const scaler = (main, index, size, colKeys, {type, group, range, lag, precomputed, decimals}) => {
17
7
  const {groupKey, groupKeyLen} = precomputed
18
- const { verticalOhlcv, instances, arrayTypes } = main;
8
+ const { verticalOhlcv, instances, arrayTypes, invalidValueIndex } = main;
19
9
  const prefix = `${type}_${size}`;
20
10
 
21
- if (index === 0) {
22
- const { len, scaledLabels } = main;
11
+ if(index === 0) {
12
+ const { len, isAlreadyComputed } = main
23
13
 
24
- if(scaledLabels.has(prefix))
14
+ if(isAlreadyComputed.has(prefix))
25
15
  {
26
- throw new Error(`Method "scaler" cannot have both "size" and "options.type" parameters specified together.`)
16
+ throw new Error(`In scaler method can not repeat params "size" and "options.type" together between indicators.`)
27
17
  }
28
- scaledLabels.add(prefix)
18
+
19
+ isAlreadyComputed.add(prefix)
29
20
 
30
- if(!instances.hasOwnProperty('scaler'))
21
+ if(!instances.hasOwnProperty('scaler')) {
22
+ instances.scaler = {}
23
+ }
24
+
25
+ if(!instances.scaler.hasOwnProperty(prefix))
31
26
  {
32
- instances.scaler = {
27
+ instances.scaler[prefix] = {
33
28
  windows: {}
34
29
  }
35
30
  }
36
31
 
32
+ const features = []
33
+
37
34
  for (const target of colKeys) {
38
35
  if (!verticalOhlcv.hasOwnProperty(target)) {
39
36
  throw new Error(`Target property "${target}" not found in verticalOhlcv`);
@@ -44,66 +41,102 @@ export const scaler = (main, index, size, colKeys, {type, group, range, lag, pre
44
41
  arrayTypes[key] = 'Float64Array'
45
42
 
46
43
  const winKey = group ? groupKey : target;
47
- instances.scaler.windows[winKey] = [];
44
+ instances.scaler[prefix].windows[winKey] = [];
48
45
 
49
- if (group) {
50
- if (!main.scaledGroups[groupKey]) main.scaledGroups[groupKey] = [];
51
- main.scaledGroups[groupKey].push(key);
52
- }
46
+ if (!main.scaledGroups[groupKey]) main.scaledGroups[groupKey] = [];
47
+ main.scaledGroups[groupKey].push(key);
53
48
 
54
49
  if (lag > 0) {
55
- if (group) {
56
- const lags = Array.from({ length: lag }).map((_, i) => `${key}_lag_${i + 1}`);
57
- main.scaledGroups[groupKey].push(...lags);
58
- }
59
- main.lag([key], lag);
50
+ const lags = Array.from({ length: lag }).map((_, i) => `${key}_lag_${i + 1}`)
51
+ main.scaledGroups[groupKey].push(...lags)
52
+ features.push(key)
60
53
  }
61
54
  }
55
+
56
+ if(lag > 0)
57
+ {
58
+ main.lag(features, lag)
59
+ }
62
60
  }
61
+
62
+ if(secondaryLoop === true && index <= invalidValueIndex) true
63
63
 
64
- const { windows } = instances.scaler
65
-
64
+ const { windows } = instances.scaler[prefix]
65
+
66
+ let hasInvalidVal = false
67
+
66
68
  // update windows with current values
67
- for (const target of colKeys) {
68
- const val = verticalOhlcv[target][index];
69
- const winKey = group ? groupKey : target;
70
- const win = windows[winKey];
69
+ for (let x = 0; x < colKeys.length; x++) {
70
+ const target = colKeys[x]
71
+ const col = verticalOhlcv[target]
72
+
73
+ if(typeof col === 'undefined')
74
+ {
75
+ hasInvalidVal = true
76
+ break
77
+ }
78
+
79
+ const val = col[index]
80
+
81
+ if(Number.isNaN(val) || val === null)
82
+ {
83
+ hasInvalidVal = true
84
+ break
85
+ }
86
+
87
+ const winKey = group ? groupKey : target
88
+ const win = windows[winKey]
71
89
 
72
- win.push(val);
90
+ win.push(val)
73
91
 
74
92
  if (win.length > (group ? size * groupKeyLen : size)) {
75
- win.shift();
93
+ win.shift()
76
94
  }
77
95
 
78
96
  }
97
+
98
+ if(hasInvalidVal) return
79
99
 
80
- const ready = index + 1 >= size;
100
+ const ready = index + 1 >= size
101
+
102
+ if(!ready) return
81
103
 
82
104
  // scale values once enough data
83
- for (const target of colKeys) {
84
- const val = verticalOhlcv[target][index];
85
- const key = `${prefix}_${target}`;
86
- let scaled = null;
87
-
88
- if (ready) {
89
- const winKey = group ? groupKey : target;
90
- const win = windows[winKey];
91
-
92
- if (type === 'minmax') {
93
- const mn = Math.min(...win);
94
- const mx = Math.max(...win);
95
- scaled = normalizeMinMax(val, mn, mx, range);
96
- } else if (type === 'zscore') {
97
- const mean = win.reduce((sum, x) => sum + x, 0) / win.length;
98
- const variance = win.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / win.length;
99
- const std = Math.sqrt(variance);
100
- scaled = normalizeZScore(val, mean, std);
101
- } else {
102
- throw new Error(`Unknown scaler type "${type}"`);
103
- }
105
+ for (let x = 0; x < colKeys.length; x++) {
106
+ const target = colKeys[x]
107
+ const col = verticalOhlcv[target]
108
+
109
+ if(typeof col === 'undefined')
110
+ {
111
+ break
112
+ }
113
+
114
+ const val = col[index]
115
+
116
+ if(Number.isNaN(val) || val === null)
117
+ {
118
+ break
119
+ }
120
+
121
+ const key = `${prefix}_${target}`
122
+ let scaled = null
123
+ const winKey = group ? groupKey : target
124
+ const win = windows[winKey]
125
+
126
+ if (type === 'minmax') {
127
+ const mn = Math.min(...win)
128
+ const mx = Math.max(...win)
129
+ scaled = normalizeMinMax(val, mn, mx, range)
130
+ } else if (type === 'zscore') {
131
+ const mean = win.reduce((sum, x) => sum + x, 0) / win.length;
132
+ const variance = win.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / win.length
133
+ const std = Math.sqrt(variance);
134
+ scaled = normalizeZScore(val, mean, std)
135
+ } else {
136
+ throw new Error(`Unknown scaler type "${type}"`)
104
137
  }
105
138
 
106
- main.pushToMain({ index, key, value: (decimals === null) ? scaled : roundDecimalPlaces(scaled, decimals)})
139
+ main.pushToMain({ index, key, value: scaled})
107
140
  }
108
- };
141
+ }
109
142
 
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Builds a flattened lookback feature vector or returns null if any value is non‐finite.
3
+ *
4
+ * @param {string[]} featureCols – list of feature column keys
5
+ * @param {Object} instances – your instances object (with crossPairs)
6
+ * @param {number} flatFeaturesColLen – total number of flattened feature slots
7
+ * @param {string} type – a string used only for error reporting
8
+ * @param {number} index – current time index
9
+ * @param {number} lookbackAbs – number of lags to include
10
+ * @param {Object} verticalOhlcv – an object mapping each key to its time series array
11
+ * @returns {number[]\|null} – the feature vector or null if exiting early
12
+ */
13
+ export const buildTrainX = ({
14
+ featureCols,
15
+ flatFeaturesColLen,
16
+ type,
17
+ index,
18
+ lookbackAbs,
19
+ main
20
+ }) => {
21
+
22
+ const {verticalOhlcv} = main
23
+
24
+ let shouldExit = false
25
+
26
+ // --- BUILD A FLATTENED LIST OF “COLUMN SLOTS” ---
27
+ const slots = [];
28
+ for (const key of featureCols) {
29
+ if (key.startsWith('one_hot_') || key.startsWith('pca_')) {
30
+
31
+ const colSize = verticalOhlcv[key][index].length
32
+
33
+ for (let bit = 0; bit < colSize; bit++) {
34
+ slots.push({ key, bit });
35
+ }
36
+ } else {
37
+ slots.push({ key });
38
+ }
39
+ }
40
+
41
+ if (slots.length !== flatFeaturesColLen) {
42
+ throw new Error(
43
+ `slots (${slots.length}) ≠ flatFeaturesColLen (${flatFeaturesColLen}) in ${type} index ${index}`
44
+ );
45
+ }
46
+
47
+ // --- ALLOCATE AND FILL trainX ---
48
+ const trainX = new Array(flatFeaturesColLen * lookbackAbs).fill(NaN);
49
+
50
+ for (let lag = 0; lag < lookbackAbs; lag++) {
51
+ const tIdx = index - lag;
52
+
53
+ for (let s = 0; s < slots.length; s++) {
54
+ const { key, bit } = slots[s];
55
+ const cell = verticalOhlcv[key][tIdx];
56
+ const value = (bit != null) ? cell[bit] : cell;
57
+
58
+ if (!Number.isFinite(value)) {
59
+ shouldExit = true;
60
+ break;
61
+ }
62
+ trainX[lag * flatFeaturesColLen + s] = value;
63
+ }
64
+
65
+ if (shouldExit) break;
66
+ }
67
+
68
+ if (shouldExit) {
69
+ return null;
70
+ }
71
+
72
+ return trainX;
73
+ };
@@ -4,15 +4,17 @@ import { roundDecimalPlaces } from '../utilities/numberUtilities.js';
4
4
  const defaultTarget = 'close'
5
5
 
6
6
  export const bollingerBands = (main, index, size, stdDev, { height, range = [], target, lag, decimals }) => {
7
+
8
+
9
+
7
10
  const { verticalOhlcv, instances } = main;
8
- const suffix = target === defaultTarget ? '' : `_${target}`;
9
- const indicatorKey = `${size}_${stdDev}${suffix}`;
10
- let prefix;
11
+ const indicatorKey = `${size}_${stdDev}`;
12
+ const prefix = 'bollinger_bands'
11
13
 
12
14
  // Initialization on the first call.
13
15
  if (index === 0) {
14
16
 
15
- const { priceBased, inputParams, verticalOhlcv, len, arrayTypes } = main;
17
+ const {inputParams, verticalOhlcv, len, arrayTypes } = main;
16
18
 
17
19
  if (!(target in verticalOhlcv)) {
18
20
  throw new Error(`bollingerBands could not find target "${target}"`);
@@ -24,10 +26,6 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
24
26
  if (o.key === 'bollingerBands') numberOfIndicators++;
25
27
  }
26
28
 
27
- prefix = numberOfIndicators > 1
28
- ? `bollinger_bands_${indicatorKey}`
29
- : `bollinger_bands${suffix}`;
30
-
31
29
  // Only create the container if it doesn't already exist.
32
30
  if (!instances.bollinger_bands) {
33
31
  instances.bollinger_bands = {
@@ -38,23 +36,29 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
38
36
  // Add (or override) the indicator instance keyed by indicatorKey.
39
37
  instances.bollinger_bands.settings[indicatorKey] = new FasterBollingerBands(size, stdDev);
40
38
 
39
+ let suffix = ''
40
+ if((numberOfIndicators > 1)) suffix += `_${indicatorKey}`
41
+ if(target !== defaultTarget) suffix += `_${target}`
42
+
41
43
  const keyNames = [
42
44
  `${prefix}_upper`,
43
45
  `${prefix}_middle`,
44
46
  `${prefix}_lower`,
45
- ]
47
+ ].map(v => `${v}${suffix}`)
46
48
 
47
49
 
48
50
  if (height) {
49
- keyNames.push(`${prefix}_height`)
51
+ keyNames.push(
52
+ `${prefix}_height${suffix}`
53
+ )
50
54
  }
51
55
 
52
56
  // Set up additional arrays for each range property.
53
57
  for (const rangeKey of range) {
54
- if (!(rangeKey in verticalOhlcv) || !priceBased.has(rangeKey)) {
55
- throw new Error(`Invalid range item value "${rangeKey}" property for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`);
58
+ if (!(rangeKey in verticalOhlcv)) {
59
+ throw new Error(`Invalid range item value "${rangeKey}" property for bollingerBands.`);
56
60
  }
57
- keyNames.push(`${prefix}_range_${rangeKey}`)
61
+ keyNames.push(`${prefix}_range_${rangeKey}${suffix}`)
58
62
 
59
63
  }
60
64
 
@@ -66,27 +70,22 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
66
70
  main.lag(keyNames, lag)
67
71
  }
68
72
 
69
- [`${prefix}_upper`, `${prefix}_middle`, `${prefix}_lower`].forEach(v => {
70
- priceBased.add(v)
71
- })
72
-
73
73
  for(const key of keyNames)
74
74
  {
75
75
  arrayTypes[key] = 'Float64Array'
76
76
  }
77
77
  }
78
78
 
79
- // Derive prefix for subsequent calls if not set.
80
- if (!prefix) {
81
- const num = instances.bollinger_bands.numberOfIndicators;
82
- prefix = num > 1 ? `bollinger_bands_${indicatorKey}` : `bollinger_bands${suffix}`;
83
- }
84
- const subPrefix = prefix;
79
+ const {numberOfIndicators} = instances.bollinger_bands
85
80
 
81
+ let suffix = ''
82
+ if((numberOfIndicators > 1)) suffix += `_${indicatorKey}`
83
+ if(target !== defaultTarget) suffix += `_${target}`
84
+
86
85
  // Update the indicator with the current value.
87
- const instance = instances.bollinger_bands.settings[indicatorKey];
88
- const value = verticalOhlcv[target][index];
89
- instance.update(value);
86
+ const instance = instances.bollinger_bands.settings[indicatorKey]
87
+ const value = verticalOhlcv[target][index]
88
+ instance.update(value)
90
89
 
91
90
  // Attempt to retrieve the result.
92
91
  let result = {};
@@ -102,9 +101,9 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
102
101
  const lower = result?.lower ?? NaN;
103
102
 
104
103
  // Always push the indicator outputs.
105
- main.pushToMain({ index, key: `${subPrefix}_upper`, value: upper });
106
- main.pushToMain({ index, key: `${subPrefix}_middle`, value: middle });
107
- main.pushToMain({ index, key: `${subPrefix}_lower`, value: lower });
104
+ main.pushToMain({ index, key: `${prefix}_upper${suffix}`, value: upper });
105
+ main.pushToMain({ index, key: `${prefix}_middle${suffix}`, value: middle });
106
+ main.pushToMain({ index, key: `${prefix}_lower${suffix}`, value: lower });
108
107
 
109
108
  // Process height if requested.
110
109
  if (height) {
@@ -112,7 +111,7 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
112
111
  if (!Number.isNaN(lower) && !Number.isNaN(upper)) {
113
112
  heightValue = ((upper - lower) / lower)
114
113
  }
115
- main.pushToMain({ index, key: `${subPrefix}_height`, value: (decimals === null) ? heightValue : roundDecimalPlaces(heightValue, decimals) });
114
+ main.pushToMain({ index, key: `${prefix}_height${suffix}`, value: (decimals === null) ? heightValue : roundDecimalPlaces(heightValue, decimals) });
116
115
  }
117
116
 
118
117
  // Process each range property.
@@ -122,7 +121,7 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
122
121
  if (!Number.isNaN(priceValue) && !Number.isNaN(lower) && !Number.isNaN(upper)) {
123
122
  rangeValue = (priceValue - lower) / (upper - lower)
124
123
  }
125
- main.pushToMain({ index, key: `${subPrefix}_range_${rangeKey}`, value: (decimals === null) ? rangeValue : roundDecimalPlaces(rangeValue, decimals) });
124
+ main.pushToMain({ index, key: `${prefix}_range_${rangeKey}${suffix}`, value: (decimals === null) ? rangeValue : roundDecimalPlaces(rangeValue, decimals) });
126
125
  }
127
126
 
128
127
  return true;
@@ -2,7 +2,10 @@
2
2
  import { roundDecimalPlaces } from "../utilities/numberUtilities.js";
3
3
 
4
4
  export const donchianChannels = (main, index, size, offset, options) => {
5
- const {verticalOhlcv, instances, len, inputParams, priceBased, arrayTypes, lag} = main
5
+
6
+
7
+
8
+ const {verticalOhlcv, instances, len, inputParams, arrayTypes, lag} = main
6
9
  const { height: includeHeight, range: rangeKeys, decimals } = options
7
10
  const indicatorKey = `${size}_${offset}`
8
11
 
@@ -19,9 +22,9 @@ export const donchianChannels = (main, index, size, offset, options) => {
19
22
  const keys = ['upper', 'basis', 'lower'].map(n => `${prefix}_${n}`);
20
23
  if (includeHeight) keys.push(`${prefix}_height`);
21
24
  for (const rk of rangeKeys) {
22
- if (!(rk in verticalOhlcv) || !priceBased.has(rk)) {
25
+ if (!(rk in verticalOhlcv)) {
23
26
  throw new Error(
24
- `Invalid range key "${rk}". Only price-based keys are allowed: ${[...priceBased]}`
27
+ `Invalid range key "${rk}".`
25
28
  );
26
29
  }
27
30
  keys.push(`${prefix}_range_${rk}`);
@@ -44,7 +47,6 @@ export const donchianChannels = (main, index, size, offset, options) => {
44
47
  if (lag > 0) main.lag(keys, lag);
45
48
 
46
49
  // mark price-based outputs
47
- ['upper','basis','lower'].forEach(n => priceBased.add(`${prefix}_${n}`));
48
50
  keys.forEach(k => arrayTypes[k] = 'Float64Array');
49
51
  }
50
52
 
@@ -4,12 +4,14 @@ const defaultTarget = 'close';
4
4
 
5
5
  export const macd = (main, index, fast, slow, signal, {target, lag, precomputed}) => {
6
6
 
7
+
8
+
7
9
  const { verticalOhlcv, instances } = main
8
10
  const {instanceKey} = precomputed
9
11
 
10
12
  // Initialization on the first index.
11
13
  if (index === 0) {
12
- const { inputParams, crossPairsList, priceBased, len , arrayTypes} = main
14
+ const { inputParams, len , arrayTypes} = main
13
15
 
14
16
  if (!verticalOhlcv.hasOwnProperty(target)) {
15
17
  throw new Error(`Target property ${target} not found in verticalOhlcv for macd.`)
@@ -29,8 +31,6 @@ export const macd = (main, index, fast, slow, signal, {target, lag, precomputed}
29
31
  const deaKey = target === defaultTarget ? `${displayPrefix}_dea` : `${displayPrefix}_dea_${target}`
30
32
  const histogramKey = target === defaultTarget ? `${displayPrefix}_histogram` : `${displayPrefix}_histogram_${target}`
31
33
 
32
- crossPairsList.push({ fast: diffKey, slow: deaKey, isDefault: true })
33
-
34
34
  if (!instances.hasOwnProperty('macd')) {
35
35
  instances.macd = {
36
36
  numberOfIndicators,
@@ -50,10 +50,6 @@ export const macd = (main, index, fast, slow, signal, {target, lag, precomputed}
50
50
  [histogramKey]: new Float64Array(len).fill(NaN),
51
51
  })
52
52
 
53
- [diffKey, deaKey, histogramKey].forEach(v => {
54
- priceBased.add(v)
55
- })
56
-
57
53
  const keyNames = [diffKey, deaKey, histogramKey]
58
54
 
59
55
  if(lag > 0)
@@ -4,59 +4,56 @@ import {FasterEMA, FasterSMA} from 'trading-signals';
4
4
  const indicatorClasses = {
5
5
  ema: FasterEMA,
6
6
  sma: FasterSMA
7
- }
7
+ }
8
+
9
+ export const movingAverages = (main, index, methodName, size, { target, lag }) => {
10
+
11
+
8
12
 
9
- export const movingAverages = (main, index, indicatorName, size, { target, lag, precomputed }) => {
10
13
  const { verticalOhlcv, instances } = main
11
- const {keyName} = precomputed
14
+ const suffix = (target !== 'close') ? `_${target}` : ''
15
+ const keyName = `${methodName}_${size}${suffix}`
12
16
 
13
17
  if (index === 0) {
14
18
 
19
+ const {arrayTypes, len} = main
20
+
15
21
  if (!verticalOhlcv.hasOwnProperty(target)) {
16
22
  throw new Error(
17
- `Target property ${target} not found in verticalOhlcv for ${indicatorName}.`
23
+ `Target property ${target} not found in verticalOhlcv for ${methodName}.`
18
24
  );
19
25
  }
26
+
27
+ // Create the main moving average instance.
28
+ instances[keyName] = new indicatorClasses[methodName](size)
29
+
30
+ verticalOhlcv[keyName] = new Float64Array(len).fill(NaN)
31
+
32
+ if(lag > 0)
33
+ {
34
+ main.lag([keyName], lag)
35
+ }
36
+
37
+ arrayTypes[keyName] = 'Float64Array'
20
38
  }
21
39
 
22
- // Retrieve the current price value.
40
+ // Retrieve the current price value
23
41
  const value = verticalOhlcv[target][index]
24
42
  const instance = instances[keyName]
25
43
 
26
44
  // Update the moving average instance.
27
- instance.update(value);
28
- let currMa = NaN;
45
+ instance.update(value)
46
+
47
+ let currMa = NaN
48
+
29
49
  try {
30
- currMa = instance.getResult();
50
+ currMa = instance.getResult()
31
51
  } catch (err) {
32
52
 
33
53
  }
34
54
 
35
55
  // Always push the MA value (even if NaN).
36
- main.pushToMain({ index, key: keyName, value: currMa });
37
-
38
- return true;
39
- }
40
-
41
- export const precomputeMovingAverages = ({main, size, target, lag, methodName}) => {
42
-
43
- const {instances, verticalOhlcv, priceBased, arrayTypes, len} = main
44
- const suffix = (target !== 'close') ? `_${target}` : ''
45
- const keyName = `${methodName}_${size}${suffix}`
46
-
47
- // Create the main moving average instance.
48
- instances[keyName] = new indicatorClasses[methodName](size)
56
+ main.pushToMain({ index, key: keyName, value: currMa })
49
57
 
50
- verticalOhlcv[keyName] = new Float64Array(len).fill(NaN)
51
-
52
- priceBased.add(keyName)
53
-
54
- if(lag > 0)
55
- {
56
- main.lag([keyName], lag)
57
- }
58
-
59
- arrayTypes[keyName] = 'Float64Array'
60
-
61
- return {keyName}
58
+ return true
62
59
  }
@@ -2,6 +2,8 @@ import { FasterSMA } from 'trading-signals';
2
2
 
3
3
  export const relativeVolume = (main, index, size, {lag}) => {
4
4
 
5
+
6
+
5
7
  const key = `relative_volume_${size}`;
6
8
  const { instances, verticalOhlcv } = main;
7
9