ohlcv-indicators 3.5.54 → 3.5.56

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
@@ -924,13 +924,19 @@ export default class OHLCV_INDICATORS {
924
924
 
925
925
  validateObject(options, 'options', methodName)
926
926
 
927
- const {lag = 0} = options
927
+ const {lag = 0, isPriceBased = false} = options
928
928
 
929
929
  validateArray(newCols, 'newCols', methodName)
930
930
  validateNumber(lag, {min: 0, allowDecimals: false}, 'options.lag', methodName)
931
+ validateBoolean(isPriceBased, 'isPriceBased', 'mapCols')
932
+
933
+ if(this.precision === false && isPriceBased) {
934
+ throw new Error(`Invalid param: If "mapCols.options.isPriceBased" is true, the "constructor.precision" param must be also true.`)
935
+ }
936
+
931
937
  const order = getOrderFromArray(newCols, methodName)
932
938
 
933
- this.inputParams.push({key: methodName, order, params: [newCols, callback, {lag}]})
939
+ this.inputParams.push({key: methodName, order, params: [newCols, callback, {lag, isPriceBased}]})
934
940
 
935
941
  return this
936
942
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ohlcv-indicators",
3
- "version": "3.5.54",
3
+ "version": "3.5.56",
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",
@@ -291,7 +291,7 @@ export const classifier = (
291
291
  {
292
292
  uniqueLabels[loopIdx] = countUniqueLabels(yRows)
293
293
 
294
- if(uniqueLabels[loopIdx] < 2)
294
+ if(uniqueLabels[loopIdx] < 1)
295
295
  {
296
296
  throw new Error(`Invalid number or labels in ${prefix}. Check the logic of your "yCallback" function.`)
297
297
  }
@@ -8,8 +8,6 @@ const indicatorClasses = {
8
8
 
9
9
  export const movingAverages = (main, index, methodName, size, { target, lag }) => {
10
10
 
11
-
12
-
13
11
  const { verticalOhlcv, instances, priceBased } = main
14
12
  const suffix = (target !== 'close') ? `_${target}` : ''
15
13
  const keyName = `${methodName}_${size}${suffix}`
@@ -2,8 +2,6 @@ import { FasterSMA } from 'trading-signals';
2
2
 
3
3
  export const relativeVolume = (main, index, size, {lag}) => {
4
4
 
5
-
6
-
7
5
  const key = `relative_volume_${size}`;
8
6
  const { instances, verticalOhlcv } = main;
9
7
 
@@ -9,11 +9,11 @@ export const vidya = (main, index, size, momentum, options = {}) => {
9
9
  // Initialization on the first bar
10
10
  if (index === 0) {
11
11
  if (!(target in verticalOhlcv)) {
12
- throw new Error(`VIDYA could not find target "${target}"`);
12
+ throw new Error(`VIDYA could not find target "${target}"`)
13
13
  }
14
14
 
15
- const prefix = `vidya_${indicatorKey}`;
16
- const keyBase = target === 'close' ? prefix : `${prefix}_${target}`;
15
+ const prefix = `vidya_${indicatorKey}`
16
+ const keyBase = target === 'close' ? prefix : `${prefix}_${target}`
17
17
  const keys = {
18
18
  vidya: keyBase,
19
19
  upper: `${prefix}_upper`,
@@ -21,7 +21,7 @@ export const vidya = (main, index, size, momentum, options = {}) => {
21
21
  delta_vol: `${prefix}_delta_vol`,
22
22
  cross: `${prefix}_cross`,
23
23
  liquidity: `${prefix}_liquidity`,
24
- };
24
+ }
25
25
 
26
26
  const priceBasedVidyaKeys = [keyBase, keys.upper, keys.lower, keys.liquidity]
27
27
 
@@ -63,13 +63,13 @@ export const vidya = (main, index, size, momentum, options = {}) => {
63
63
  };
64
64
 
65
65
  // Series inputs
66
- const price = verticalOhlcv[target][index];
67
- const high = verticalOhlcv.high[index];
68
- const low = verticalOhlcv.low[index];
69
- const close = verticalOhlcv.close[index];
70
- const open = verticalOhlcv.open[index];
71
- const vol = verticalOhlcv.volume[index];
72
- const prevCross = index > 0 ? verticalOhlcv[keys.cross][index - 1] : 0;
66
+ const price = verticalOhlcv[target][index]
67
+ const high = verticalOhlcv.high[index]
68
+ const low = verticalOhlcv.low[index]
69
+ const close = verticalOhlcv.close[index]
70
+ const open = verticalOhlcv.open[index]
71
+ const vol = verticalOhlcv.volume[index]
72
+ const prevCross = index > 0 ? verticalOhlcv[keys.cross][index - 1] : 0
73
73
 
74
74
  const highestHigh = (inst.liquidity.high.length === liquidityLookback) ? Math.max(...inst.liquidity.high) : NaN
75
75
  const lowestLow = (inst.liquidity.low.length === liquidityLookback) ? Math.min(...inst.liquidity.low) : NaN
@@ -81,77 +81,77 @@ export const vidya = (main, index, size, momentum, options = {}) => {
81
81
  high - low,
82
82
  Math.abs(high - prevClose),
83
83
  Math.abs(low - prevClose)
84
- );
85
- inst.atrBuffer.push(tr);
86
- if (inst.atrBuffer.length > atrLength) inst.atrBuffer.shift();
84
+ )
85
+ inst.atrBuffer.push(tr)
86
+ if (inst.atrBuffer.length > atrLength) inst.atrBuffer.shift()
87
87
  }
88
88
  const atr = inst.atrBuffer.length === atrLength
89
89
  ? inst.atrBuffer.reduce((a, b) => a + b, 0) / atrLength
90
- : NaN;
90
+ : NaN
91
91
 
92
92
  // Compute raw VIDYA
93
- let rawVidya = NaN;
94
- let mom = NaN;
95
- if (index > 0) mom = price - verticalOhlcv[target][index - 1];
96
- inst.momentumBuffer.push(mom);
97
- if (inst.momentumBuffer.length > momentum) inst.momentumBuffer.shift();
93
+ let rawVidya = NaN
94
+ let mom = NaN
95
+ if (index > 0) mom = price - verticalOhlcv[target][index - 1]
96
+ inst.momentumBuffer.push(mom)
97
+ if (inst.momentumBuffer.length > momentum) inst.momentumBuffer.shift()
98
98
  if (inst.momentumBuffer.length === momentum) {
99
- let sumPos = 0, sumNeg = 0;
99
+ let sumPos = 0, sumNeg = 0
100
100
  inst.momentumBuffer.forEach(m => m >= 0 ? sumPos += m : sumNeg += -m);
101
- const denom = sumPos + sumNeg;
101
+ const denom = sumPos + sumNeg
102
102
  const cmo = denom === 0 ? 0 : Math.abs((sumPos - sumNeg) / denom) * 100;
103
- const alpha = 2 / (size + 1);
104
- const a = alpha * (cmo / 100);
103
+ const alpha = 2 / (size + 1)
104
+ const a = alpha * (cmo / 100)
105
105
  rawVidya = isNaN(inst.prevVidya) ? price : (a * price + (1 - a) * inst.prevVidya);
106
- inst.prevVidya = rawVidya;
106
+ inst.prevVidya = rawVidya
107
107
  }
108
108
 
109
109
  // Smooth VIDYA using fixed 15-bar SMA
110
- inst.sma.update(rawVidya);
111
- let smoothedVidya = NaN;
112
- try { smoothedVidya = inst.sma.getResult(); } catch {}
110
+ inst.sma.update(rawVidya)
111
+ let smoothedVidya = NaN
112
+ try { smoothedVidya = inst.sma.getResult() } catch {}
113
113
 
114
114
  // Bands
115
115
  const upper = (!isNaN(atr) && !isNaN(smoothedVidya))
116
116
  ? smoothedVidya + atr * bandDistance
117
- : NaN;
117
+ : NaN
118
118
  const lower = (!isNaN(atr) && !isNaN(smoothedVidya))
119
119
  ? smoothedVidya - atr * bandDistance
120
- : NaN;
120
+ : NaN
121
121
 
122
122
  // Detect crosses
123
- const prevPrice = index > 0 ? verticalOhlcv[target][index - 1] : NaN;
124
- const prevUpper = index > 0 ? verticalOhlcv[keys.upper][index - 1] : NaN;
125
- const prevLower = index > 0 ? verticalOhlcv[keys.lower][index - 1] : NaN;
126
- const crossUp = index > 0 && prevPrice <= prevUpper && price > upper;
127
- const crossDown = index > 0 && prevPrice >= prevLower && price < lower;
123
+ const prevPrice = index > 0 ? verticalOhlcv[target][index - 1] : NaN
124
+ const prevUpper = index > 0 ? verticalOhlcv[keys.upper][index - 1] : NaN
125
+ const prevLower = index > 0 ? verticalOhlcv[keys.lower][index - 1] : NaN
126
+ const crossUp = index > 0 && prevPrice <= prevUpper && price > upper
127
+ const crossDown = index > 0 && prevPrice >= prevLower && price < lower
128
128
 
129
129
  // Update trend state
130
- if (crossUp) inst.isTrendUp = true;
131
- if (crossDown) inst.isTrendUp = false;
130
+ if (crossUp) inst.isTrendUp = true
131
+ if (crossDown) inst.isTrendUp = false
132
132
 
133
133
  // Cumulative cross counter: positive when uptrend, negative when downtrend
134
134
  let cross = 0;
135
135
  if (index > 0) {
136
136
  if (inst.isTrendUp) {
137
- cross = prevCross > 0 ? prevCross + 1 : 1;
137
+ cross = prevCross > 0 ? prevCross + 1 : 1
138
138
  } else {
139
- cross = prevCross < 0 ? prevCross - 1 : -1;
139
+ cross = prevCross < 0 ? prevCross - 1 : -1
140
140
  }
141
141
  }
142
142
 
143
143
  // Volume accumulation
144
144
  if (crossUp || crossDown) {
145
- inst.upVol = inst.downVol = 0;
145
+ inst.upVol = inst.downVol = 0
146
146
  } else {
147
- inst.upVol += (close > open ? vol : 0);
148
- inst.downVol += (close < open ? vol : 0);
147
+ inst.upVol += (close > open ? vol : 0)
148
+ inst.downVol += (close < open ? vol : 0)
149
149
  }
150
- const avgVol = (inst.upVol + inst.downVol) / 2 || 1;
151
- const deltaPct = (inst.upVol - inst.downVol) / avgVol;
150
+ const avgVol = (inst.upVol + inst.downVol) / 2 || 1
151
+ const deltaPct = (inst.upVol - inst.downVol) / avgVol
152
152
 
153
153
  // Trend-based boundary (reset on crossover)
154
- const trendValue = (inst.isTrendUp) ? lower : upper;
154
+ const trendValue = (inst.isTrendUp) ? lower : upper
155
155
  let liquidity = NaN
156
156
 
157
157
  if(cross > 0)
@@ -164,12 +164,12 @@ export const vidya = (main, index, size, momentum, options = {}) => {
164
164
  }
165
165
 
166
166
  // Push outputs
167
- main.pushToMain({ index, key: keys.vidya, value: trendValue });
168
- main.pushToMain({ index, key: keys.upper, value: upper });
169
- main.pushToMain({ index, key: keys.lower, value: lower });
170
- main.pushToMain({ index, key: keys.delta_vol, value: deltaPct });
171
- main.pushToMain({ index, key: keys.cross, value: cross });
172
- main.pushToMain({ index, key: keys.liquidity, value: liquidity });
167
+ main.pushToMain({ index, key: keys.vidya, value: trendValue })
168
+ main.pushToMain({ index, key: keys.upper, value: upper })
169
+ main.pushToMain({ index, key: keys.lower, value: lower })
170
+ main.pushToMain({ index, key: keys.delta_vol, value: deltaPct })
171
+ main.pushToMain({ index, key: keys.cross, value: cross })
172
+ main.pushToMain({ index, key: keys.liquidity, value: liquidity })
173
173
 
174
174
  inst.liquidity.high.push(high)
175
175
  inst.liquidity.low.push(low)
@@ -177,5 +177,5 @@ export const vidya = (main, index, size, momentum, options = {}) => {
177
177
  if(inst.liquidity.high.length > liquidityLookback) inst.liquidity.high.shift()
178
178
  if(inst.liquidity.low.length > liquidityLookback) inst.liquidity.low.shift()
179
179
 
180
- return true;
181
- };
180
+ return true
181
+ }
@@ -1,12 +1,9 @@
1
- export const mapCols = (main, index, newCols, callback, {lag}) => {
1
+ export const mapCols = (main, index, newCols, callback, {lag, isPriceBased}) => {
2
2
 
3
-
4
-
5
- const {verticalOhlcv, len} = main
3
+ const {verticalOhlcv, len, priceBased, precision} = main
6
4
 
7
5
  if(index === 0)
8
6
  {
9
-
10
7
  for(const key of newCols)
11
8
  {
12
9
  if(verticalOhlcv.hasOwnProperty(key)) {
@@ -14,6 +11,11 @@ export const mapCols = (main, index, newCols, callback, {lag}) => {
14
11
  }
15
12
 
16
13
  verticalOhlcv[key] = new Array(len).fill(0)
14
+
15
+ if(precision && isPriceBased)
16
+ {
17
+ priceBased.add(key)
18
+ }
17
19
  }
18
20
 
19
21
  if(lag > 0)
@@ -35,12 +37,10 @@ export const mapCols = (main, index, newCols, callback, {lag}) => {
35
37
  export const defaultMapColsCallback = ({main, index}) => {
36
38
  const {verticalOhlcv} = main
37
39
 
38
- const currClose = verticalOhlcv.close[index]
39
- const prevClose = verticalOhlcv.close[index-1]
40
+ const close = verticalOhlcv.close[index]
41
+ const open = verticalOhlcv.open[index-1]
40
42
 
41
- if(typeof prevClose === 'undefined') return null
42
-
43
43
  return {
44
- change: (currClose - prevClose) / prevClose
44
+ change: ((close - open) / open) * 100
45
45
  }
46
46
  }
@@ -1,98 +1,109 @@
1
- // ------------------------------
2
- // calcPrecisionMultiplier (unchanged semantics)
3
- // ------------------------------
1
+
2
+ //calcPrecisionMultiplier is calculated once in the class constructor, using the open, high, low and close values of the first row
3
+ //the multiplier (min 10000) is store in constructor
4
+
4
5
  export const calcPrecisionMultiplier = main => {
5
- const { precision, firstRow, initialPriceBased } = main;
6
+ const { precision, firstRow, initialPriceBased } = main
6
7
  if (precision === false) return 1
7
8
 
8
- const multipliers = []
9
-
10
- const err = 'If "precision" is set to true all the "open", "high", "low" and "close" values must be numeric strings like "123.45" with no commas or currency symbols.'
9
+ let maxMultiplier = 1
11
10
 
12
11
  for (const [key, strNum] of Object.entries(firstRow)) {
12
+ if (!initialPriceBased.has(key)) continue
13
+
14
+ if (typeof strNum !== 'string' || !/^\d+(\.\d+)?$/.test(strNum)) {
15
+ throw new Error(
16
+ `If "precision" is set to true all the "open", "high", "low" and "close" values must be numeric strings like "123.45" with no commas or currency symbols. Property "${key}" had the value "${strNum}" type ${typeof strNum}.`
17
+ )
18
+ }
19
+
20
+ const [integerStr, decimals = ''] = strNum.split('.')
21
+ const integer = Number(integerStr)
22
+
23
+ let decimalPrecision = 4
24
+
25
+ // Treat any non-zero integer (positive or negative) as "normal"
26
+ if (Math.abs(integer) === 0) {
27
+ const decimalsLen = decimals.length
28
+ const countDecimals = Math.max(4, decimalsLen)
29
+
30
+ // index of first non-zero decimal digit, -1 if none
31
+ const firstNonZeroIndex = decimals.search(/[1-9]/)
13
32
 
14
- if(!initialPriceBased.has(key)) continue
15
-
16
- if (typeof strNum !== 'string' || !/^-?\d+(\.\d+)?$/.test(strNum)) {
17
- throw new Error(`${err}
18
- Property "${key}" had the value "${strNum}" type ${typeof strNum}.`)
33
+ if (firstNonZeroIndex === -1) {
34
+ // 0, 0.0, 0.0000... -> nothing special to preserve
35
+ decimalPrecision = countDecimals
36
+ } else {
37
+ // Give extra headroom after the first non-zero digit
38
+ const extra = 5 // tweak as you like
39
+ const needed = firstNonZeroIndex + 1 + extra
40
+ decimalPrecision = Math.max(countDecimals, needed)
41
+ }
19
42
  }
20
43
 
21
- if (!new Set(['open', 'high', 'low', 'close']).has(key)) continue;
44
+ // Optional: cap exponent to keep numbers sane
45
+ const multiplier = 10 ** decimalPrecision
22
46
 
23
- const [, decimals = ''] = strNum.split('.')
24
- const decimalPrecision = Math.max(4, decimals.length)
25
- multipliers.push(Math.pow(10, decimalPrecision)) // returns Number
47
+ if (multiplier > maxMultiplier) maxMultiplier = multiplier
26
48
  }
27
49
 
28
- if (multipliers.length === 0) return 1
29
- return Math.max(...multipliers)
50
+ return maxMultiplier
30
51
  }
31
52
 
32
- // ------------------------------
33
- // Precise BigInt-based helpers (no `n` literals)
34
- // ------------------------------
35
- export const addPrecisionExact = (strNum, mul) => {
53
+
54
+ //this function converts the strNum into a integer * multiplier
55
+ export const addPrecisionAsNumber = (strNum, multiplier) => {
36
56
  if (typeof strNum !== 'string') throw new Error('addPrecisionExact expects a string when precision=true')
37
57
 
38
58
  const negative = strNum.charAt(0) === '-'
39
59
  const s = negative ? strNum.slice(1) : strNum
40
60
  const [intPart, decPart = ''] = s.split('.')
41
61
 
42
- // Compute decimals from mul (mul expected as 10^decimals)
43
- const mulStr = String(mul);
44
- const decimals = (mul === 1) ? 0 : (mulStr.length - 1)
62
+ // Compute decimals from multiplier (multiplier expected as 10^decimals)
63
+ const mulStr = String(multiplier);
64
+ const decimals = (multiplier === 1) ? 0 : (mulStr.length - 1)
45
65
 
46
66
  const decPadded = decPart.padEnd(decimals, '0').slice(0, decimals); // pad or trim
47
67
  const integerString = intPart + decPadded // e.g. "12" + "3400" => "123400"
48
68
 
49
- // Use BigInt constructor (no n-literals)
50
- let result = BigInt(integerString || '0'); // handle "0" or empty
69
+ let result = parseInt(integerString || '0', 10)
70
+
51
71
  if (negative) result = -result
72
+
52
73
  return result
53
74
  }
54
75
 
55
- export const revertPrecisionExact = (num, mul) => {
56
-
57
-
58
- // Convert input to BigInt without using literal suffixes
59
- const big = (typeof num === 'bigint') ? num : BigInt(parseInt(num))
60
- const neg = big < BigInt(0)
61
- const abs = neg ? -big : big
62
76
 
63
- const mulBig = BigInt(mul);
64
- if (mulBig === BigInt(1)) {
65
- const s = abs.toString()
66
- return neg ? `-${s}` : s
77
+ //this function converts the outputs the number generated in addPrecisionAsNumber / multiplier
78
+ export const revertPrecisionAsNumber = (num, multiplier) => {
79
+ if (typeof num !== 'number' || !Number.isFinite(num)) {
80
+ throw new Error('revertPrecisionAsNumber expects a finite number')
67
81
  }
68
82
 
69
- const intPart = abs / mulBig
70
- const fracPart = abs % mulBig
83
+ const negative = num < 0
84
+ let abs = Math.abs(num)
71
85
 
72
- const decimals = String(mul).length - 1
73
- const fracStr = fracPart.toString().padStart(decimals, '0')
86
+ // Make sure we're working with an integer, mirroring addPrecisionAsNumber
87
+ abs = Math.trunc(abs)
74
88
 
75
- // Remove trailing zeros in fractional part (optional)
76
- const trimmedFrac = fracStr.replace(/0+$/, '')
77
- const result = trimmedFrac.length === 0
78
- ? intPart.toString()
79
- : `${intPart.toString()}.${trimmedFrac}`
89
+ const decimals = (multiplier === 1) ? 0 : (String(multiplier).length - 1)
80
90
 
81
- return neg ? `-${result}` : result
82
- }
91
+ if (decimals === 0) {
92
+ // No fractional part; just restore sign
93
+ return negative ? -abs : abs
94
+ }
83
95
 
84
- // ------------------------------
85
- // Convenience wrappers (Numbers) — may lose precision
86
- // ------------------------------
87
- export const addPrecisionAsNumber = (strNum, mul) => {
96
+ // Integer division and remainder
97
+ const intPart = Math.trunc(abs / multiplier)
98
+ const fracPart = abs % multiplier
88
99
 
89
- const big = addPrecisionExact(strNum, mul)
90
-
91
- return Number(big)
92
- }
100
+ // Build fractional string with leading zeros, then trim trailing zeros
101
+ let fracStr = String(fracPart).padStart(decimals, '0')
102
+ fracStr = fracStr.replace(/0+$/, '')
93
103
 
94
- export const revertPrecisionAsNumber = (num, mul) => {
104
+ const resultStr = fracStr.length === 0
105
+ ? String(intPart)
106
+ : `${intPart}.${fracStr}`
95
107
 
96
- const asStr = revertPrecisionExact(num, mul)
97
- return Number(asStr)
98
- }
108
+ return Number(negative ? `-${resultStr}` : resultStr)
109
+ }
@@ -2,7 +2,7 @@
2
2
  import {FasterATR, FasterWSMA} from 'trading-signals';
3
3
 
4
4
  export const atr = (main, index, size, {lag, percentage, upper, lower}) => {
5
- const { verticalOhlcv, instances } = main
5
+ const { verticalOhlcv, instances, priceBased } = main
6
6
  const baseKeyName = `atr_${size}`
7
7
 
8
8
  if (index === 0) {
@@ -20,6 +20,7 @@ export const atr = (main, index, size, {lag, percentage, upper, lower}) => {
20
20
  for(const k of keyNames)
21
21
  {
22
22
  verticalOhlcv[k] = new Float64Array(len).fill(NaN)
23
+ priceBased.add(k)
23
24
  }
24
25
 
25
26
  if(lag > 0)