ohlcv-indicators 2.1.5 → 2.1.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
@@ -3,7 +3,10 @@ import { correlation } from './src/studies/correlation.js'
3
3
  import { setIndicatorsFromInputParams } from './src/utilities/setIndicatorsFromInputParams.js'
4
4
  import { validateDate } from './src/utilities/validators.js'
5
5
  import { isAlreadyComputed } from './src/utilities/validators.js'
6
- import { divideByMultiplier } from './src/utilities/precision.js'
6
+ import { divideByMultiplier } from './src/utilities/numberUtilities.js'
7
+ import { getMovingAveragesParams } from './src/moving-averages/movingAverages.js'
8
+
9
+ const validMagnitudeValues = [0.01, 0.02, 0.025, 0.05, 0.1, 0.20, 0.25, 0.5, 1]
7
10
 
8
11
  export default class OHLCV_INDICATORS {
9
12
  constructor({input, ticker = null, precision = true}) {
@@ -206,7 +209,7 @@ export default class OHLCV_INDICATORS {
206
209
  {
207
210
  const key = colKeys[x]
208
211
 
209
- if(this.priceBased.find(v => key.startsWith(v)))
212
+ if(this.priceBased.find(v => key === v))
210
213
  {
211
214
  this.priceBased.push(key)
212
215
  }
@@ -215,11 +218,18 @@ export default class OHLCV_INDICATORS {
215
218
  return this;
216
219
  }
217
220
 
218
- relativeVolume(size) {
221
+ relativeVolume(size, options = {}) {
219
222
 
220
223
  isAlreadyComputed(this)
221
224
 
222
- this.inputParams.push({key: 'relativeVolume', params: [size]})
225
+ const {scale = null} = options
226
+
227
+ if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
228
+
229
+ throw new Error(`"scale" value in relativeVolume must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
230
+ }
231
+
232
+ this.inputParams.push({key: 'relativeVolume', params: [size, {scale}]})
223
233
 
224
234
  return this
225
235
  }
@@ -228,19 +238,9 @@ export default class OHLCV_INDICATORS {
228
238
 
229
239
  isAlreadyComputed(this)
230
240
 
231
- if(!options || typeof options !== 'object')
232
- {
233
- throw new Error('"options" must be an object in ema. eg: {target, height, range}');
234
- }
235
-
236
- const {target = 'close'} = options
237
-
238
- if (typeof size !== 'number' || size <= 0) {
239
- throw new Error('"size" must be a positive number in ema.');
240
- }
241
+ const optionArgs = getMovingAveragesParams('ema', size, options, validMagnitudeValues)
241
242
 
242
- this.inputParams.push({key: 'ema', params: [size, target]})
243
- this.priceBased.push(`ema_${size}`)
243
+ this.inputParams.push({key: 'movingAverages', params: ['ema', size, optionArgs]})
244
244
 
245
245
  return this
246
246
  }
@@ -248,23 +248,14 @@ export default class OHLCV_INDICATORS {
248
248
 
249
249
  isAlreadyComputed(this)
250
250
 
251
- if(!options || typeof options !== 'object')
252
- {
253
- throw new Error('"options" must be an object in sma. eg: {target, height, range}');
254
- }
255
-
256
- const {target = 'close'} = options
257
-
258
- if (typeof size !== 'number' || size <= 0) {
259
- throw new Error('"size" must be a positive number in sma.');
260
- }
261
-
251
+ const optionArgs = getMovingAveragesParams('sma', size, options, validMagnitudeValues)
262
252
 
263
- this.inputParams.push({key: 'sma', params: [size, target]})
264
- this.priceBased.push(`sma_${size}`)
253
+ this.inputParams.push({key: 'movingAverages', params: ['sma', size, optionArgs]})
265
254
 
266
- return this
255
+ return this
267
256
  }
257
+
258
+
268
259
  macd(fast = 12, slow = 26, signal = 9, options = {}) {
269
260
 
270
261
  isAlreadyComputed(this)
@@ -300,7 +291,7 @@ export default class OHLCV_INDICATORS {
300
291
  throw new Error('"options" must be an object in bollingerBands. eg: {target, height, range}');
301
292
  }
302
293
 
303
- const {target = 'close', height = false, range = [], zScore = []} = options
294
+ const {target = 'close', height = false, range = [], zScore = [], scale = null} = options
304
295
 
305
296
  // Validate size and times
306
297
  if (typeof size !== 'number' || size <= 0) {
@@ -317,15 +308,23 @@ export default class OHLCV_INDICATORS {
317
308
  }
318
309
  if (typeof height !== 'boolean') {
319
310
  throw new Error('"height" must be a boolean in bollingerBands.');
320
- }
311
+ }
312
+ else
313
+ {
314
+ if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
315
+
316
+ throw new Error(`"scale" value in bollingerBands must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
317
+ }
318
+ }
319
+
320
+
321
321
 
322
- this.inputParams.push({key: 'bollingerBands', params: [size, stdDev, {target, height, range, zScore}]});
323
- this.priceBased.push('bollinger_bands_middle', 'bollinger_bands_upper', 'bollinger_bands_lower');
322
+ this.inputParams.push({key: 'bollingerBands', params: [size, stdDev, {target, height, scale, range, zScore}]});
324
323
 
325
324
  return this;
326
325
  }
327
326
 
328
- rsi(size)
327
+ rsi(size, options = {})
329
328
  {
330
329
  isAlreadyComputed(this)
331
330
 
@@ -334,71 +333,97 @@ export default class OHLCV_INDICATORS {
334
333
  throw new Error('"size" must be a positive number in rsi.');
335
334
  }
336
335
 
337
- this.inputParams.push({key: 'rsi', params: [size]})
336
+ const {scale = null, target = 'close'} = options
337
+
338
+ if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
339
+
340
+ throw new Error(`"scale" value in rsi must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
341
+ }
342
+
343
+ this.inputParams.push({key: 'rsi', params: [size, {scale, target}]})
338
344
 
339
345
  return this
340
346
  }
341
- donchianChannels(size = 20, offset = 0, options = {})
342
- {
343
- isAlreadyComputed(this)
344
-
347
+ donchianChannels(size = 20, offset = 0, options = {}) {
348
+ isAlreadyComputed(this);
349
+
345
350
  if (typeof size !== 'number' || size <= 0) {
346
- throw new Error('"size" must be a positive number or 0 in donchianChannels.');
351
+ throw new Error('"size" must be a positive number greater than 0 in donchianChannels.');
347
352
  }
348
-
353
+
349
354
  if (typeof offset !== 'number' || offset < 0) {
350
- throw new Error('"offset" must be a positive number or 0 in donchianChannels.');
355
+ throw new Error('"offset" must be a number greater than or equal to 0 in donchianChannels.');
351
356
  }
352
-
353
- const {height = false, range = []} = options
354
-
357
+
358
+ const { height = false, range = [], scale = null } = options;
359
+
355
360
  if (!Array.isArray(range)) {
356
- throw new Error('If set, "range" must be a array of column names in donchianChannels.');
361
+ throw new Error('If set, "range" must be an array of column names in donchianChannels.');
357
362
  }
358
-
363
+
359
364
  if (typeof height !== 'boolean') {
360
- throw new Error('"height" must be a boolean in donchianChannels.');
361
- }
365
+ throw new Error('"height" must be a boolean in donchianChannels.');
366
+ }
367
+ else
368
+ {
362
369
 
363
- this.inputParams.push({key: 'donchianChannels', params: [size, offset, {height, range}]})
364
- this.priceBased.push('donchian_channel_upper', 'donchian_channel_lower', 'donchian_channel_basis')
370
+ if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
365
371
 
366
- return this
372
+ throw new Error(`"scale" value in donchianChannels must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
373
+ }
374
+ }
375
+
376
+ this.inputParams.push({ key: 'donchianChannels', params: [size, offset, { height, range, scale }] });
377
+
378
+ return this;
367
379
  }
368
- candleStudies(size = 20, options = {})
369
- {
370
-
371
- isAlreadyComputed(this)
380
+
372
381
 
382
+ candleStudies(size = 20, stdDev = 2, options = {}) {
383
+ isAlreadyComputed(this);
384
+
373
385
  if (typeof size !== 'number' || size <= 0) {
374
- throw new Error('"size" must be a positive number or 0 in candleStudies.');
386
+ throw new Error('"size" must be a positive number greater than 0 in candleStudies.');
375
387
  }
388
+
389
+ if (typeof stdDev !== 'number' || stdDev <= 0) {
390
+ throw new Error('"stdDev" must be a positive number greater than 0 in candleStudies.');
391
+ }
392
+
393
+ const { lag = 0, scale = 0.05 } = options;
376
394
 
377
- const {stdDev = 2, lag = 0} = options
378
395
 
379
- if(typeof stdDev !== 'number' || (typeof stdDev === 'number' && stdDev <= 0))
380
- {
381
- throw new Error('"stdDev" must be a positive number greater than 0 in candleStudies.');
382
- }
396
+ if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
383
397
 
384
- this.inputParams.push({key: 'candleStudies', params: [size, stdDev, lag]})
385
- return this
386
- }
398
+ throw new Error(`"scale" value in candleStudies must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
399
+ }
400
+
401
+ this.inputParams.push({ key: 'candleStudies', params: [size, stdDev, {lag, scale}] });
402
+ return this;
403
+ }
404
+
387
405
 
388
- volumeOscillator(fastSize = 5, slowSize = 10)
406
+ volumeOscillator(fastSize = 5, slowSize = 10, options = {})
389
407
  {
390
408
 
391
409
  isAlreadyComputed(this)
392
410
 
393
- if (typeof fastSize !== 'number' || fastSize <= 0) {
394
- throw new Error('fastSize" must be a positive number in volumeOscillator.');
411
+ if (typeof fastSize !== 'number' || fastSize <= 1) {
412
+ throw new Error('fastSize" must be a positive number greater than 1 in volumeOscillator.');
395
413
  }
396
414
 
397
415
  if (typeof slowSize !== 'number' || slowSize <= fastSize) {
398
416
  throw new Error('"slowSize" must be a positive number greater than the "fastSize" in volumeOscillator.');
399
417
  }
400
418
 
401
- this.inputParams.push({key: 'volumeOscillator', params: [fastSize, slowSize]})
419
+ const {scale = null} = options
420
+
421
+ if (typeof scale === 'number' && !validMagnitudeValues.includes(scale)) {
422
+
423
+ throw new Error(`"scale" value in volumeOscillator must be any of the following numbers: ${validMagnitudeValues.join(', ')}`);
424
+ }
425
+
426
+ this.inputParams.push({key: 'volumeOscillator', params: [fastSize, slowSize, {scale}]})
402
427
  return this
403
428
  }
404
429
  dateTime()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohlcv-indicators",
3
- "version": "2.1.5",
3
+ "version": "2.1.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,12 +1,12 @@
1
1
  import { FasterBollingerBands } from 'trading-signals'
2
+ import { calcMagnitude } from '../utilities/numberUtilities.js';
2
3
 
3
4
  const defaultTarget = 'close'
4
5
 
5
- export const bollingerBands = (main, index, size, stdDev, options) => {
6
+ export const bollingerBands = (main, index, size, stdDev, { height, range = [], zScore = [], target, scale }) => {
6
7
  // Destructure options ensure range and zScore default to empty arrays if not provided.
7
8
 
8
9
  const { verticalOhlcv, instances, lastIndexReplace } = main;
9
- const { height, range = [], zScore = [], target } = options
10
10
  const suffix = target === defaultTarget ? '' : `_${target}`
11
11
  const indicatorKey = `${size}_${stdDev}${suffix}`
12
12
  let prefix // Will be computed during initialization
@@ -47,10 +47,12 @@ export const bollingerBands = (main, index, size, stdDev, options) => {
47
47
  ...(height && { [`${prefix}_height`]: [...nullArray] })
48
48
  }
49
49
 
50
+ main.priceBased.push(`${prefix}_upper`, `${prefix}_middle`, `${prefix}_lower`)
51
+
50
52
  // Set up additional arrays for each range property.
51
53
  for (const rangeKey of range) {
52
54
  if (!(rangeKey in verticalOhlcv) || !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)}`)
55
+ throw new Error(`Invalid range item value "${rangeKey}" property for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`)
54
56
  }
55
57
  ohlcvSetup[`${prefix}_range_${rangeKey}`] = [...nullArray]
56
58
  }
@@ -58,7 +60,7 @@ export const bollingerBands = (main, index, size, stdDev, options) => {
58
60
  // Set up additional arrays for each zScore property.
59
61
  for (const zScoreKey of zScore) {
60
62
  if (!(zScoreKey in verticalOhlcv) || !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)}`)
63
+ throw new Error(`Invalid zScore item value "${zScoreKey}" for bollingerBands. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`)
62
64
  }
63
65
  ohlcvSetup[`${prefix}_zscore_${zScoreKey}`] = [...nullArray]
64
66
  }
@@ -97,23 +99,40 @@ export const bollingerBands = (main, index, size, stdDev, options) => {
97
99
 
98
100
  // Process height if a height instance exists.
99
101
  if (height) {
100
- const heightValue = ((upper - lower) / lower) * 100
102
+ let heightValue = ((upper - lower) / lower)
103
+
104
+ if(scale)
105
+ {
106
+ heightValue = calcMagnitude(heightValue, scale)
107
+ }
108
+
101
109
  main.pushToMain({index, key: `${subPrefix}_height`, value: heightValue})
102
110
  }
103
111
 
104
112
  // Process each range property.
105
113
  for (const rangeKey of range) {
106
- const rangeValue = (verticalOhlcv[rangeKey][index] - lower) / (upper - lower)
114
+ let rangeValue = (verticalOhlcv[rangeKey][index] - lower) / (upper - lower)
115
+
116
+ if(scale)
117
+ {
118
+ rangeValue = calcMagnitude(rangeValue, scale)
119
+ }
120
+
107
121
  main.pushToMain({index, key: `${subPrefix}_range_${rangeKey}`, value: rangeValue})
108
122
  }
109
123
 
110
124
  // Process each zScore property.
111
125
  for (const zScoreKey of zScore) {
112
126
  const denominator = upper - lower
113
- const zScoreValue = denominator !== 0
127
+ let zScoreValue = denominator !== 0
114
128
  ? (2 * stdDev * (verticalOhlcv[zScoreKey][index] - middle)) / denominator
115
129
  : 0
116
130
 
131
+ if(denominator !== 0 && scale)
132
+ {
133
+ zScoreValue = calcMagnitude(zScoreValue, scale)
134
+ }
135
+
117
136
  main.pushToMain({index, key: `${subPrefix}_zscore_${zScoreKey}`, value: zScoreValue})
118
137
  }
119
138
 
@@ -1,41 +1,47 @@
1
+ import { calcMagnitude } from "../utilities/numberUtilities.js"
2
+
3
+
4
+ export const donchianChannels = (main, index, size, offset, { height, range, scale}) => {
1
5
 
2
- export const donchianChannels = (main, index, size, offset, options) => {
3
- const { height, range } = options
4
6
  const indicatorKey = `${size}_${offset}`
7
+ const {verticalOhlcv, instances, len} = main
5
8
 
6
9
  // Initialization: create output arrays and indicator instance on the first call.
7
10
  if (index === 0) {
8
- const numberOfIndicators = main.inputParams.filter(o => o.key === 'donchianChannels').length
11
+ const {inputParams, nullArray, priceBased} = main
12
+ const numberOfIndicators = inputParams.filter(o => o.key === 'donchianChannels').length
9
13
  const prefix = numberOfIndicators > 1 ? `donchian_channel_${indicatorKey}` : 'donchian_channel'
10
14
 
11
- Object.assign(main.verticalOhlcv, {
12
- [`${prefix}_upper`]: [...main.nullArray],
13
- [`${prefix}_basis`]: [...main.nullArray],
14
- [`${prefix}_lower`]: [...main.nullArray],
15
- ...(height && { [`${prefix}_height`]: [...main.nullArray] })
15
+ Object.assign(verticalOhlcv, {
16
+ [`${prefix}_upper`]: [...nullArray],
17
+ [`${prefix}_basis`]: [...nullArray],
18
+ [`${prefix}_lower`]: [...nullArray],
19
+ ...(height && { [`${prefix}_height`]: [...nullArray] })
16
20
  })
17
21
 
18
- if (!main.instances.hasOwnProperty('donchian_channel')) {
19
- main.instances.donchian_channel = { numberOfIndicators, settings: {} }
22
+ priceBased.push(`${prefix}_upper`, `${prefix}_basis`, `${prefix}_lower`);
23
+
24
+ if (!instances.hasOwnProperty('donchian_channel')) {
25
+ instances.donchian_channel = { numberOfIndicators, settings: {} }
20
26
  }
21
27
 
22
28
  // Set up additional arrays for each range property.
23
29
  for (const rangeKey of range) {
24
- if (!(rangeKey in main.verticalOhlcv) || !main.priceBased.includes(rangeKey)) {
25
- throw new Error(`Invalid range item value "${rangeKey}" property for donchianChannels. Only price based key names are accepted:\n${JSON.stringify(main.priceBased)}`)
30
+ if (!(rangeKey in verticalOhlcv) || !priceBased.includes(rangeKey)) {
31
+ throw new Error(`Invalid range item value "${rangeKey}" property for donchianChannels. Only price based key names are accepted:\n${JSON.stringify(priceBased)}`)
26
32
  }
27
- main.verticalOhlcv[`${prefix}_range_${rangeKey}`] = [...main.nullArray]
33
+ verticalOhlcv[`${prefix}_range_${rangeKey}`] = [...nullArray]
28
34
  }
29
35
 
30
- main.instances.donchian_channel.settings[indicatorKey] = {
36
+ instances.donchian_channel.settings[indicatorKey] = {
31
37
  maxDeque: [], // will hold indices for highs in descending order
32
38
  minDeque: [] // will hold indices for lows in ascending order
33
39
  }
34
40
  }
35
41
 
36
- const numberOfIndicators = main.instances.donchian_channel.numberOfIndicators
42
+ const numberOfIndicators = instances.donchian_channel.numberOfIndicators
37
43
  const subPrefix = numberOfIndicators > 1 ? `donchian_channel_${indicatorKey}` : 'donchian_channel'
38
- const state = main.instances.donchian_channel.settings[indicatorKey]
44
+ const state = instances.donchian_channel.settings[indicatorKey]
39
45
  const { maxDeque, minDeque } = state
40
46
 
41
47
  // Compute the “current bar index” for the window.
@@ -45,10 +51,10 @@ export const donchianChannels = (main, index, size, offset, options) => {
45
51
  const startIdx = endIdx - size // window: [startIdx, endIdx)
46
52
 
47
53
  // If the window is not fully available, exit early.
48
- if (startIdx < 0 || endIdx > main.len) return true
54
+ if (startIdx < 0 || endIdx > len) return true
49
55
 
50
- const highs = main.verticalOhlcv.high
51
- const lows = main.verticalOhlcv.low
56
+ const highs = verticalOhlcv.high
57
+ const lows = verticalOhlcv.low
52
58
 
53
59
  // **Update the maximum deque:**
54
60
  // Remove indices that are out of the current window.
@@ -86,13 +92,25 @@ export const donchianChannels = (main, index, size, offset, options) => {
86
92
  main.pushToMain({index, key: `${subPrefix}_lower`, value: lower})
87
93
 
88
94
  if (height) {
89
- const heightValue = ((upper - lower) / lower) * 100
95
+ let heightValue = ((upper - lower) / lower)
96
+
97
+ if(scale)
98
+ {
99
+ heightValue = calcMagnitude(heightValue, scale)
100
+ }
101
+
90
102
  main.pushToMain({index, key: `${subPrefix}_height`, value: heightValue})
91
103
  }
92
104
 
93
105
  // Process each range property.
94
106
  for (const rangeKey of range) {
95
- const rangeValue = (main.verticalOhlcv[rangeKey][index] - lower) / (upper - lower)
107
+ let rangeValue = (verticalOhlcv[rangeKey][index] - lower) / (upper - lower)
108
+
109
+ if(scale)
110
+ {
111
+ rangeValue = calcMagnitude(rangeValue, scale)
112
+ }
113
+
96
114
  main.pushToMain({index, key: `${subPrefix}_range_${rangeKey}`, value: rangeValue})
97
115
  }
98
116
 
@@ -1,73 +1,77 @@
1
- import {FasterEMA, FasterMACD} from 'trading-signals';
1
+ import { FasterEMA, FasterMACD } from 'trading-signals';
2
2
 
3
- const defaultTarget = 'close'
3
+ const defaultTarget = 'close';
4
4
 
5
5
  export const macd = (main, index, fast, slow, signal, options) => {
6
+ const { target } = options;
7
+ const { verticalOhlcv, instances } = main;
6
8
 
7
- const {target} = options
8
- const suffix = target === defaultTarget ? '' : `_${target}`;
9
- const indicatorKey = `${fast}_${slow}_${signal}${suffix}`;
10
-
11
- if (index === 0) {
12
-
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
- }
33
-
34
- main.instances.macd.settings[indicatorKey] = new FasterMACD(
35
- new FasterEMA(fast),
36
- new FasterEMA(slow),
37
- new FasterEMA(signal)
38
- );
39
-
40
- Object.assign(main.verticalOhlcv, {
41
- [`${prefix}_diff`]: [...main.nullArray],
42
- [`${prefix}_dea`]: [...main.nullArray],
43
- [`${prefix}_histogram`]: [...main.nullArray],
44
- })
45
- }
9
+ // Instance key (for internal storage) includes target if not default.
10
+ const instanceKey = `${fast}_${slow}_${signal}${target === defaultTarget ? '' : `_${target}`}`;
11
+
12
+ // On the first index, do the initialization.
13
+ if (index === 0) {
14
+ const { inputParams, crossPairsList, nullArray } = main;
46
15
 
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]
54
- macdInstance.update(value, main.lastIndexReplace);
55
-
56
- let macdResult;
57
-
58
- try {
59
- macdResult = macdInstance.getResult();
60
- } catch (err) {
61
- //do nothing
16
+ if (!verticalOhlcv.hasOwnProperty(target)) {
17
+ throw new Error(`Target property ${target} not found in verticalOhlcv for macd.`);
62
18
  }
63
19
 
20
+ const numberOfIndicators = inputParams.filter(o => o.key === 'macd').length;
21
+ // For display keys: if there's more than one indicator, include the parameters in the prefix.
22
+ const displayPrefix = numberOfIndicators > 1 ? `macd_${fast}_${slow}_${signal}` : 'macd';
23
+
24
+ // Build the keys: if target is not the default, append it at the end.
25
+ const diffKey = target === defaultTarget ? `${displayPrefix}_diff` : `${displayPrefix}_diff_${target}`;
26
+ const deaKey = target === defaultTarget ? `${displayPrefix}_dea` : `${displayPrefix}_dea_${target}`;
27
+ const histogramKey = target === defaultTarget ? `${displayPrefix}_histogram` : `${displayPrefix}_histogram_${target}`;
64
28
 
65
- if (macdResult) {
29
+ crossPairsList.push({ fast: diffKey, slow: deaKey, isDefault: true });
66
30
 
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})
31
+ if (!instances.hasOwnProperty('macd')) {
32
+ instances.macd = {
33
+ numberOfIndicators,
34
+ settings: {}
35
+ };
70
36
  }
71
37
 
72
- return true;
38
+ instances.macd.settings[instanceKey] = new FasterMACD(
39
+ new FasterEMA(fast),
40
+ new FasterEMA(slow),
41
+ new FasterEMA(signal)
42
+ );
43
+
44
+ Object.assign(verticalOhlcv, {
45
+ [diffKey]: [...nullArray],
46
+ [deaKey]: [...nullArray],
47
+ [histogramKey]: [...nullArray],
48
+ });
49
+ }
50
+
51
+ const {numberOfIndicators, settings} = instances.macd
52
+
53
+ // Determine display prefix for updating values.
54
+ const displayPrefix = numberOfIndicators > 1 ? `macd_${fast}_${slow}_${signal}` : 'macd';
55
+ const diffKey = target === defaultTarget ? `${displayPrefix}_diff` : `${displayPrefix}_diff_${target}`;
56
+ const deaKey = target === defaultTarget ? `${displayPrefix}_dea` : `${displayPrefix}_dea_${target}`;
57
+ const histogramKey = target === defaultTarget ? `${displayPrefix}_histogram` : `${displayPrefix}_histogram_${target}`;
58
+
59
+ const macdInstance = settings[instanceKey];
60
+ const value = verticalOhlcv[target][index];
61
+ macdInstance.update(value, main.lastIndexReplace);
62
+
63
+ let macdResult;
64
+ try {
65
+ macdResult = macdInstance.getResult();
66
+ } catch (err) {
67
+ // Do nothing if no result is available.
68
+ }
69
+
70
+ if (macdResult) {
71
+ main.pushToMain({ index, key: diffKey, value: macdResult.macd });
72
+ main.pushToMain({ index, key: deaKey, value: macdResult.signal });
73
+ main.pushToMain({ index, key: histogramKey, value: macdResult.histogram });
74
+ }
75
+
76
+ return true;
73
77
  };
@@ -0,0 +1,160 @@
1
+
2
+ import {FasterEMA, FasterSMA, FasterBollingerBands} from 'trading-signals';
3
+ import { classifyBoll } from '../utilities/classification.js'
4
+
5
+ const indicatorClasses = {ema: FasterEMA, sma: FasterSMA}
6
+
7
+ export const movingAverages = (main, index, indicatorName, size, {target, diff}) => {
8
+
9
+ const {verticalOhlcv, instances, priceBased, lastIndexReplace} = main
10
+ let suffix = (typeof target === 'string' && verticalOhlcv.hasOwnProperty(target) && target !== 'close') ? `_${target}` : ''
11
+ const keyName = `${indicatorName}_${size}${suffix}`
12
+ const diffKeyName = `${indicatorName}_${size}_diff${suffix}`
13
+
14
+ if(index === 0)
15
+ {
16
+ const {nullArray} = main
17
+
18
+ if(!verticalOhlcv.hasOwnProperty(target))
19
+ {
20
+ throw new Error(`Target property ${target} not found in verticalOhlcv for ${indicatorName}.`)
21
+ }
22
+
23
+ instances[keyName] = {
24
+ maInstance: new indicatorClasses[indicatorName](size)
25
+ }
26
+
27
+ verticalOhlcv[keyName] = [...nullArray]
28
+
29
+ if(diff !== null)
30
+ {
31
+ instances[keyName].diffInstance = new FasterBollingerBands(diff.size, diff.stdDev)
32
+
33
+ const lagDiffArr = []
34
+
35
+ for(const targetKey of diff.targets)
36
+ {
37
+ verticalOhlcv[`${diffKeyName}_${targetKey}`] = [...nullArray]
38
+
39
+ if(diff.lag > 0)
40
+ {
41
+ lagDiffArr.push(`${diffKeyName}_${targetKey}`)
42
+ }
43
+ }
44
+
45
+ if(diff.lag > 0)
46
+ {
47
+ console.log(lagDiffArr, diff.lag)
48
+ main.lag(lagDiffArr, diff.lag)
49
+ }
50
+ }
51
+
52
+ priceBased.push(keyName)
53
+ }
54
+
55
+ const value = verticalOhlcv[target][index]
56
+ const {maInstance, diffInstance} = instances[keyName]
57
+ maInstance.update(value, lastIndexReplace)
58
+
59
+ let currMa
60
+
61
+ try{
62
+ currMa = maInstance.getResult()
63
+ } catch(err)
64
+ {
65
+ //do nothing
66
+ }
67
+
68
+ if(currMa)
69
+ {
70
+ main.pushToMain({index, key: keyName, value: currMa})
71
+
72
+ if(diff === null)
73
+ {
74
+ return true
75
+ }
76
+
77
+ for(const targetKey of diff.targets)
78
+ {
79
+
80
+ const diffValue = (target === targetKey) ? value - currMa : verticalOhlcv[targetKey][index]
81
+
82
+ diffInstance.update(Math.abs(diffValue), lastIndexReplace)
83
+
84
+ let diffBoll
85
+
86
+ try {
87
+ diffBoll = diffInstance.getResult()
88
+ } catch(err)
89
+ {
90
+ diffBoll = null
91
+ }
92
+
93
+ main.pushToMain({index, key: `${diffKeyName}_${targetKey}`, value: classifyBoll(diffValue, diffBoll, diff.scale)})
94
+
95
+ }
96
+
97
+
98
+
99
+ }
100
+
101
+ }
102
+
103
+
104
+
105
+ export const getMovingAveragesParams = (indicatorName, size, options, validMagnitudeValues) => {
106
+ if (!options || typeof options !== 'object') {
107
+ throw new Error(`"options" must be an object in ${indicatorName}. eg: {target, height, range}`);
108
+ }
109
+
110
+ let { target = 'close', diff = null } = options;
111
+
112
+ if(typeof target !== 'string')
113
+ {
114
+ throw new Error(`"target" must be a valid keyName ${indicatorName}.`);
115
+ }
116
+
117
+ if (typeof size !== 'number' || size <= 0) {
118
+ throw new Error(`"size" must be a positive number in ${indicatorName}.`);
119
+ }
120
+
121
+ //diff can be null, true, false or an object with optional {stdDev, scale, targets, and size}
122
+ const optionArgs = { target, diff: null };
123
+
124
+ if (diff !== null && (typeof diff === 'object' || (typeof diff === 'boolean' && diff === true))) {
125
+
126
+ diff = (diff === true) ? ({}) : diff
127
+
128
+ const {
129
+ stdDev = 2,
130
+ scale = 0.05,
131
+ targets = ['close'],
132
+ size: diffSize = size,
133
+ lag = 0
134
+ } = diff;
135
+
136
+ if (typeof stdDev !== 'number' || stdDev <= 0) {
137
+ throw new Error(`"diff.stdDev" must be a number greater than 0 in ${indicatorName}.`);
138
+ }
139
+
140
+ if (typeof scale !== 'number' || !validMagnitudeValues.includes(scale)) {
141
+ throw new Error(`"diff.scale" must be a number between 0.01 and 1 in ${indicatorName}.`);
142
+ }
143
+
144
+ if (typeof diffSize !== 'number' || !Number.isInteger(diffSize) || diffSize <= 0) {
145
+ throw new Error(`"diff.size" must be an integer greater than or equal to size in ${indicatorName}.`);
146
+ }
147
+
148
+ if (!Array.isArray(targets)) {
149
+ throw new Error(`"diff.targest" must be an array of keyNames with at least 1 item in ${indicatorName}.`);
150
+ }
151
+
152
+ if (typeof lag !== 'number' || !Number.isInteger(lag) || lag < 0) {
153
+ throw new Error(`"diff.lag" must be an integer greater than 0 in ${indicatorName}.`);
154
+ }
155
+
156
+ optionArgs.diff = { stdDev, scale, size: diffSize, targets, lag };
157
+ }
158
+
159
+ return optionArgs;
160
+ }
@@ -1,6 +1,7 @@
1
1
  import {FasterSMA} from 'trading-signals';
2
+ import { calcMagnitude } from '../utilities/numberUtilities.js';
2
3
 
3
- export const relativeVolume = (main, index, size = 10) => {
4
+ export const relativeVolume = (main, index, size = 10, {scale}) => {
4
5
 
5
6
  const key = `relative_volume_${size}`
6
7
 
@@ -32,10 +33,22 @@ export const relativeVolume = (main, index, size = 10) => {
32
33
 
33
34
  const {prevRelativeVolumeSma} = main.instances[key]
34
35
 
36
+ let currRelativeVolume = null
37
+
38
+ if(smaValue !== null && prevRelativeVolumeSma !== null)
39
+ {
40
+ currRelativeVolume = value / prevRelativeVolumeSma
41
+
42
+ if(scale)
43
+ {
44
+ currRelativeVolume = calcMagnitude(currRelativeVolume, scale)
45
+ }
46
+ }
47
+
35
48
  main.pushToMain({
36
49
  index,
37
50
  key,
38
- value: (smaValue !== null && prevRelativeVolumeSma !== null) ? value / prevRelativeVolumeSma : null
51
+ value: currRelativeVolume
39
52
  })
40
53
 
41
54
  main.instances[key].prevRelativeVolumeSma = smaValue
@@ -1,47 +1,65 @@
1
1
  import {FasterRSI} from 'trading-signals';
2
2
  import { FasterSMA } from 'trading-signals';
3
+ import { calcMagnitude } from '../utilities/numberUtilities.js';
3
4
 
5
+ const defaultTarget = 'close'
4
6
 
5
- export const rsi = (main, index, size) => {
7
+ export const rsi = (main, index, size, {scale, target}) => {
6
8
 
7
- const value = main.verticalOhlcv.close[index]
9
+ const {verticalOhlcv, instances, lastIndexReplace} = main
10
+ const suffix = (target === defaultTarget) ? '' : `_${target}`
11
+ const rsiKey = `rsi_${size}${suffix}`
12
+ const rsiSmaKey = `rsi_sma_${size}${suffix}`
8
13
 
9
14
  if(index === 0)
10
15
  {
11
- main.crossPairsList.push({fast: `rsi_${size}`, slow: `rsi_sma_${size}`, isDefault: true})
16
+ const {crossPairsList, nullArray} = main
12
17
 
13
- Object.assign(main.instances, {
14
- [`rsi_${size}`]: new FasterRSI(size),
15
- [`rsi_sma_${size}`]: new FasterSMA(size)
18
+ if(!verticalOhlcv.hasOwnProperty(target))
19
+ {
20
+ throw new Error(`Target property ${target} not found in verticalOhlcv for rsi.`)
21
+ }
22
+
23
+ crossPairsList.push({fast: rsiKey, slow: rsiSmaKey, isDefault: true})
24
+
25
+ Object.assign(instances, {
26
+ [rsiKey]: new FasterRSI(size),
27
+ [rsiSmaKey]: new FasterSMA(size)
16
28
  })
17
29
 
18
- Object.assign(main.verticalOhlcv, {
19
- [`rsi_${size}`]: [...main.nullArray],
20
- [`rsi_sma_${size}`]: [...main.nullArray]
30
+ Object.assign(verticalOhlcv, {
31
+ [rsiKey]: [...nullArray],
32
+ [rsiSmaKey]: [...nullArray]
21
33
  })
22
34
  }
23
-
35
+
36
+ const value = verticalOhlcv[target][index]
24
37
  let currentRsi
25
38
  let smoothedRsi
26
39
 
27
- main.instances[`rsi_${size}`].update(value, main.lastIndexReplace)
40
+ instances[rsiKey].update(value, lastIndexReplace)
28
41
 
29
42
  try
30
43
  {
31
- currentRsi = main.instances[`rsi_${size}`].getResult()
44
+ currentRsi = instances[rsiKey].getResult()
32
45
  } catch(err) {
33
46
  currentRsi = null
34
47
  }
35
48
 
36
49
  if(currentRsi)
37
50
  {
38
- main.pushToMain({index, key: `rsi_${size}`, value: currentRsi})
39
- main.instances[`rsi_sma_${size}`].update(currentRsi, main.lastIndexReplace)
51
+ if(scale)
52
+ {
53
+ currentRsi = calcMagnitude(currentRsi, scale)
54
+ }
55
+
56
+ main.pushToMain({index, key: rsiKey, value: currentRsi})
57
+ instances[rsiSmaKey].update(currentRsi, lastIndexReplace)
40
58
  }
41
59
 
42
60
  try
43
61
  {
44
- smoothedRsi = main.instances[`rsi_sma_${size}`].getResult()
62
+ smoothedRsi = instances[rsiSmaKey].getResult()
45
63
  } catch(err)
46
64
  {
47
65
  smoothedRsi = null
@@ -49,7 +67,12 @@ export const rsi = (main, index, size) => {
49
67
 
50
68
  if(smoothedRsi)
51
69
  {
52
- main.pushToMain({index, key: `rsi_sma_${size}`, value: smoothedRsi})
70
+ if(scale)
71
+ {
72
+ smoothedRsi = calcMagnitude(smoothedRsi, scale)
73
+ }
74
+
75
+ main.pushToMain({index, key: rsiSmaKey, value: smoothedRsi})
53
76
  }
54
77
 
55
78
  return true
@@ -1,6 +1,7 @@
1
1
  import { FasterEMA } from 'trading-signals';
2
+ import { calcMagnitude } from '../utilities/numberUtilities.js';
2
3
 
3
- export const volumeOscillator = (main, index, fast, slow) => {
4
+ export const volumeOscillator = (main, index, fast, slow, {scale}) => {
4
5
 
5
6
  const value = main.verticalOhlcv.volume[index]
6
7
  const key = 'volume_oscillator'
@@ -38,12 +39,19 @@ export const volumeOscillator = (main, index, fast, slow) => {
38
39
  slowValue = null;
39
40
  }
40
41
 
41
- if (typeof fastValue === 'number' && typeof slowValue === 'number' && slowValue !== 0) {
42
+ let volumeOscValue = null
42
43
 
43
- main.pushToMain({index, key, value: 100 * (fastValue - slowValue) / slowValue})
44
- } else {
45
- main.pushToMain({index, key, value: null})
44
+ if(typeof fastValue === 'number' && typeof slowValue === 'number' && slowValue !== 0)
45
+ {
46
+ volumeOscValue = 100 * (fastValue - slowValue) / slowValue
47
+
48
+ if(scale)
49
+ {
50
+ volumeOscValue = calcMagnitude(volumeOscValue, scale)
51
+ }
46
52
  }
47
53
 
54
+ main.pushToMain({index, key, value: volumeOscValue})
55
+
48
56
  return true;
49
57
  };
@@ -1,12 +1,9 @@
1
1
  import { FasterSMA, FasterBollingerBands } from 'trading-signals'
2
2
  import { classifyBoll } from '../utilities/classification.js'
3
3
 
4
- const diff = (a, b) => {
5
- const value = a - b
6
- return {value: Math.abs(value), positive: value >= 0}
7
- }
4
+ const diff = (a, b) => a - b
8
5
 
9
- export const candleStudies = (main, index, size, stdDev, lag) => {
6
+ export const candleStudies = (main, index, size, stdDev, {lag, scale}) => {
10
7
 
11
8
  const { verticalOhlcv, instances, lastIndexReplace } = main
12
9
 
@@ -14,16 +11,12 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
14
11
  {
15
12
  const {nullArray} = main
16
13
 
17
- Object.assign(verticalOhlcv, {
18
- candle_gap: [...nullArray],
19
- candle_body: [...nullArray],
20
- candle_top: [...nullArray],
21
- candle_bottom: [...nullArray],
22
- candle_close: [...nullArray],
23
- candle_high: [...nullArray],
24
- candle_low: [...nullArray],
25
- candle_open: [...nullArray],
26
- })
14
+ const candleParts = ['candle_body', 'candle_top', 'candle_bottom']
15
+ const candleGaps = ['candle_gap']
16
+ const candleChanges = ['candle_change_close', 'candle_change_high', 'candle_change_low', 'candle_change_open']
17
+ const keyNames = [...candleParts, ...candleGaps, ...candleChanges]
18
+
19
+ Object.assign(verticalOhlcv, Object.fromEntries(keyNames.map(k => [k, nullArray])))
27
20
 
28
21
  Object.assign(instances, {
29
22
  candleStudies: {
@@ -37,7 +30,6 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
37
30
 
38
31
  if(lag > 0)
39
32
  {
40
- const keyNames = ['candle_body', 'candle_gap', 'candle_close', 'candle_high', 'candle_low', 'candle_open', 'candle_top', 'candle_bottom']
41
33
  main.lag(keyNames, lag)
42
34
  }
43
35
 
@@ -57,11 +49,15 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
57
49
  const currClose = verticalOhlcv.close[index]
58
50
 
59
51
 
60
- //sizes
52
+ //parts
61
53
  const bodySize = diff(currClose, currOpen)
62
54
  const topSize = diff(currHigh, Math.max(currOpen, currClose))
63
55
  const bottomSize = diff(Math.min(currOpen, currClose), currLow)
64
- const gapSize = diff(prevClose, currOpen)
56
+
57
+ //gaps
58
+ const gapOpenClose = diff(prevClose, currOpen)
59
+
60
+ //changes
65
61
  const closeChange = diff(currClose, prevClose)
66
62
  const highChange = diff(currHigh, prevHigh)
67
63
  const lowChange = diff(currLow, prevLow)
@@ -82,11 +78,11 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
82
78
  } = instances.candleStudies
83
79
 
84
80
  // Update instances with current absolute values
85
- bodyInstance.update(bodySize.value, lastIndexReplace)
86
- closeInstance.update(closeChange.value, lastIndexReplace)
87
- highInstance.update(highChange.value, lastIndexReplace)
88
- lowInstance.update(lowChange.value, lastIndexReplace)
89
- openInstance.update(openChange.value, lastIndexReplace)
81
+ bodyInstance.update(Math.abs(bodySize), lastIndexReplace)
82
+ closeInstance.update(Math.abs(closeChange), lastIndexReplace)
83
+ highInstance.update(Math.abs(highChange), lastIndexReplace)
84
+ lowInstance.update(Math.abs(lowChange), lastIndexReplace)
85
+ openInstance.update(Math.abs(openChange), lastIndexReplace)
90
86
 
91
87
  try {
92
88
  bodyBoll = bodyInstance.getResult()
@@ -99,15 +95,20 @@ export const candleStudies = (main, index, size, stdDev, lag) => {
99
95
  }
100
96
 
101
97
 
102
- main.pushToMain({index, key: 'candle_close', value: classifyBoll(closeChange, closeBoll)})
103
- main.pushToMain({index, key: 'candle_high', value: classifyBoll(highChange, highBoll)})
104
- main.pushToMain({index, key: 'candle_low', value: classifyBoll(lowChange, lowBoll)})
105
- main.pushToMain({index, key: 'candle_open', value: classifyBoll(openChange, openBoll)})
106
98
 
107
- main.pushToMain({index, key: 'candle_body', value: classifyBoll(bodySize, bodyBoll)})
108
- main.pushToMain({index, key: 'candle_gap', value: classifyBoll(gapSize, bodyBoll)})
109
- main.pushToMain({index, key: 'candle_top', value: classifyBoll(topSize, bodyBoll)})
110
- main.pushToMain({index, key: 'candle_bottom', value: classifyBoll(bottomSize, bodyBoll)})
99
+ //parts
100
+ main.pushToMain({index, key: 'candle_body', value: classifyBoll(bodySize, bodyBoll, scale)})
101
+ main.pushToMain({index, key: 'candle_top', value: classifyBoll(topSize, bodyBoll, scale)})
102
+ main.pushToMain({index, key: 'candle_bottom', value: classifyBoll(bottomSize, bodyBoll, scale)})
103
+
104
+ //gaps
105
+ main.pushToMain({index, key: 'candle_gap', value: classifyBoll(gapOpenClose, bodyBoll, scale)})
106
+
107
+ //changes
108
+ main.pushToMain({index, key: 'candle_change_close', value: classifyBoll(closeChange, closeBoll, scale)})
109
+ main.pushToMain({index, key: 'candle_change_high', value: classifyBoll(highChange, highBoll, scale)})
110
+ main.pushToMain({index, key: 'candle_change_low', value: classifyBoll(lowChange, lowBoll, scale)})
111
+ main.pushToMain({index, key: 'candle_change_open', value: classifyBoll(openChange, openBoll, scale)})
111
112
 
112
113
  return true
113
114
  }
@@ -1,36 +1,48 @@
1
+ import { calcMagnitude } from "./numberUtilities.js"
2
+
1
3
  /**
2
- * Classifies a given value relative to the Bollinger bands.
4
+ * Classifies a given value relative to provided Bollinger band thresholds.
3
5
  *
4
- * @param {Object} params - Parameters object.
5
- * @param {number} params.value - The value to classify.
6
- * @param {boolean} [params.positive=true] - Flag to determine if the result should be positive.
7
- * @param {Object} bollingerBands - Object containing the Bollinger band thresholds.
6
+ * The function evaluates the input value against the lower, middle, and upper thresholds
7
+ * of the Bollinger bands. If the value is below the lower threshold, it returns 0.
8
+ * If the value exceeds the upper threshold, it returns 1 (or -1 if the value is negative).
9
+ * For values within the bands, it calculates a normalized magnitude between 0 and 1 (or 0 and -1 for negatives)
10
+ * based on the position within the range, rounded to one decimal place.
11
+ * Additionally, if the value is nearly equal to the middle threshold (within a small tolerance),
12
+ * the function returns 0.5 or -0.5 depending on the sign of the value.
13
+ *
14
+ * @param {number} value - The value to classify.
15
+ * @param {Object} bollingerBands - An object containing Bollinger band thresholds.
8
16
  * @param {number} bollingerBands.upper - The upper band threshold.
9
17
  * @param {number} bollingerBands.middle - The middle band value.
10
18
  * @param {number} bollingerBands.lower - The lower band threshold.
11
- * @returns {number|null} The classified value, or null if inputs are invalid.
19
+ * @returns {number|null} The classified value, or null if inputs are invalid or if upper equals lower.
12
20
  */
13
- export const classifyBoll = ({ value, positive = true }, bollingerBands) => {
14
- // Validate that value and bollingerBands are not null/undefined
15
- if (value == null || bollingerBands == null) return null;
16
-
17
- const { upper, middle, lower } = bollingerBands;
18
-
19
-
20
- // Handle values outside the Bollinger bands
21
- if (value < lower) return 0;
22
- if (value > upper) return positive ? 1 : -1;
23
-
24
- // Use a tolerance for floating point comparisons
25
- const epsilon = Number.EPSILON * Math.max(Math.abs(value), Math.abs(middle));
26
- if (Math.abs(value - middle) < epsilon) return positive ? 0.5 : -0.5;
27
-
28
- // Prevent division by zero if upper and lower are equal
29
- if (upper === lower) return null;
30
-
31
- const rangeValue = (value - lower) / (upper - lower);
32
- const magnitude = Math.round(rangeValue * 10) / 10;
33
-
34
- return positive ? magnitude : -magnitude;
35
- };
36
-
21
+
22
+
23
+ export const classifyBoll = (value, bollingerBands, scale = 0.05) => {
24
+ // Validate that value and bollingerBands are not null/undefined
25
+ if (value == null || bollingerBands == null) return null
26
+
27
+ const positive = value >= 0
28
+
29
+ const { upper, middle, lower } = bollingerBands
30
+
31
+ // Handle values outside the Bollinger bands
32
+ if (value < lower) return 0
33
+ if (value > upper) return positive ? 1 : -1
34
+
35
+ // Use a tolerance for floating point comparisons
36
+ const epsilon = Number.EPSILON * Math.max(Math.abs(value), Math.abs(middle))
37
+ if (Math.abs(value - middle) < epsilon) return positive ? 0.5 : -0.5
38
+
39
+ // Prevent division by zero if upper and lower are equal
40
+ if (upper === lower) return null
41
+
42
+ const rangeValue = (value - lower) / (upper - lower)
43
+ let magnitude = calcMagnitude(rangeValue, scale)
44
+
45
+ magnitude = Math.min(Math.max(magnitude, 0), 1)
46
+
47
+ return positive ? magnitude : -magnitude
48
+ }
@@ -1,3 +1,14 @@
1
+
2
+ export const calcMagnitude = (number, magnitudeLevel = 0.1) => {
3
+
4
+ const factor = 1 / magnitudeLevel
5
+
6
+ return Math.round(number * factor) / factor
7
+ }
8
+
9
+ // Helper to clean non-numeric characters (except "-" at the start and decimal point)
10
+ export const cleanNumStr = str => str.replace(/(?!^-)[^0-9.]/g, '');
11
+
1
12
  export const divideByMultiplier = ({row, precisionMultiplier, priceBased}) => {
2
13
  const output = {}
3
14
 
@@ -1,7 +1,6 @@
1
1
  import { validateInputParams } from "./validators.js";
2
2
  import { rsi } from "../oscillators/rsi.js";
3
- import { sma } from "../moving-averages/sma.js";
4
- import { ema } from "../moving-averages/ema.js";
3
+ import { movingAverages } from "../moving-averages/movingAverages.js";
5
4
  import { macd } from "../moving-averages/macd.js";
6
5
  import { relativeVolume } from "../moving-averages/relativeVolume.js";
7
6
  import { donchianChannels } from "../moving-averages/donchianChannel.js";
@@ -13,16 +12,14 @@ import { crossPairs } from "../studies/findCrosses.js";
13
12
  import { dateTime } from "../studies/dateTime.js";
14
13
  import { priceVariations } from "../studies/priceVariations.js";
15
14
 
16
- // Helper to clean non-numeric characters (except "-" at the start and decimal point)
17
- const cleanNumStr = str => str.replace(/(?!^-)[^0-9.]/g, '');
15
+ import { cleanNumStr } from "./numberUtilities.js";
18
16
 
19
17
  // Map indicator keys to their respective functions
20
18
  const indicatorFunctions = {
21
19
  dateTime,
22
20
  priceVariations,
23
21
  rsi,
24
- sma,
25
- ema,
22
+ movingAverages,
26
23
  macd,
27
24
  relativeVolume,
28
25
  donchianChannels,
@@ -1,32 +0,0 @@
1
-
2
- import {FasterEMA} from 'trading-signals'
3
-
4
- export const ema = (main, index, size, target) => {
5
-
6
- const value = main.verticalOhlcv.close[index]
7
-
8
- let prefix = (typeof target === 'string' && main.verticalOhlcv.hasOwnProperty(target) && target !== 'close') ? `${target}_` : ''
9
- const keyName = `${prefix}ema_${size}`
10
-
11
- if(index === 0)
12
- {
13
- main.instances[keyName] = new FasterEMA(size)
14
- main.verticalOhlcv[keyName] = [...main.nullArray]
15
- }
16
-
17
- let currEma
18
- main.instances[keyName].update(value, main.lastIndexReplace)
19
-
20
- try{
21
- currEma = main.instances[keyName].getResult()
22
- } catch(err)
23
- {
24
- currEma = null
25
- }
26
-
27
- if(currEma)
28
- {
29
- main.pushToMain({index, key: keyName, value: currEma})
30
- }
31
-
32
- }
@@ -1,33 +0,0 @@
1
-
2
- import {FasterSMA} from 'trading-signals';
3
-
4
- export const sma = (main, index, size, target) => {
5
-
6
- const value = main.verticalOhlcv.close[index]
7
-
8
- let prefix = (typeof target === 'string' && main.verticalOhlcv.hasOwnProperty(target) && target !== 'close') ? `${target}_` : ''
9
- const keyName = `${prefix}sma_${size}`
10
-
11
-
12
- if(index === 0)
13
- {
14
- main.instances[keyName] = new FasterSMA(size)
15
- main.verticalOhlcv[keyName] = [...main.nullArray]
16
- }
17
-
18
- let currSma
19
- main.instances[keyName].update(value, main.lastIndexReplace)
20
-
21
- try{
22
- currSma = main.instances[keyName].getResult()
23
- } catch(err)
24
- {
25
- //do nothing
26
- }
27
-
28
- if(currSma)
29
- {
30
- main.pushToMain({index, key: keyName, value: currSma})
31
- }
32
-
33
- }