ohlcv-indicators 3.5.79 → 3.5.80

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.
@@ -2,17 +2,18 @@
2
2
  import {FasterEMA, FasterSMA} from '../core-indicators/index.js';
3
3
  import { validateInputValues } from '../utilities/validators.js';
4
4
  import { initializeColumns } from '../core-functions/initializeColumns.js';
5
+ import { mathLog } from '../utilities/math.js';
5
6
 
6
7
  const indicatorClasses = {
7
8
  ema: FasterEMA,
8
9
  sma: FasterSMA
9
10
  }
10
11
 
11
- export const movingAverages = (main, index, methodName, size, { target, lag }) => {
12
+ export const movingAverages = (main, index, methodName, size, { target, lag, retLogs = false }) => {
12
13
 
13
14
  const { verticalOhlcv, instances, priceBased } = main
14
15
  const suffix = (target !== 'close') ? `_${target}` : ''
15
- const keyName = `${methodName}_${size}${suffix}`
16
+ const keyName = `${retLogs ? 'ret_log_' : ''}${methodName}_${size}${suffix}`
16
17
 
17
18
  if (index === 0) {
18
19
  validateInputValues({ [target]: true }, verticalOhlcv, index, methodName)
@@ -26,7 +27,7 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
26
27
  // Create the main moving average instance.
27
28
  instances[keyName] = new indicatorClasses[methodName](size)
28
29
 
29
- initializeColumns(main, [{ key: keyName, priceBased: priceBased.has(target) }], { lag })
30
+ initializeColumns(main, [{ key: keyName, priceBased: !retLogs && priceBased.has(target) }], { lag })
30
31
  }
31
32
 
32
33
  // Retrieve the current price value
@@ -38,6 +39,15 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
38
39
 
39
40
  const currMa = instance.isStable ? instance.getResult() : NaN
40
41
 
41
- // Always push the MA value (even if NaN).
42
- main.pushToMain({ index, key: keyName, value: currMa })
42
+ let output = currMa
43
+ if (retLogs) {
44
+ const ratio = value / currMa
45
+ // Log-relative distance is defined only for finite, positive ratios and operands.
46
+ output = value > 0 && currMa > 0 && ratio > 0 && Number.isFinite(ratio)
47
+ ? mathLog(ratio, 1)
48
+ : NaN
49
+ }
50
+
51
+ // Always push the selected value (even if NaN).
52
+ main.pushToMain({ index, key: keyName, value: output })
43
53
  }
@@ -1,12 +1,12 @@
1
1
  import { FasterSMA } from '../core-indicators/index.js';
2
- import { isPositiveInteger } from '../utilities/numberUtilities.js';
2
+ import { mathLog } from '../utilities/math.js';
3
3
  import { validateInputValues } from '../utilities/validators.js';
4
4
  import { initializeColumns } from '../core-functions/initializeColumns.js';
5
5
 
6
6
 
7
- export const relativeVolume = (main, index, size, {lag}) => {
7
+ export const relativeVolume = (main, index, size, {lag, retLogs = false}) => {
8
8
 
9
- const key = `relative_volume_${size}`;
9
+ const key = `${retLogs ? 'ret_log_' : ''}relative_volume_${size}`;
10
10
  const { instances, verticalOhlcv } = main;
11
11
 
12
12
 
@@ -42,9 +42,13 @@ export const relativeVolume = (main, index, size, {lag}) => {
42
42
  return
43
43
  }
44
44
 
45
- let currRelativeVolume = volume / prevSma;
45
+ const ratio = volume / prevSma;
46
+ const currRelativeVolume = retLogs
47
+ ? volume > 0 && prevSma > 0 && ratio > 0 && Number.isFinite(ratio)
48
+ ? mathLog(ratio, 1)
49
+ : NaN
50
+ : ratio;
46
51
 
47
52
  main.pushToMain({ index, key, value: currRelativeVolume });
48
- instances[key].prevRelativeVolumeSma = smaValue;
49
53
 
50
54
  };
@@ -0,0 +1,56 @@
1
+ import { mathLog } from '../utilities/math.js';
2
+ import { validateInputValues } from '../utilities/validators.js';
3
+ import { initializeColumns } from '../core-functions/initializeColumns.js';
4
+
5
+ export const mfi = (main, index, size, { lag, retLogs }) => {
6
+ const { verticalOhlcv, instances } = main;
7
+ const key = `${retLogs ? 'ret_log_' : ''}mfi_${size}`;
8
+
9
+ if (index === 0) {
10
+ validateInputValues({ high: true, low: true, close: true, volume: true }, verticalOhlcv, index, 'mfi');
11
+ // Raw and logged variants own separate state, even at the same period.
12
+ initializeColumns(main, [{ key }], { lag });
13
+ instances[key] = {
14
+ flows: new Float64Array(size),
15
+ next: 0,
16
+ count: 0,
17
+ previousTypical: NaN
18
+ };
19
+ }
20
+
21
+ const state = instances[key];
22
+ const typical = (verticalOhlcv.high[index] + verticalOhlcv.low[index] + verticalOhlcv.close[index]) / 3;
23
+ const change = typical - state.previousTypical;
24
+ state.previousTypical = typical;
25
+
26
+ // The first candle seeds the comparison; a size-period MFI needs size + 1 rows.
27
+ if (index === 0) return;
28
+
29
+ const flow = typical * verticalOhlcv.volume[index];
30
+ // A tie is neutral, but an invalid comparison must not silently become zero.
31
+ // Zero-volume candles contribute zero and still advance the candle window.
32
+ state.flows[state.next] = Number.isFinite(change) && Number.isFinite(flow) && flow >= 0
33
+ ? Math.sign(change) * flow : NaN;
34
+ state.next = (state.next + 1) % size;
35
+ if (state.count < size) state.count++;
36
+ if (state.count < size) return;
37
+
38
+ // Sum oldest to newest: bounded storage, no accumulated subtraction drift,
39
+ // and invalid values recover naturally after leaving the window.
40
+ let positive = 0;
41
+ let negative = 0;
42
+ let slot = state.next;
43
+ for (let step = 0; step < size; step++) {
44
+ const value = state.flows[slot];
45
+ if (Number.isNaN(value)) return;
46
+ if (value > 0) positive += value;
47
+ else if (value < 0) negative -= value;
48
+ if (++slot === size) slot = 0;
49
+ }
50
+
51
+ if (!Number.isFinite(positive) || !Number.isFinite(negative)) return;
52
+ // No directional money flow gives an undefined ratio, not a neutral signal.
53
+ const value = positive === 0 && negative === 0
54
+ ? NaN : 100 - 100 / (1 + positive / negative);
55
+ main.pushToMain({ index, key, value: Number.isNaN(value) ? NaN : retLogs ? mathLog(value, 50) : value });
56
+ };
@@ -7,7 +7,7 @@ import { initializeColumns } from '../core-functions/initializeColumns.js';
7
7
  export const volumeOscillator = (main, index, fast, slow, {lag, retLogs}) => {
8
8
 
9
9
  const {verticalOhlcv, instances} = main
10
- const key = `volume_oscillator_${fast}_${slow}`
10
+ const key = retLogs ? `ret_log_volume_oscillator_${fast}_${slow}` : `volume_oscillator_${fast}_${slow}`
11
11
 
12
12
  if (index === 0) {
13
13
  validateInputValues({ volume: true }, verticalOhlcv, index, 'volumeOscillator');
@@ -19,10 +19,7 @@ export const volumeOscillator = (main, index, fast, slow, {lag, retLogs}) => {
19
19
  }
20
20
  })
21
21
 
22
- initializeColumns(main, [
23
- { key },
24
- { key: `ret_log_${key}`, enabled: retLogs }
25
- ], { lag });
22
+ initializeColumns(main, [{ key }], { lag });
26
23
  }
27
24
 
28
25
  const { fastEMA, slowEMA } = instances[key];
@@ -46,12 +43,9 @@ export const volumeOscillator = (main, index, fast, slow, {lag, retLogs}) => {
46
43
  return;
47
44
  }
48
45
 
49
- volumeOscValue = 100 * (fastValue - slowValue) / slowValue
50
-
51
- if(retLogs) {
52
- volumeOscRetLog = mathLog(fastValue, slowValue)
53
- main.pushToMain({index, key: `ret_log_${key}`, value: volumeOscRetLog})
54
- }
46
+ volumeOscValue = retLogs
47
+ ? mathLog(fastValue, slowValue)
48
+ : 100 * (fastValue - slowValue) / slowValue
55
49
 
56
50
  main.pushToMain({index, key, value: volumeOscValue})
57
51
  };
