ohlcv-indicators 3.5.52 → 3.5.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,113 +1,137 @@
1
1
  import { FasterEMA } from 'trading-signals';
2
2
 
3
- export const heikenAshi = (main, index, smoothLength, afterSmoothLength, {lag}) => {
4
- const { verticalOhlcv, instances, len } = main
5
- const prefix = `heiken_ashi_${smoothLength}_${afterSmoothLength}`
6
- const keys = ['open', 'high', 'low', 'close']
7
-
8
- // ---- INIT ----
9
- if (index === 0) {
10
-
11
- // Initialize pre-smoothing EMA instances and buffers
12
- instances[prefix] = {
13
- emaPre: Object.fromEntries(keys.map(k => [k, new FasterEMA(smoothLength)])),
14
- emaPost: Object.fromEntries(keys.map(k => [k, new FasterEMA(afterSmoothLength)])),
15
- prevHaOpen: NaN,
16
- prevHaClose: NaN,
17
- isTrendUp: false,
18
- };
19
-
20
- const keyNames = keys.map(k => `${prefix}_${k}`)
21
-
22
- const verticalOhlcvSetup = Object.fromEntries([...keyNames, `${prefix}_cross`].map(v => [v, new Float64Array(len).fill(NaN)]))
3
+ export const heikenAshi = (main, index, smoothLength, afterSmoothLength, { lag = 0, bothNull = false } = {}) => {
4
+ const { verticalOhlcv, instances, len, priceBased } = main;
5
+ const prefix = (bothNull) ? 'heiken_ashi' : `heiken_ashi_${smoothLength}_${afterSmoothLength}`;
6
+ const keys = ['open', 'high', 'low', 'close'];
7
+
8
+ // ---- INIT ----
9
+ if (index === 0) {
10
+ // Initialize state
11
+ instances[prefix] = {
12
+ // Only allocate EMAs when smoothing is requested
13
+ emaPre: !bothNull
14
+ ? Object.fromEntries(keys.map(k => [k, new FasterEMA(smoothLength)]))
15
+ : null,
16
+ emaPost: !bothNull
17
+ ? Object.fromEntries(keys.map(k => [k, new FasterEMA(afterSmoothLength)]))
18
+ : null,
19
+ prevHaOpen: NaN,
20
+ prevHaClose: NaN,
21
+ isTrendUp: false,
22
+ };
23
+
24
+ const keyNames = keys.map(k => `${prefix}_${k}`);
25
+ const verticalOhlcvSetup = Object.fromEntries(
26
+ [...keyNames, `${prefix}_cross`].map(v => [v, new Float64Array(len).fill(NaN)])
27
+ );
28
+
29
+ for(const k of keyNames) {
30
+ priceBased.add(k)
31
+ }
23
32
 
24
- Object.assign(verticalOhlcv, {...verticalOhlcvSetup})
33
+ Object.assign(verticalOhlcv, { ...verticalOhlcvSetup });
25
34
 
26
- if(lag > 0)
27
- {
28
- main.lag(keyNames, lag)
29
- }
35
+ if (lag > 0) {
36
+ main.lag(keyNames, lag);
30
37
  }
38
+ }
31
39
 
32
- // ---- FETCH RAW INPUTS ----
33
- const open = verticalOhlcv.open[index]
34
- const high = verticalOhlcv.high[index]
35
- const low = verticalOhlcv.low[index]
36
- const close = verticalOhlcv.close[index]
37
- const inst = instances[prefix]
40
+ // ---- FETCH RAW INPUTS ----
41
+ const open = verticalOhlcv.open[index];
42
+ const high = verticalOhlcv.high[index];
43
+ const low = verticalOhlcv.low[index];
44
+ const close = verticalOhlcv.close[index];
45
+ const inst = instances[prefix];
38
46
 
39
- // ---- PRE-SMOOTHING (EMA) ----
40
- inst.emaPre.open.update(open)
41
- inst.emaPre.high.update(high)
42
- inst.emaPre.low.update(low)
43
- inst.emaPre.close.update(close)
47
+ let sOpen, sHigh, sLow, sClose;
44
48
 
45
- let sOpen, sHigh, sLow, sClose
49
+ if (!bothNull) {
50
+ // ---- PRE-SMOOTHING (EMA) ----
51
+ inst.emaPre.open.update(open);
52
+ inst.emaPre.high.update(high);
53
+ inst.emaPre.low.update(low);
54
+ inst.emaPre.close.update(close);
46
55
 
47
56
  try {
48
- sOpen = inst.emaPre.open.getResult()
49
- sHigh = inst.emaPre.high.getResult()
50
- sLow = inst.emaPre.low.getResult()
51
- sClose = inst.emaPre.close.getResult()
57
+ sOpen = inst.emaPre.open.getResult();
58
+ sHigh = inst.emaPre.high.getResult();
59
+ sLow = inst.emaPre.low.getResult();
60
+ sClose = inst.emaPre.close.getResult();
52
61
  } catch {
53
- return true; // skip if not enough data
62
+ return true; // skip if not enough data
54
63
  }
55
-
56
- // ---- HEIKEN ASHI CORE ----
57
- const haClose = (sOpen + sHigh + sLow + sClose) / 4
58
- const haOpen = (Number.isNaN(inst.prevHaOpen) || Number.isNaN(inst.prevHaClose)) ? (sOpen + sClose) / 2 : (inst.prevHaOpen + inst.prevHaClose) / 2
59
- const haHigh = Math.max(sHigh, haOpen, haClose)
60
- const haLow = Math.min(sLow, haOpen, haClose)
61
-
62
- // Store for next round
63
- inst.prevHaOpen = haOpen
64
- inst.prevHaClose = haClose
65
-
64
+ } else {
65
+ // No pre-smoothing: use raw OHLC
66
+ sOpen = open;
67
+ sHigh = high;
68
+ sLow = low;
69
+ sClose = close;
70
+ }
71
+
72
+ // ---- HEIKEN ASHI CORE (from s*) ----
73
+ const haClose = (sOpen + sHigh + sLow + sClose) / 4;
74
+ const haOpen = (Number.isNaN(inst.prevHaOpen) || Number.isNaN(inst.prevHaClose))
75
+ ? (sOpen + sClose) / 2
76
+ : (inst.prevHaOpen + inst.prevHaClose) / 2;
77
+ const haHigh = Math.max(sHigh, haOpen, haClose);
78
+ const haLow = Math.min(sLow, haOpen, haClose);
79
+
80
+ // Store for next round
81
+ inst.prevHaOpen = haOpen;
82
+ inst.prevHaClose = haClose;
83
+
84
+ let smOpen, smHigh, smLow, smClose;
85
+
86
+ if (!bothNull) {
66
87
  // ---- POST-SMOOTHING (EMA) ----
67
- inst.emaPost.open.update(haOpen)
68
- inst.emaPost.high.update(haHigh)
69
- inst.emaPost.low.update(haLow)
70
- inst.emaPost.close.update(haClose)
71
-
72
- let smOpen, smHigh, smLow, smClose
88
+ inst.emaPost.open.update(haOpen);
89
+ inst.emaPost.high.update(haHigh);
90
+ inst.emaPost.low.update(haLow);
91
+ inst.emaPost.close.update(haClose);
73
92
 
74
93
  try {
75
- smOpen = inst.emaPost.open.getResult()
76
- smHigh = inst.emaPost.high.getResult()
77
- smLow = inst.emaPost.low.getResult()
78
- smClose = inst.emaPost.close.getResult()
94
+ smOpen = inst.emaPost.open.getResult();
95
+ smHigh = inst.emaPost.high.getResult();
96
+ smLow = inst.emaPost.low.getResult();
97
+ smClose = inst.emaPost.close.getResult();
79
98
  } catch {
80
- return true
99
+ return true; // skip if not enough data
81
100
  }
82
-
83
- // Cumulative cross counter: positive when uptrend, negative when downtrend
84
- const prevCross = index > 0 ? verticalOhlcv[`${prefix}_cross`][index - 1] : 0
85
- const prevHaOpen = index > 0 ? verticalOhlcv[`${prefix}_open`][index - 1] : NaN
86
- const prevHaClose = index > 0 ? verticalOhlcv[`${prefix}_close`][index - 1] : NaN
87
- const crossUp = !Number.isNaN(prevHaClose) && prevHaClose <= prevHaOpen && smClose > smOpen
88
- const crossDown = !Number.isNaN(prevHaClose) && prevHaClose >= prevHaOpen && smClose < smOpen
89
-
90
-
91
- if (crossUp) inst.isTrendUp = true;
92
- if (crossDown) inst.isTrendUp = false;
93
-
94
- let cross = 0
95
-
96
- if (index > 0) {
97
- if (inst.isTrendUp) {
98
- cross = prevCross > 0 ? prevCross + 1 : 1
99
- } else {
100
- cross = prevCross < 0 ? prevCross - 1 : -1
101
- }
101
+ } else {
102
+ // No post-smoothing: final values are plain HA
103
+ smOpen = haOpen;
104
+ smHigh = haHigh;
105
+ smLow = haLow;
106
+ smClose = haClose;
107
+ }
108
+
109
+ // ---- TREND/CROSS LOGIC (works for both modes) ----
110
+ const prevCross = index > 0 ? verticalOhlcv[`${prefix}_cross`][index - 1] : 0;
111
+ const prevHaOpenArr = index > 0 ? verticalOhlcv[`${prefix}_open`][index - 1] : NaN;
112
+ const prevHaCloseArr = index > 0 ? verticalOhlcv[`${prefix}_close`][index - 1] : NaN;
113
+
114
+ const crossUp = !Number.isNaN(prevHaCloseArr) && prevHaCloseArr <= prevHaOpenArr && smClose > smOpen;
115
+ const crossDown = !Number.isNaN(prevHaCloseArr) && prevHaCloseArr >= prevHaOpenArr && smClose < smOpen;
116
+
117
+ if (crossUp) inst.isTrendUp = true;
118
+ if (crossDown) inst.isTrendUp = false;
119
+
120
+ let cross = 0;
121
+ if (index > 0) {
122
+ if (inst.isTrendUp) {
123
+ cross = prevCross > 0 ? prevCross + 1 : 1;
124
+ } else {
125
+ cross = prevCross < 0 ? prevCross - 1 : -1;
102
126
  }
127
+ }
103
128
 
104
- // ---- PUSH OUTPUTS ----
105
- main.pushToMain({ index, key: `${prefix}_open`, value: smOpen })
106
- main.pushToMain({ index, key: `${prefix}_high`, value: smHigh })
107
- main.pushToMain({ index, key: `${prefix}_low`, value: smLow })
108
- main.pushToMain({ index, key: `${prefix}_close`, value: smClose })
109
- main.pushToMain({ index, key: `${prefix}_cross`, value: cross })
110
-
129
+ // ---- PUSH OUTPUTS ----
130
+ main.pushToMain({ index, key: `${prefix}_open`, value: smOpen });
131
+ main.pushToMain({ index, key: `${prefix}_high`, value: smHigh });
132
+ main.pushToMain({ index, key: `${prefix}_low`, value: smLow });
133
+ main.pushToMain({ index, key: `${prefix}_close`, value: smClose });
134
+ main.pushToMain({ index, key: `${prefix}_cross`, value: cross });
111
135
 
112
- return true
113
- }
136
+ return true;
137
+ };
@@ -6,7 +6,7 @@ export const macd = (main, index, fast, slow, signal, {target, lag, precomputed}
6
6
 
7
7
 
8
8
 
9
- const { verticalOhlcv, instances } = main
9
+ const { verticalOhlcv, instances, priceBased } = main
10
10
  const {instanceKey} = precomputed
11
11
 
12
12
  // Initialization on the first index.
@@ -52,6 +52,10 @@ export const macd = (main, index, fast, slow, signal, {target, lag, precomputed}
52
52
 
53
53
  const keyNames = [diffKey, deaKey, histogramKey]
54
54
 
55
+ for(const k of keyNames) {
56
+ priceBased.add(k)
57
+ }
58
+
55
59
  if(lag > 0)
56
60
  {
57
61
  main.lag(keyNames, lag)
@@ -10,7 +10,7 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
10
10
 
11
11
 
12
12
 
13
- const { verticalOhlcv, instances } = main
13
+ const { verticalOhlcv, instances, priceBased } = main
14
14
  const suffix = (target !== 'close') ? `_${target}` : ''
15
15
  const keyName = `${methodName}_${size}${suffix}`
16
16
 
@@ -29,6 +29,10 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
29
29
 
30
30
  verticalOhlcv[keyName] = new Float64Array(len).fill(NaN)
31
31
 
32
+ if(priceBased.has(target)){
33
+ priceBased.add(keyName)
34
+ }
35
+
32
36
  if(lag > 0)
33
37
  {
34
38
  main.lag([keyName], lag)
@@ -2,7 +2,7 @@ import { FasterSMA } from 'trading-signals';
2
2
  // Variable Index Dynamic Average (VIDYA) indicator with volume stats
3
3
 
4
4
  export const vidya = (main, index, size, momentum, options = {}) => {
5
- const { verticalOhlcv, instances, len } = main;
5
+ const { verticalOhlcv, instances, len, priceBased } = main;
6
6
  const { target, lag, atrLength = 200, bandDistance = 2, liquidityLookback = 5 } = options
7
7
  const indicatorKey = `${size}_${momentum}`;
8
8
 
@@ -23,6 +23,12 @@ export const vidya = (main, index, size, momentum, options = {}) => {
23
23
  liquidity: `${prefix}_liquidity`,
24
24
  };
25
25
 
26
+ const priceBasedVidyaKeys = [keyBase, keys.upper, keys.lower, keys.liquidity]
27
+
28
+ for(const k of priceBasedVidyaKeys) {
29
+ priceBased.add(k)
30
+ }
31
+
26
32
  // Prepare instances container
27
33
  if (!instances.vidya) instances.vidya = { settings: {} };
28
34
  instances.vidya.settings[indicatorKey] = {
@@ -0,0 +1,74 @@
1
+ export const volumeDelta = (main, index, { lag = 0 }) => {
2
+ const { verticalOhlcv, instances, len } = main;
3
+ const groupName = 'volume_delta';
4
+
5
+ if (index === 0) {
6
+ if (!instances[groupName]) instances[groupName] = { isBuyVolume: true, cross: 0 };
7
+
8
+ const keys = [
9
+ `${groupName}_open`,
10
+ `${groupName}_high`,
11
+ `${groupName}_low`,
12
+ `${groupName}_close`,
13
+ `${groupName}_cross`,
14
+ ];
15
+
16
+ // allocate outputs
17
+ for (const k of keys) verticalOhlcv[k] = new Float64Array(len).fill(NaN);
18
+
19
+ // optional lag
20
+ if (lag > 0) main.lag(keys, lag);
21
+ }
22
+
23
+ const inst = instances[groupName];
24
+
25
+ // inputs
26
+ const vol = verticalOhlcv.volume[index];
27
+ const open = verticalOhlcv.open[index];
28
+ const close = verticalOhlcv.close[index];
29
+ const prevClose = index > 0 ? verticalOhlcv.close[index - 1] : NaN;
30
+
31
+ // TradingView tie-breaks:
32
+ // 1) close>open -> buy, 2) close<open -> sell,
33
+ // 3) equal: compare to prev close, 4) still equal: carry prior state
34
+ let isBuy = inst.isBuyVolume;
35
+ if (close > open) isBuy = true;
36
+ else if (close < open) isBuy = false;
37
+ else if (close > prevClose) isBuy = true;
38
+ else if (close < prevClose) isBuy = false;
39
+
40
+ inst.isBuyVolume = isBuy;
41
+
42
+ if(index < 1)
43
+ {
44
+ if(isBuy) inst.cross = 1
45
+ else inst.cross = -1
46
+ }
47
+ else {
48
+ if(isBuy)
49
+ {
50
+ if(inst.cross > 0) inst.cross = inst.cross + 1
51
+ else inst.cross = 1 //reset
52
+ } else {
53
+ if(inst.cross > 0) inst.cross = -1 //reset
54
+ else inst.cross = inst.cross - 1
55
+ }
56
+ }
57
+
58
+
59
+ const delta = Number.isFinite(vol) ? (isBuy ? vol : -vol) : NaN;
60
+
61
+ // delta "candle"
62
+ const openVal = Number.isNaN(delta) ? NaN : 0;
63
+ const closeVal = delta;
64
+ const highVal = Number.isNaN(delta) ? NaN : Math.max(delta, 0);
65
+ const lowVal = Number.isNaN(delta) ? NaN : Math.min(delta, 0);
66
+
67
+ main.pushToMain({ index, key: `${groupName}_open`, value: openVal });
68
+ main.pushToMain({ index, key: `${groupName}_high`, value: highVal });
69
+ main.pushToMain({ index, key: `${groupName}_low`, value: lowVal });
70
+ main.pushToMain({ index, key: `${groupName}_close`, value: closeVal });
71
+ main.pushToMain({ index, key: `${groupName}_cross`, value: inst.cross });
72
+
73
+ return true;
74
+ };
@@ -1,5 +1,6 @@
1
1
  import { oneHotEncode } from "../machine-learning/ml-utilities.js"
2
2
  import { buildArray } from "../utilities/assignTypes.js"
3
+ import { addPrecisionAsNumber } from "../utilities/precisionMultiplier.js"
3
4
  const eq = (fast, slow) => fast === slow
4
5
  const gt = (fast, slow) => fast > slow
5
6
  const lt = (fast, slow) => fast < slow
@@ -132,9 +133,9 @@ class CrossInstance {
132
133
 
133
134
  }
134
135
 
135
- export const crossPairs = (main, index, crossPairsList) => {
136
+ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
136
137
 
137
- const {verticalOhlcv, verticalOhlcvTempCols, instances, len} = main
138
+ const {verticalOhlcv, verticalOhlcvTempCols, instances, len, precision, precisionMultiplier, priceBased} = main
138
139
 
139
140
  for (const { fast, slow } of crossPairsList) {
140
141
 
@@ -147,7 +148,15 @@ export const crossPairs = (main, index, crossPairsList) => {
147
148
 
148
149
  const col = slow.toString()
149
150
  verticalOhlcvTempCols.add(col)
150
- verticalOhlcv[col] = buildArray('Int16Array', len, slow)
151
+
152
+ const requiresPrecision = precision && (fast === 'price' || priceBased.has(fast))
153
+
154
+ const slowVal = requiresPrecision
155
+ ? addPrecisionAsNumber(col, precisionMultiplier)
156
+ : slow
157
+
158
+ const numArrType = (slow >= -125 && slow <= 125 && requiresPrecision === false) ? 'Int8Array' : 'Float64Array'
159
+ verticalOhlcv[col] = buildArray(numArrType, len, slowVal)
151
160
  }
152
161
 
153
162
  if(!instances.hasOwnProperty('crossPairs'))
@@ -162,10 +171,15 @@ export const crossPairs = (main, index, crossPairsList) => {
162
171
  max: -Infinity
163
172
  }
164
173
 
165
- const crossArrType = 'Int16Array'
174
+ const crossArrType = (limit !== null && limit <= 125) ? 'Int8Array' : 'Float64Array'
166
175
 
167
176
  verticalOhlcv[crossName] = buildArray(crossArrType, len, 0)
168
177
 
178
+ if(oneHot)
179
+ {
180
+ verticalOhlcv[`one_hot_${crossName}`] = buildArray('Array', len, null)
181
+ }
182
+
169
183
  } else if(index + 1 === len) {
170
184
  // sanity checks
171
185
  if (fast !== "price" && !verticalOhlcv.hasOwnProperty(fast)) {
@@ -197,9 +211,21 @@ export const crossPairs = (main, index, crossPairsList) => {
197
211
  run.update(index, { fast: fastVal, slow: slowVal })
198
212
  }
199
213
 
200
- const value = run.getResult()
214
+ const rawValue = run.getResult()
215
+ const value = (limit !== null) ? Math.min(Math.max(rawValue, -limit), limit) : rawValue
201
216
  main.pushToMain({index, key: crossName, value})
202
217
 
218
+ if(oneHot)
219
+ {
220
+ const oneHotIdx = value + limit;
221
+ const oneHotSize = 2 * limit + 1;
222
+ main.pushToMain({
223
+ index,
224
+ key: `one_hot_${crossName}`,
225
+ value: oneHotEncode(oneHotIdx, oneHotSize)
226
+ });
227
+ }
228
+
203
229
  }
204
230
 
205
231
  return true
@@ -4,32 +4,37 @@ export const lag = (main, index, colKeys, lookback) => {
4
4
 
5
5
 
6
6
 
7
- const { verticalOhlcv, len } = main
7
+ const { verticalOhlcv, len, priceBased } = main
8
8
 
9
9
  if(index === 0)
10
10
  {
11
- for (const colKey of colKeys) {
12
- if (!verticalOhlcv.hasOwnProperty(colKey)) {
13
- throw new Error(
14
- `Lag processing invoked by col "${colKey}" was not found in "verticalOhlcv".`
15
- )
11
+ for (const targetKey of colKeys) {
12
+ if (!verticalOhlcv.hasOwnProperty(targetKey)) {
13
+ throw new Error(
14
+ `Lag processing invoked by col "${targetKey}" was not found in "verticalOhlcv".`
15
+ )
16
16
  }
17
17
 
18
+ const addToPriceBased = priceBased.has(targetKey)
19
+
18
20
  for (let step = 1; step <= lookback; step++) {
19
- const key = `${colKey}_lag_${step}`
20
- const thisArrType = getArrayType(colKey, verticalOhlcv[colKey])
21
+ const key = `${targetKey}_lag_${step}`
22
+
23
+ if(addToPriceBased) priceBased.add(key)
24
+
25
+ const thisArrType = getArrayType(targetKey, verticalOhlcv[targetKey])
21
26
 
22
- verticalOhlcv[key] = buildArray(thisArrType, len)
27
+ verticalOhlcv[key] = buildArray(thisArrType, len)
23
28
  }
24
29
  }
25
30
  }
26
31
 
27
- for (const colKey of colKeys) {
28
- const currentColumn = verticalOhlcv[colKey]
32
+ for (const targetKey of colKeys) {
33
+ const currentColumn = verticalOhlcv[targetKey]
29
34
 
30
35
  // Populate the lagged values each tick:
31
36
  for (let step = 1; step <= lookback; step++) {
32
- const key = `${colKey}_lag_${step}`
37
+ const key = `${targetKey}_lag_${step}`
33
38
  const laggedIndex = index - step
34
39
  const value =
35
40
  laggedIndex < 0 || currentColumn[laggedIndex] === undefined
@@ -1,8 +1,6 @@
1
1
  import { classifyNum } from "./numberUtilities.js";
2
2
  import { selectDateFormatter } from "./dateUtilities.js";
3
3
 
4
- const numberKeys = new Set(['open', 'high', 'low', 'close'])
5
-
6
4
  export const buildArray = (arrayType, len, fallbackValue) => {
7
5
  const ctorMap = {
8
6
  Array: Array,
@@ -35,7 +33,7 @@ export const buildArray = (arrayType, len, fallbackValue) => {
35
33
  }
36
34
 
37
35
  export const assignTypes = main => {
38
- const {firstRow} = main
36
+ const {firstRow, precision, initialPriceBased} = main
39
37
  const inputTypes = {}
40
38
  const arrayTypes = {}
41
39
 
@@ -50,12 +48,12 @@ export const assignTypes = main => {
50
48
  inputTypes[colName] = classifyNum(cellValue, true)
51
49
  arrayTypes[colName] = 'Int32Array'
52
50
  }
53
- else if(numberKeys.has(colName))
51
+ else if(initialPriceBased.has(colName))
54
52
  {
55
- inputTypes[colName] = classifyNum(cellValue, true)
53
+ inputTypes[colName] = classifyNum(cellValue, true, precision)
56
54
  arrayTypes[colName] = 'Float64Array'
57
55
  } else {
58
- const thisType = classifyNum(cellValue, false)
56
+ const thisType = classifyNum(cellValue, false, precision)
59
57
  inputTypes[colName] = thisType
60
58
  arrayTypes[colName] = (thisType === 'number') ? 'Float64Array' : 'Array'
61
59
  }
@@ -1,52 +1,35 @@
1
- // Helper to clean non-numeric characters (except "-" at the start and decimal point)
2
- export const sanitizeDirtyNumberString = str => str.replace(/(?!^-)[^0-9.]/g, '');
1
+ import { addPrecisionAsNumber, revertPrecisionAsNumber } from "./precisionMultiplier.js";
3
2
 
4
- export const numberFormater = {
3
+ export const inputNumberFormatter = {
5
4
  number: n => n,
6
5
  numberCleanString: n => Number(n),
7
- numberDirtyString: n => Number(sanitizeDirtyNumberString(n))
6
+ precisionNumberCleanString: (strNum, multiplier) => addPrecisionAsNumber(strNum, multiplier)
8
7
  }
9
8
 
9
+ export const outputNumberFormatter = {
10
+ number: n => n,
11
+ numberCleanString: n => n,
12
+ precisionNumberCleanString: (strNum, multiplier) => revertPrecisionAsNumber(strNum, multiplier)
13
+ }
10
14
 
11
- export const classifyNum = (num, throwError = false) => {
12
- // 1) true numbers
13
- if (typeof num === 'number' && !Number.isNaN(num)) {
14
- return 'number';
15
- }
15
+ export const classifyNum = (num, throwError = false, precision = false) => {
16
16
 
17
- // 2) strings
18
- if (typeof num === 'string') {
19
- const str = num.trim();
20
- const cleanRe = /^-?\d+(\.\d+)?$/;
17
+ const cleanRe = /^-?\d+(\.\d+)?$/;
18
+ const isNumber = typeof num === 'number' && !Number.isNaN(num)
19
+ const isString = typeof num === 'string'
20
+ const isCleanString = isString && cleanRe.test(num)
21
21
 
22
- // a) clean numeric string: only digits and an optional single period
23
- if (cleanRe.test(str)) {
24
- return 'numberCleanString';
25
- }
22
+ if(precision === true) {
23
+ if(isCleanString) return 'precisionNumberCleanString'
26
24
 
27
- // b) dirty numeric string: contains non-digits (dollar, commas, letters, etc.)
28
- // but once you strip those out, you're left with a valid number
29
- const stripped = str.replace(/[^0-9.]/g, '');
30
- if (stripped && cleanRe.test(stripped)) {
31
- return 'numberDirtyString';
32
- }
25
+ if(throwError) throw new Error('Invalid number forma')
26
+ return typeof num
33
27
  }
34
-
35
-
36
- if(throwError)
37
- {
38
- throw new TypeError(
39
- 'Invalid input: expected a number or numeric string'
40
- );
28
+ else {
29
+ if(isNumber) return 'number'
30
+ else if(isCleanString) return 'numberCleanString'
41
31
  }
42
32
 
33
+ if(throwError) throw new TypeError('Invalid input: expected a number or numeric string')
43
34
  return typeof num
44
- };
45
-
46
-
47
- export const roundDecimalPlaces = (value, decimals) => {
48
-
49
- const factor = 10 ** decimals
50
- // add a tiny epsilon to mitigate floating-point quirks around .5
51
- return Math.round((value + Number.EPSILON) * factor) / factor
52
- };
35
+ }