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 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
- getData() {
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
- const value = verticalOhlcv[header][i];
61
-
62
- // If value is null, mark the row to be skipped and break
63
- if (value === null || typeof value === 'undefined') {
64
- isInvalidValue = true;
65
- break;
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 result;
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
- return this.getData()[this.getData().length -1]
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
- this.inputParams.push({key: 'macd', params: [fast, slow, signal]})
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 = false, range = false} = options
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.5",
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
- let defaultTarget = 'close'
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
- const { instance, heightInstance } = main.instances[`bollinger_bands${suffix}`]
40
- const value = main.verticalOhlcv[target][index]
41
- instance.update(value, main.lastIndexReplace)
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
- let result;
44
- try {
45
- result = instance.getResult();
46
- } catch (err) {
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
- if(!result) return true
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
- const { upper, middle, lower } = result;
53
- main.verticalOhlcv[`bollinger_bands_upper${suffix}`][index] = upper;
54
- main.verticalOhlcv[`bollinger_bands_middle${suffix}`][index] = middle;
55
- main.verticalOhlcv[`bollinger_bands_lower${suffix}`][index] = lower;
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
- if (!height && !range) return true;
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
- try {
66
- heightMean = heightInstance.getResult();
67
- } catch (err) {
68
- //do nothing
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
- main.verticalOhlcv[`bollinger_bands_range${suffix}`][index] = rangeValue;
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
- if(heightMean)
74
- {
75
- main.verticalOhlcv[`bollinger_bands_height${suffix}`][index] = classifySize(heightValue, heightMean, 1.5);
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
- return true;
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
- const highs = main.verticalOhlcv.high
8
- const lows = main.verticalOhlcv.low
9
- const {height, range} = options
10
-
11
-
12
- // Ensure that the required arrays exist
13
- if (index === 0) {
14
-
15
- Object.assign(main.verticalOhlcv, {
16
- donchian_channel_upper: [...main.nullArray],
17
- donchian_channel_basis: [...main.nullArray],
18
- donchian_channel_lower: [...main.nullArray],
19
- ...(height && {
20
- [`donchian_channel_height`]: [...main.nullArray]
21
- }),
22
- ...(range && {
23
- [`donchian_channel_range`]: [...main.nullArray],
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
- const { heightInstance } = main.instances['donchian_channel']
35
-
36
- // Compute the slice start and end to get exactly 'size' bars
37
- const endIdx = (index - offset) + 1 // inclusive end of the slice
38
- const startIdx = endIdx - size // start index for slicing
39
-
40
- // If any of these indices is out of bounds, return
41
- if (startIdx < 0 || endIdx > main.len) return true
42
-
43
- // Extract the relevant chunks of data
44
- const highChunk = highs.slice(startIdx, endIdx)
45
- const lowChunk = lows.slice(startIdx, endIdx)
46
-
47
- // Compute the Donchian channels
48
- const upper = Math.max(...highChunk)
49
- const lower = Math.min(...lowChunk)
50
- const basis = (upper + lower) / 2
51
- const close = main.verticalOhlcv.close[index]
52
- const rangeValue = (close - lower) / (upper - lower)
53
- const heightValue = upper - lower
54
-
55
- if(height)
56
- {
57
- heightInstance.update(heightValue, main.lastIndexReplace)
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
- // Place the result at index
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
- return true
91
- }
111
+ return true;
112
+ };
@@ -26,7 +26,7 @@ export const ema = (main, index, size, target) => {
26
26
 
27
27
  if(currEma)
28
28
  {
29
- main.verticalOhlcv[keyName][index] = currEma
29
+ main.pushToMain({index, key: keyName, value: currEma})
30
30
  }
31
31
 
32
32
  }
@@ -1,27 +1,56 @@
1
1
  import {FasterEMA, FasterMACD} from 'trading-signals';
2
2
 
3
- export const macd = (main, index, fast, slow, signal) => {
3
+ const defaultTarget = 'close'
4
4
 
5
- const value = main.verticalOhlcv.close[index]
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.crossPairsList.push({fast: 'macd_diff', slow: 'macd_dea', isDefault: true});
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['macd'] = new FasterMACD(
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
- macd_diff: [...main.nullArray],
19
- macd_dea: [...main.nullArray],
20
- macd_histogram: [...main.nullArray],
41
+ [`${prefix}_diff`]: [...main.nullArray],
42
+ [`${prefix}_dea`]: [...main.nullArray],
43
+ [`${prefix}_histogram`]: [...main.nullArray],
21
44
  })
22
45
  }
23
46
 
24
- const macdInstance = main.instances[`macd`];
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
- main.verticalOhlcv['macd_diff'][index] = macdResult.macd;
38
- main.verticalOhlcv['macd_dea'][index] = macdResult.signal;
39
- main.verticalOhlcv['macd_histogram'][index] = macdResult.histogram;
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 value = main.verticalOhlcv.volume[index]
5
+ const key = `relative_volume_${size}`
6
6
 
7
7
  if (index === 0) {
8
- main.instances[`relative_volume_${size}`] = {
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
- [`relative_volume_${size}`]: [...main.nullArray],
14
+ [key]: [...main.nullArray],
15
15
  })
16
16
  }
17
17
 
18
- const smaInstance = main.instances[`relative_volume_${size}`].instance
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[`relative_volume_${size}`]
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[`relative_volume_${size}`].prevRelativeVolumeSma = smaValue
41
+ main.instances[key].prevRelativeVolumeSma = smaValue
38
42
 
39
43
  return true;
40
44
  };
@@ -27,7 +27,7 @@ export const sma = (main, index, size, target) => {
27
27
 
28
28
  if(currSma)
29
29
  {
30
- main.verticalOhlcv[keyName][index] = currSma
30
+ main.pushToMain({index, key: keyName, value: currSma})
31
31
  }
32
32
 
33
33
  }
@@ -35,7 +35,7 @@ export const rsi = (main, index, size) => {
35
35
 
36
36
  if(currentRsi)
37
37
  {
38
- main.verticalOhlcv[`rsi_${size}`][index] = currentRsi
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.verticalOhlcv[`rsi_sma_${size}`][index] = smoothedRsi
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[`volume_oscillator`] = [...main.nullArray];
17
- main.crossPairsList.push({ fast: `volume_oscillator`, slow: 0, isDefault: true });
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[`volume_oscillator`];
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
- main.verticalOhlcv[`volume_oscillator`][index] = 100 * (fastValue - slowValue) / slowValue
42
+
43
+ main.pushToMain({index, key, value: 100 * (fastValue - slowValue) / slowValue})
42
44
  } else {
43
- main.verticalOhlcv[`volume_oscillator`][index] = null;
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.verticalOhlcv.candle_body_size[index] = isUp
116
- ? classifySize(candleBodySize, bodySizeMean, 2, changeLevel)
117
- : -Math.abs(classifySize(candleBodySize, bodySizeMean, 2, changeLevel)) || 0;
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.verticalOhlcv.candle_gap_size[index] = isUp
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.verticalOhlcv.candle_close_direction[index] = isUp
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
- main.verticalOhlcv.candle_high_direction[index] = isUp
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.verticalOhlcv.candle_low_direction[index] = isUp
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.verticalOhlcv.candle_open_direction[index] = isUp
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
- main.verticalOhlcv.candle_body_size[index] = isUp ? candleBodySize : -candleBodySize;
145
- main.verticalOhlcv.candle_gap_size[index] = isUp ? gapSize : -gapSize;
146
- main.verticalOhlcv.candle_close_direction[index] = isUp ? closeDirection : -closeDirection;
147
- main.verticalOhlcv.candle_high_direction[index] = isUp ? highDirection : -highDirection;
148
- main.verticalOhlcv.candle_low_direction[index] = isUp ? lowDirection : -lowDirection;
149
- main.verticalOhlcv.candle_open_direction[index] = isUp ? openDirection : -openDirection;
150
-
151
- main.verticalOhlcv.candle_top_size[index] = topSize;
152
- main.verticalOhlcv.candle_bottom_size[index] = bottomSize;
153
- main.verticalOhlcv.candle_shadow_size[index] = shadowSize;
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;
@@ -48,15 +48,14 @@ export const dateTime = (main, index) => {
48
48
  main.instances.dateTime.sessionIntradayIndexCount = 0
49
49
  }
50
50
 
51
-
52
- main.verticalOhlcv.session_daily_index[index] = main.instances.dateTime.sessionDailyIndexCount
53
- main.verticalOhlcv.session_intraday_index[index] = main.instances.dateTime.sessionIntradayIndexCount
54
- main.verticalOhlcv.day_of_the_week[index] = day_of_the_week
55
- main.verticalOhlcv.day_of_the_month[index] = day_of_the_month
56
- main.verticalOhlcv.week_of_the_month[index] = week_of_the_month
57
- main.verticalOhlcv.month[index] = month
58
- main.verticalOhlcv.year[index] = year
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.verticalOhlcv[crossName][index] = main.instances[crossName].getResult()
191
+ main.pushToMain({index, key: crossName, value: main.instances[crossName].getResult()})
192
192
  }
193
193
 
194
194
  }
@@ -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.verticalOhlcv[key][index] = currentColumn[laggedIndex]
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.verticalOhlcv.mid_price_open_close[index] = (open + close) / 2
20
- main.verticalOhlcv.mid_price_high_low[index] = (high + low) / 2
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
- dateTime,
19
- priceVariations,
20
- rsi,
21
- sma,
22
- ema,
23
- macd,
24
- relativeVolume,
25
- donchianChannels,
26
- bollingerBands,
27
- volumeOscillator,
28
- candleStudies
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
- // Fresh initialization
45
- for (let i = 0; i < baseKeys.length; i++) {
46
- main.verticalOhlcv[baseKeys[i]] = [...main.nullArray]
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
- const inputKeys = Object.keys(input[0]);
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
- if (!main.otherKeys) {
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
- const indicatorCalls = processIndicatorCalls(inputParams);
68
-
69
-
70
- for (let x = startIndex; x < len; x++) {
71
- const current = input[x];
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
- const processIndicatorCalls = inputParams => {
99
- if (!inputParams || typeof inputParams !== 'object') return []
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
- const indicatorCalls = []
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
- for(let x = 0; x < inputParams.length; x++)
104
- {
105
- const {key, params} = inputParams[x]
106
- indicatorCalls.push({key, fn: indicatorFunctions[key], args: params})
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
- return indicatorCalls;
168
+ // Process these indicators separately (ensuring their execution order)
169
+ lag(main, x);
170
+ crossPairs(main, x);
171
+
172
+ main.lastComputedIndex++;
173
+ }
110
174
  };