@@ -56,14 +56,34 @@ export const candleFeatures = (main, index, {lag, colKeys, retLogs}) => {
56
56
  const upperWickTop = Math.max(currOpen, currClose)
57
57
  const lowerWickTop = Math.min(currOpen, currClose)
58
58
 
59
+ const change = getRet(currClose, prevClose)
60
+ const midPriceChange = getRet(currMidPrice, prevMidPrice)
61
+ const upperWick = getRet(currHigh, upperWickTop)
62
+ const lowerWick = getRet(lowerWickTop, currLow)
63
+ const gap = getRet(currOpen, prevClose)
64
+ const body = getRet(currClose, currOpen)
65
+ const range = getRet(currHigh, currLow)
66
+
67
+ if (colKeys.length === 0) {
68
+ main.pushToMain({ index, key: `${prefix}change`, value: change })
69
+ main.pushToMain({ index, key: `${prefix}mid_price_change`, value: midPriceChange })
70
+ main.pushToMain({ index, key: `${prefix}upper_wick`, value: upperWick })
71
+ main.pushToMain({ index, key: `${prefix}lower_wick`, value: lowerWick })
72
+ main.pushToMain({ index, key: `${prefix}gap`, value: gap })
73
+ main.pushToMain({ index, key: `${prefix}body`, value: body })
74
+ main.pushToMain({ index, key: `${prefix}range`, value: range })
75
+ return
76
+ }
77
+
78
+ // Keep optional-target overwrite/order semantics and all-or-nothing validation.
59
79
  const row = {
60
- [`${prefix}change`]: getRet(currClose, prevClose),
61
- [`${prefix}mid_price_change`]: getRet(currMidPrice, prevMidPrice),
62
- [`${prefix}upper_wick`]: getRet(currHigh, upperWickTop),
63
- [`${prefix}lower_wick`]: getRet(lowerWickTop, currLow),
64
- [`${prefix}gap`]: getRet(currOpen, prevClose),
65
- [`${prefix}body`]: getRet(currClose, currOpen),
66
- [`${prefix}range`]: getRet(currHigh, currLow)
80
+ [`${prefix}change`]: change,
81
+ [`${prefix}mid_price_change`]: midPriceChange,
82
+ [`${prefix}upper_wick`]: upperWick,
83
+ [`${prefix}lower_wick`]: lowerWick,
84
+ [`${prefix}gap`]: gap,
85
+ [`${prefix}body`]: body,
86
+ [`${prefix}range`]: range
67
87
  }
68
88
 
69
89
  let hasInvalidTarget = false
@@ -86,4 +106,4 @@ export const candleFeatures = (main, index, {lag, colKeys, retLogs}) => {
86
106
  main.pushToMain({index, key, value})
87
107
  }
88
108
 
89
- }
109
+ }
@@ -1,6 +1,8 @@
1
1
  import {oneHotEncode} from '../machine-learning/ml-utilities.js'
2
2
  import { initializeColumns } from '../core-functions/initializeColumns.js'
3
3
 
4
+ const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
5
+
4
6
  export const dateTime = (main, index, {lag, oneHot, precompute}) => {
5
7
 
6
8
 
@@ -40,43 +42,44 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
40
42
 
41
43
  const currDate = verticalOhlcv.date[index]
42
44
 
43
- const dateInfo = getDateInfo(currDate, oneHot, colKeySizes, formatter)
44
-
45
- for(const [key, value] of Object.entries(dateInfo))
46
- {
47
- main.pushToMain({index, key, value})
45
+ let yearPart, monthPart, hourPart, minutePart, weekdayPart, dayPart
46
+ for (const part of formatter.formatToParts(currDate)) {
47
+ switch (part.type) {
48
+ case 'year': yearPart = part.value; break
49
+ case 'month': monthPart = part.value; break
50
+ case 'hour': hourPart = part.value; break
51
+ case 'minute': minutePart = part.value; break
52
+ case 'weekday': weekdayPart = part.value; break
53
+ case 'day': dayPart = part.value; break
54
+ }
55
+ }
56
+ const year = Number(yearPart)
57
+ const month = Number(monthPart) - 1
58
+ const hour = Number(hourPart)
59
+ const minute = Number(minutePart)
60
+ const dayOfWeek = weekdays.indexOf(weekdayPart)
61
+ const dayOfMonth = Number(dayPart) - 1
62
+
63
+ if (!oneHot) {
64
+ main.pushToMain({ index, key: 'year', value: year })
65
+ main.pushToMain({ index, key: 'month', value: month + 1 })
66
+ main.pushToMain({ index, key: 'hour', value: hour })
67
+ main.pushToMain({ index, key: 'minute', value: minute })
68
+ main.pushToMain({ index, key: 'day_of_the_week', value: ((dayOfWeek + 6) % 7) + 1 })
69
+ main.pushToMain({ index, key: 'day_of_the_month', value: dayOfMonth + 1 })
70
+ return
48
71
  }
49
- }
50
-
51
-
52
-
53
- const getDateInfo = (date, oneHot, colKeySizes, formatter) => {
54
-
55
-
56
- const parts = Object.fromEntries(formatter.formatToParts(date).map(({ type, value }) => [type, value]));
57
- const year = Number(parts.year);
58
- const month = Number(parts.month) - 1;
59
- const hour = Number(parts.hour);
60
- const minute = Number(parts.minute);
61
- const dayOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(parts.weekday);
62
- const dayOfMonth = Number(parts.day) - 1;
63
-
64
- if (!oneHot) {
65
- return {
66
- year: year,
67
- month: month + 1, //iso month
68
- hour: hour,
69
- minute: minute,
70
- day_of_the_week: ((dayOfWeek + 6) % 7) + 1, //iso date of the week
71
- day_of_the_month: dayOfMonth + 1, //iso date of the month
72
- };
73
- }
74
72
 
75
- return {
76
- one_hot_month: oneHotEncode(month, colKeySizes.one_hot_month),
77
- one_hot_hour: oneHotEncode(hour, colKeySizes.one_hot_hour),
78
- one_hot_minute: oneHotEncode(minute, colKeySizes.one_hot_minute),
79
- one_hot_day_of_the_week: oneHotEncode(dayOfWeek, colKeySizes.one_hot_day_of_the_week),
80
- one_hot_day_of_the_month: oneHotEncode(dayOfMonth, colKeySizes.one_hot_day_of_the_month),
81
- };
82
- };
73
+ // Build a fresh vector for every field and row before writing.
74
+ const oneHotMonth = oneHotEncode(month, colKeySizes.one_hot_month)
75
+ const oneHotHour = oneHotEncode(hour, colKeySizes.one_hot_hour)
76
+ const oneHotMinute = oneHotEncode(minute, colKeySizes.one_hot_minute)
77
+ const oneHotWeekday = oneHotEncode(dayOfWeek, colKeySizes.one_hot_day_of_the_week)
78
+ const oneHotDay = oneHotEncode(dayOfMonth, colKeySizes.one_hot_day_of_the_month)
79
+
80
+ main.pushToMain({ index, key: 'one_hot_month', value: oneHotMonth })
81
+ main.pushToMain({ index, key: 'one_hot_hour', value: oneHotHour })
82
+ main.pushToMain({ index, key: 'one_hot_minute', value: oneHotMinute })
83
+ main.pushToMain({ index, key: 'one_hot_day_of_the_week', value: oneHotWeekday })
84
+ main.pushToMain({ index, key: 'one_hot_day_of_the_month', value: oneHotDay })
85
+ }
@@ -24,11 +24,7 @@ class CrossInstance {
24
24
  prevSlow: NaN,
25
25
  prevHigh: NaN,
26
26
  prevLow: NaN,
27
- areHighAndLowUndefined: false,
28
- crossIndexes: {
29
- up: [],
30
- down: []
31
- }
27
+ areHighAndLowUndefined: false
32
28
  })
33
29
 
34
30
  }
@@ -115,9 +111,6 @@ class CrossInstance {
115
111
  }
116
112
 
117
113
 
118
- if(this.interval === 1) this.crossIndexes.up.push(index)
119
- if(this.interval === -1) this.crossIndexes.down.push(index)
120
-
121
114
  //save prev state
122
115
  Object.assign(this, {
123
116
  prevState: currState,
@@ -238,4 +231,4 @@ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
238
231
 
239
232
  }
240
233
 
241
- }
234
+ }
@@ -10,27 +10,27 @@ export const lag = (main, index, colKeys, lookback) => {
10
10
  {
11
11
  const lagColumns = new Map()
12
12
 
13
- for (const targetKey of colKeys) {
14
- if (!verticalOhlcv.hasOwnProperty(targetKey)) {
13
+ for (const target of colKeys) {
14
+ if (!verticalOhlcv.hasOwnProperty(target)) {
15
15
  throw new Error(
16
- `Lag processing invoked by col "${targetKey}" was not found in "verticalOhlcv".`
16
+ `Lag processing invoked by col "${target}" was not found in "verticalOhlcv".`
17
17
  )
18
18
  }
19
19
 
20
- const addToPriceBased = priceBased.has(targetKey)
21
- const type = Array.isArray(verticalOhlcv[targetKey]) ? 'Array' : 'Float64Array'
20
+ const addToPriceBased = priceBased.has(target)
21
+ const type = Array.isArray(verticalOhlcv[target]) ? 'Array' : 'Float64Array'
22
22
  const columns = []
23
23
  const keyNames = []
24
24
 
25
25
  for (let step = 1; step <= lookback; step++) {
26
- const key = `${targetKey}_lag_${step}`
26
+ const key = `${target}_lag_${step}`
27
27
 
28
28
  columns.push({ key, type, priceBased: addToPriceBased, includeInLag: false })
29
29
  keyNames.push(key)
30
30
  }
31
31
 
32
32
  initializeColumns(main, columns)
33
- lagColumns.set(targetKey, keyNames)
33
+ lagColumns.set(target, keyNames)
34
34
  }
35
35
 
36
36
  // colKeys belongs to the runtime queue; keep cached names out of exported params.
@@ -40,15 +40,14 @@ export const lag = (main, index, colKeys, lookback) => {
40
40
 
41
41
  const lagColumns = instances[lagColumnsKey]?.get(colKeys)
42
42
 
43
- for (const targetKey of colKeys) {
44
- const keyNames = lagColumns?.get(targetKey)
45
- const currentColumn = verticalOhlcv[targetKey]
43
+ for (const target of colKeys) {
44
+ const keyNames = lagColumns?.get(target)
45
+ const currentColumn = verticalOhlcv[target]
46
46
  const missingValue = Array.isArray(currentColumn) ? null : NaN
47
47
 
48
48
  // Populate the lagged values each tick:
49
49
  for (let step = 1; step <= lookback; step++) {
50
- // Runtime callbacks may change the target list or lookback after initialization.
51
- const key = keyNames?.[step - 1] ?? `${targetKey}_lag_${step}`
50
+ const key = keyNames?.[step - 1] ?? `${target}_lag_${step}`
52
51
  const laggedIndex = index - step
53
52
  const value =
54
53
  laggedIndex < 0 || currentColumn[laggedIndex] == null
@@ -1,15 +1,11 @@
1
- import { defaultMapColsCallback } from '../studies/mapCols.js';
2
-
3
1
  export const CONFIG_VERSION = 1;
4
- const mapCallbacks = new Map([['default', defaultMapColsCallback]]);
5
2
 
6
3
  // Copy configuration without JSON's silent conversion of functions/NaN to null.
7
- export const copyConfig = (value, allowFunctions = false, ancestors = new Set()) => {
4
+ export const copyConfig = (value, ancestors = new Set()) => {
8
5
  if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
9
6
  if (typeof value === 'number' && Number.isFinite(value)) return value;
10
- if (typeof value === 'function' && allowFunctions) return value;
11
7
  if (typeof value !== 'object' || value === null) {
12
- throw new TypeError('Configuration values must be JSON-safe: no undefined, non-finite numbers, or unregistered functions.');
8
+ throw new TypeError('Configuration values must be JSON-safe: no undefined, non-finite numbers, or functions.');
13
9
  }
14
10
  if (ancestors.has(value)) throw new TypeError('Configuration must not contain circular references.');
15
11
  const prototype = Object.getPrototypeOf(value);
@@ -20,8 +16,8 @@ export const copyConfig = (value, allowFunctions = false, ancestors = new Set())
20
16
 
21
17
  ancestors.add(value);
22
18
  const result = Array.isArray(value)
23
- ? Array.from(value, item => copyConfig(item, allowFunctions, ancestors))
24
- : Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyConfig(item, allowFunctions, ancestors)]));
19
+ ? Array.from(value, item => copyConfig(item, ancestors))
20
+ : Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyConfig(item, ancestors)]));
25
21
  ancestors.delete(value);
26
22
  return result;
27
23
  };
@@ -33,32 +29,3 @@ export const freezeConfig = value => {
33
29
  }
34
30
  return value;
35
31
  };
36
-
37
- export const registerMapCallback = (name, callback) => {
38
- if (typeof name !== 'string' || !name || typeof callback !== 'function') {
39
- throw new TypeError('registerMapCallback requires a nonempty name and a function.');
40
- }
41
- if (mapCallbacks.has(name) && mapCallbacks.get(name) !== callback) {
42
- throw new Error(`mapCols callback "${name}" is already registered. Use a new versioned name.`);
43
- }
44
- mapCallbacks.set(name, callback);
45
- };
46
-
47
- export const resolveMapCallback = callback => {
48
- if (typeof callback === 'function') return callback;
49
- if (typeof callback === 'string' && mapCallbacks.has(callback)) return mapCallbacks.get(callback);
50
- throw new Error(`Unknown mapCols callback "${callback}". Register it before loading the configuration.`);
51
- };
52
-
53
- export const exportInputParams = inputParams => {
54
- const result = copyConfig(inputParams, true);
55
- for (const job of result) {
56
- if (job.key !== 'mapCols' || typeof job.params[1] !== 'function') continue;
57
- const entry = [...mapCallbacks].find(([, callback]) => callback === job.params[1]);
58
- if (!entry) {
59
- throw new Error('Cannot export an unregistered mapCols callback. Register it with OHLCV_INDICATORS.registerMapCallback(name, callback).');
60
- }
61
- job.params[1] = entry[0];
62
- }
63
- return copyConfig(result);
64
- };
@@ -5,13 +5,13 @@ export const isPositiveInteger = n => Number.isInteger(n) && n >= 0;
5
5
  export const inputNumberFormatter = {
6
6
  number: n => n,
7
7
  numberCleanString: n => Number(n),
8
- precisionNumberCleanString: (strNum, multiplier) => addPrecisionAsNumber(strNum, multiplier)
8
+ precisionNumberCleanString: (strNum, multiplier, cachedDecimals) => addPrecisionAsNumber(strNum, multiplier, cachedDecimals)
9
9
  }
10
10
 
11
11
  export const outputNumberFormatter = {
12
12
  number: n => n,
13
13
  numberCleanString: n => n,
14
- precisionNumberCleanString: (strNum, multiplier) => revertPrecisionAsNumber(strNum, multiplier)
14
+ precisionNumberCleanString: (strNum, multiplier, cachedDecimals) => revertPrecisionAsNumber(strNum, multiplier, cachedDecimals)
15
15
  }
16
16
 
17
17
  export const classifyNum = (num, throwError = false, precision = false) => {
@@ -34,4 +34,4 @@ export const classifyNum = (num, throwError = false, precision = false) => {
34
34
 
35
35
  if(throwError) throw new TypeError('Invalid input: expected a number or numeric string')
36
36
  return typeof num
37
- }
37
+ }
@@ -51,8 +51,14 @@ export const calcPrecisionMultiplier = main => {
51
51
  }
52
52
 
53
53
 
54
+ // Keep the existing decimal-width rule, including scientific-notation multipliers.
55
+ export const getPrecisionDecimals = multiplier => {
56
+ const mulStr = String(multiplier)
57
+ return multiplier === 1 ? 0 : mulStr.length - 1
58
+ }
59
+
54
60
  //this function converts the strNum into a integer * multiplier
55
- export const addPrecisionAsNumber = (strNum, multiplier) => {
61
+ export const addPrecisionAsNumber = (strNum, multiplier, cachedDecimals) => {
56
62
  if (typeof strNum !== 'string') throw new Error('addPrecisionExact expects a string when precision=true')
57
63
 
58
64
  const negative = strNum.charAt(0) === '-'
@@ -60,8 +66,7 @@ export const addPrecisionAsNumber = (strNum, multiplier) => {
60
66
  const [intPart, decPart = ''] = s.split('.')
61
67
 
62
68
  // Compute decimals from multiplier (multiplier expected as 10^decimals)
63
- const mulStr = String(multiplier);
64
- const decimals = (multiplier === 1) ? 0 : (mulStr.length - 1)
69
+ const decimals = cachedDecimals === undefined ? getPrecisionDecimals(multiplier) : cachedDecimals
65
70
 
66
71
  const decPadded = decPart.padEnd(decimals, '0').slice(0, decimals); // pad or trim
67
72
  const integerString = intPart + decPadded // e.g. "12" + "3400" => "123400"
@@ -75,7 +80,7 @@ export const addPrecisionAsNumber = (strNum, multiplier) => {
75
80
 
76
81
 
77
82
  //this function converts the outputs the number generated in addPrecisionAsNumber / multiplier
78
- export const revertPrecisionAsNumber = (num, multiplier) => {
83
+ export const revertPrecisionAsNumber = (num, multiplier, cachedDecimals) => {
79
84
  if (typeof num !== 'number' || !Number.isFinite(num)) {
80
85
  throw new Error('revertPrecisionAsNumber expects a finite number')
81
86
  }
@@ -86,7 +91,7 @@ export const revertPrecisionAsNumber = (num, multiplier) => {
86
91
  // Make sure we're working with an integer, mirroring addPrecisionAsNumber
87
92
  abs = Math.trunc(abs)
88
93
 
89
- const decimals = (multiplier === 1) ? 0 : (String(multiplier).length - 1)
94
+ const decimals = cachedDecimals === undefined ? getPrecisionDecimals(multiplier) : cachedDecimals
90
95
 
91
96
  if (decimals === 0) {
92
97
  // No fractional part; just restore sign
@@ -106,4 +111,4 @@ export const revertPrecisionAsNumber = (num, multiplier) => {
106
111
  : `${intPart}.${fracStr}`
107
112
 
108
113
  return Number(negative ? `-${resultStr}` : resultStr)
109
- }
114
+ }
@@ -1,5 +1,6 @@
1
1
  import { dateOutputFormaters } from "./dateUtilities.js"
2
2
  import { outputNumberFormatter } from "./numberUtilities.js"
3
+ import { getPrecisionDecimals } from "./precisionMultiplier.js"
3
4
 
4
5
  export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, dateFormat}) => {
5
6
 
@@ -8,6 +9,10 @@ export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, da
8
9
 
9
10
 
10
11
  if (verticalOhlcvKeyNames.length === 0) return []
12
+
13
+ const precisionDecimals = precision && typeof precisionMultiplier === 'number'
14
+ ? getPrecisionDecimals(precisionMultiplier)
15
+ : undefined
11
16
 
12
17
  const skipNullIndex = skipNull && invalidValueIndex >= 0 ? invalidValueIndex + 1 : 0
13
18
  const maxStartIndex = Math.max(skipNullIndex, startIndex)
@@ -33,7 +38,7 @@ export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, da
33
38
  const value = arr[i]
34
39
  result[i - maxStartIndex][key] = value == null || Number.isNaN(value)
35
40
  ? value
36
- : formatter(value, precisionMultiplier)
41
+ : formatter(value, precisionMultiplier, precisionDecimals)
37
42
  }
38
43
  }
39
44
  else
@@ -16,7 +16,7 @@ export const atr = (main, index, size, {lag, retLogs}) => {
16
16
 
17
17
  instances[baseKeyName] = new FasterATR(size, FasterWSMA)
18
18
 
19
- initializeColumns(main, [{ key: baseKeyName, priceBased: true }], { lag })
19
+ initializeColumns(main, [{ key: baseKeyName, priceBased: !retLogs }], { lag })
20
20
  }
21
21
 
22
22
  // Retrieve the current price value.