ohlcv-indicators 3.5.78 → 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.
Files changed (43) hide show
  1. package/AGENTS.md +113 -0
  2. package/README.md +456 -0
  3. package/dist/ohlcv-indicators.min.js +2 -1
  4. package/dist/ohlcv-indicators.min.js.LICENSE.txt +22 -0
  5. package/index.js +112 -84
  6. package/package.json +2 -5
  7. package/src/core-functions/initializeColumns.js +40 -0
  8. package/src/core-functions/mainLoop.js +35 -30
  9. package/src/core-functions/pushToMain.js +8 -27
  10. package/src/core-indicators/README.md +60 -0
  11. package/src/core-indicators/composites.js +136 -0
  12. package/src/core-indicators/index.js +5 -0
  13. package/src/core-indicators/indicator.js +81 -0
  14. package/src/core-indicators/movingAverages.js +88 -0
  15. package/src/core-indicators/rsi.js +41 -0
  16. package/src/core-indicators/volatility.js +107 -0
  17. package/src/moving-averages/bollingerBands.js +39 -31
  18. package/src/moving-averages/donchianChannel.js +62 -36
  19. package/src/moving-averages/heikenAshi.js +33 -52
  20. package/src/moving-averages/macd.js +10 -26
  21. package/src/moving-averages/movingAverages.js +19 -25
  22. package/src/moving-averages/relativeVolume.js +30 -24
  23. package/src/oscillators/mfi.js +56 -0
  24. package/src/oscillators/rsi.js +7 -26
  25. package/src/oscillators/stochastic.js +6 -19
  26. package/src/oscillators/volumeDelta.js +36 -28
  27. package/src/oscillators/volumeOscillator.js +24 -32
  28. package/src/studies/candleFeatures.js +40 -28
  29. package/src/studies/dateTime.js +54 -56
  30. package/src/studies/findCrosses.js +18 -16
  31. package/src/studies/lag.js +32 -20
  32. package/src/utilities/assignTypes.js +1 -1
  33. package/src/utilities/config.js +31 -0
  34. package/src/utilities/numberUtilities.js +5 -3
  35. package/src/utilities/precisionMultiplier.js +11 -6
  36. package/src/utilities/validators.js +40 -1
  37. package/src/utilities/verticalToHorizontal.js +26 -16
  38. package/src/volatility/adx.js +7 -20
  39. package/src/volatility/atr.js +10 -23
  40. package/test/test.js +1230 -0
  41. package/webpack.config.js +0 -11
  42. package/src/moving-averages/vidya.js +0 -181
  43. package/src/studies/mapCols.js +0 -46
@@ -1,39 +1,40 @@
1
+ import { validateInputValues } from '../utilities/validators.js'
2
+ import { initializeColumns } from '../core-functions/initializeColumns.js'
3
+ import { mathLog } from '../utilities/math.js'
4
+
1
5
  export const donchianChannels = (main, index, size, offset, options) => {
2
- const { verticalOhlcv, instances, len, inputParams, priceBased, useFullNames } = main
3
- const { lag: outputLag = 0 } = options
6
+ const { verticalOhlcv, instances, len, inputParams, useFullNames } = main
7
+ const { lag: outputLag = 0, retLogs = false } = options
4
8
  const indicatorKey = `${size}_${offset}`
9
+ const instanceKey = `${retLogs ? 'ret_log_' : ''}${indicatorKey}`
10
+ const prefix = `${retLogs ? 'ret_log_' : ''}donchian_channel`
5
11
 
6
12
  // ---- INIT (only at first bar) ----
7
13
  if (index === 0) {
14
+ validateInputValues({ high: true, low: true }, verticalOhlcv, index, 'donchianChannels')
15
+ if (retLogs) validateInputValues({ close: true }, verticalOhlcv, index, 'donchianChannels')
16
+
8
17
  const numberOfIndicators = inputParams.filter(o => o.key === 'donchianChannels').length
9
18
  const useIndexedKeys = numberOfIndicators > 1 || useFullNames
10
19
 
11
20
  const getKey = name =>
12
21
  useIndexedKeys
13
- ? `donchian_channel_${name}_${indicatorKey}`
14
- : `donchian_channel_${name}`
22
+ ? `${prefix}_${name}_${indicatorKey}`
23
+ : `${prefix}_${name}`
15
24
 
16
- const keys = ['upper', 'basis', 'lower'].map(getKey)
17
-
18
- for (const k of keys) {
19
- priceBased.add(k)
20
- }
25
+ const keys = (retLogs ? ['width', 'position'] : ['upper', 'basis', 'lower']).map(getKey)
21
26
 
22
27
  if (!instances.donchian_channel) {
23
28
  instances.donchian_channel = { numberOfIndicators, settings: {} }
24
29
  }
25
30
 
26
31
  instances.donchian_channel.numberOfIndicators = numberOfIndicators
27
- instances.donchian_channel.settings[indicatorKey] = { maxDeque: [], minDeque: [] }
28
-
29
- Object.assign(
30
- verticalOhlcv,
31
- Object.fromEntries(keys.map(k => [k, new Float64Array(len).fill(NaN)]))
32
- )
33
-
34
- if (outputLag > 0) {
35
- main.lag(keys, outputLag)
32
+ instances.donchian_channel.settings[instanceKey] = {
33
+ maxDeque: { indices: [], head: 0, length: 0 },
34
+ minDeque: { indices: [], head: 0, length: 0 }
36
35
  }
36
+
37
+ initializeColumns(main, keys.map(key => ({ key, priceBased: !retLogs })), { lag: outputLag })
37
38
  }
38
39
 
39
40
  // ---- PER-BAR COMPUTATION ----
@@ -42,41 +43,66 @@ export const donchianChannels = (main, index, size, offset, options) => {
42
43
 
43
44
  const getKey = name =>
44
45
  useIndexedKeys
45
- ? `donchian_channel_${name}_${indicatorKey}`
46
- : `donchian_channel_${name}`
46
+ ? `${prefix}_${name}_${indicatorKey}`
47
+ : `${prefix}_${name}`
47
48
 
48
- const { maxDeque, minDeque } = settings[indicatorKey]
49
+ const { maxDeque, minDeque } = settings[instanceKey]
49
50
  const current = index - offset
50
51
  const start = current - size + 1
51
52
 
52
- const outKeys = ['upper', 'basis', 'lower'].map(getKey)
53
-
54
- if (start < 0 || current + 1 > len) {
55
- outKeys.forEach(key =>
56
- main.pushToMain({ index, key, value: NaN })
57
- )
58
- return true
59
- }
53
+ // No source candle is available yet.
54
+ if (current < 0 || current >= len) return
60
55
 
61
56
  const { high: highs, low: lows } = verticalOhlcv
62
57
 
63
58
  const update = (dq, arr, cmp) => {
64
- while (dq.length && dq[0] < start) dq.shift()
65
- while (dq.length && cmp(arr[dq.at(-1)], arr[current])) dq.pop()
66
- dq.push(current)
59
+ const { indices } = dq
60
+ while (dq.length && indices[dq.head] < start) {
61
+ dq.head = (dq.head + 1) % size
62
+ dq.length--
63
+ }
64
+ while (dq.length && cmp(arr[indices[(dq.head + dq.length - 1) % size]], arr[current])) {
65
+ dq.length--
66
+ }
67
+ // Reuse at most size slots instead of moving entries or growing with the input.
68
+ indices[(dq.head + dq.length) % size] = current
69
+ dq.length++
67
70
  }
68
71
 
69
72
  update(maxDeque, highs, (a, b) => a <= b)
70
73
  update(minDeque, lows, (a, b) => a >= b)
71
74
 
75
+ // Collect warm-up candles, but leave outputs as NaN until the window is complete.
76
+ if (start < 0) return
77
+
72
78
  const hasBounds = maxDeque.length && minDeque.length
73
- const upper = hasBounds ? highs[maxDeque[0]] : NaN
74
- const lower = hasBounds ? lows[minDeque[0]] : NaN
79
+ const upper = hasBounds ? highs[maxDeque.indices[maxDeque.head]] : NaN
80
+ const lower = hasBounds ? lows[minDeque.indices[minDeque.head]] : NaN
81
+
82
+ if (retLogs) {
83
+ let width = NaN
84
+ let position = NaN
85
+ const rangeRatio = upper / lower
86
+ if (lower > 0 && upper >= lower && Number.isFinite(upper) && Number.isFinite(rangeRatio)) {
87
+ const logRange = mathLog(rangeRatio, 1)
88
+ width = logRange / 2
89
+ // Offset applies to the channel window, not to the current close.
90
+ const close = verticalOhlcv.close[index]
91
+ const locationRatio = close / lower
92
+ if (logRange > 0 && close > 0 && locationRatio > 0 && Number.isFinite(locationRatio)) {
93
+ const normalized = 2 * mathLog(locationRatio, 1) / logRange - 1
94
+ if (Number.isFinite(normalized)) position = normalized
95
+ }
96
+ }
97
+ main.pushToMain({ index, key: getKey('width'), value: width })
98
+ main.pushToMain({ index, key: getKey('position'), value: position })
99
+ return
100
+ }
101
+
75
102
  const basis = hasBounds ? (upper + lower) / 2 : NaN
76
103
 
77
104
  main.pushToMain({ index, key: getKey('upper'), value: upper })
78
105
  main.pushToMain({ index, key: getKey('basis'), value: basis })
79
106
  main.pushToMain({ index, key: getKey('lower'), value: lower })
80
107
 
81
- return true
82
- }
108
+ }
@@ -1,4 +1,6 @@
1
- import { FasterEMA } from 'trading-signals';
1
+ import { FasterEMA } from '../core-indicators/index.js';
2
+ import { validateInputValues } from '../utilities/validators.js';
3
+ import { initializeColumns } from '../core-functions/initializeColumns.js';
2
4
 
3
5
  const isBadNumber = v => v == null || !Number.isFinite(v);
4
6
 
@@ -9,9 +11,8 @@ export const heikenAshi = (
9
11
  afterSmoothLength,
10
12
  { lag = 0, bothNull = false, retLogs = true } = {}
11
13
  ) => {
12
- const { verticalOhlcv, instances, len, scaledGroups } = main;
14
+ const { verticalOhlcv, instances, scaledGroups } = main;
13
15
  const indicatorKey = `${smoothLength}_${afterSmoothLength}`;
14
- const ohlcKeys = ['open', 'high', 'low', 'close'];
15
16
 
16
17
  const getRet = (next, prev) => retLogs
17
18
  ? Math.log(next / prev)
@@ -23,13 +24,6 @@ export const heikenAshi = (
23
24
 
24
25
  const getKey = key => `${prefix}heiken_ashi_${key}${paramsKey}`;
25
26
 
26
- const featureKeys = [
27
- 'body',
28
- 'upper_wick',
29
- 'lower_wick',
30
- 'range'
31
- ];
32
-
33
27
  const crossKey = bothNull
34
28
  ? 'heiken_ashi_cross'
35
29
  : `heiken_ashi_cross_${indicatorKey}`;
@@ -40,6 +34,10 @@ export const heikenAshi = (
40
34
 
41
35
  // ---- INIT ----
42
36
  if (index === 0) {
37
+ validateInputValues({ open: true, high: true, low: true, close: true }, verticalOhlcv, index, 'heikenAshi');
38
+ const ohlcKeys = ['open', 'high', 'low', 'close'];
39
+ const featureKeys = ['body', 'upper_wick', 'lower_wick', 'range'];
40
+
43
41
  instances[instanceKey] = {
44
42
  emaPre: !bothNull
45
43
  ? Object.fromEntries(ohlcKeys.map(k => [k, new FasterEMA(smoothLength)]))
@@ -56,19 +54,11 @@ export const heikenAshi = (
56
54
 
57
55
  const keyNames = featureKeys.map(getKey);
58
56
 
59
- const verticalOhlcvSetup = Object.fromEntries(
60
- [...keyNames, crossKey].map(k => [k, new Float64Array(len).fill(NaN)])
61
- );
62
-
63
- Object.assign(verticalOhlcv, verticalOhlcvSetup);
57
+ initializeColumns(main, [...keyNames, crossKey].map(key => ({ key })), { lag });
64
58
 
65
59
  if (scaledGroups) {
66
60
  scaledGroups.heikenAshi = keyNames;
67
61
  }
68
-
69
- if (lag > 0) {
70
- main.lag(keyNames, lag);
71
- }
72
62
  }
73
63
 
74
64
  // ---- FETCH RAW INPUTS ----
@@ -84,7 +74,7 @@ export const heikenAshi = (
84
74
  isBadNumber(low) ||
85
75
  isBadNumber(close)
86
76
  ) {
87
- return true;
77
+ return;
88
78
  }
89
79
 
90
80
  let sOpen, sHigh, sLow, sClose;
@@ -96,14 +86,12 @@ export const heikenAshi = (
96
86
  inst.emaPre.low.update(low);
97
87
  inst.emaPre.close.update(close);
98
88
 
99
- try {
100
- sOpen = inst.emaPre.open.getResult();
101
- sHigh = inst.emaPre.high.getResult();
102
- sLow = inst.emaPre.low.getResult();
103
- sClose = inst.emaPre.close.getResult();
104
- } catch {
105
- return true;
106
- }
89
+ if (!inst.emaPre.open.isStable || !inst.emaPre.high.isStable ||
90
+ !inst.emaPre.low.isStable || !inst.emaPre.close.isStable) return;
91
+ sOpen = inst.emaPre.open.getResult();
92
+ sHigh = inst.emaPre.high.getResult();
93
+ sLow = inst.emaPre.low.getResult();
94
+ sClose = inst.emaPre.close.getResult();
107
95
  } else {
108
96
  sOpen = open;
109
97
  sHigh = high;
@@ -117,7 +105,7 @@ export const heikenAshi = (
117
105
  isBadNumber(sLow) ||
118
106
  isBadNumber(sClose)
119
107
  ) {
120
- return true;
108
+ return;
121
109
  }
122
110
 
123
111
  // ---- HEIKEN ASHI CORE ----
@@ -145,14 +133,12 @@ export const heikenAshi = (
145
133
  inst.emaPost.low.update(haLow);
146
134
  inst.emaPost.close.update(haClose);
147
135
 
148
- try {
149
- smOpen = inst.emaPost.open.getResult();
150
- smHigh = inst.emaPost.high.getResult();
151
- smLow = inst.emaPost.low.getResult();
152
- smClose = inst.emaPost.close.getResult();
153
- } catch {
154
- return true;
155
- }
136
+ if (!inst.emaPost.open.isStable || !inst.emaPost.high.isStable ||
137
+ !inst.emaPost.low.isStable || !inst.emaPost.close.isStable) return;
138
+ smOpen = inst.emaPost.open.getResult();
139
+ smHigh = inst.emaPost.high.getResult();
140
+ smLow = inst.emaPost.low.getResult();
141
+ smClose = inst.emaPost.close.getResult();
156
142
  } else {
157
143
  smOpen = haOpen;
158
144
  smHigh = haHigh;
@@ -170,7 +156,7 @@ export const heikenAshi = (
170
156
  smLow <= 0 ||
171
157
  smClose <= 0
172
158
  ) {
173
- return true;
159
+ return;
174
160
  }
175
161
 
176
162
  // ---- TREND/CROSS LOGIC ----
@@ -214,21 +200,16 @@ export const heikenAshi = (
214
200
  const bodyTop = Math.max(smOpen, smClose);
215
201
  const bodyBottom = Math.min(smOpen, smClose);
216
202
 
217
- const row = {
218
- [getKey('body')]: getRet(smClose, smOpen),
219
- [getKey('upper_wick')]: getRet(smHigh, bodyTop),
220
- [getKey('lower_wick')]: getRet(bodyBottom, smLow),
221
- [getKey('range')]: getRet(smHigh, smLow),
222
- };
203
+ const body = getRet(smClose, smOpen);
204
+ const upperWick = getRet(smHigh, bodyTop);
205
+ const lowerWick = getRet(bodyBottom, smLow);
206
+ const range = getRet(smHigh, smLow);
223
207
 
224
- for (const [key, value] of Object.entries(row)) {
225
- if (!isBadNumber(value)) {
226
- main.pushToMain({ index, key, value });
227
- }
228
- }
208
+ if (!isBadNumber(body)) main.pushToMain({ index, key: getKey('body'), value: body });
209
+ if (!isBadNumber(upperWick)) main.pushToMain({ index, key: getKey('upper_wick'), value: upperWick });
210
+ if (!isBadNumber(lowerWick)) main.pushToMain({ index, key: getKey('lower_wick'), value: lowerWick });
211
+ if (!isBadNumber(range)) main.pushToMain({ index, key: getKey('range'), value: range });
229
212
 
230
213
  // Do not add return logs to cross.
231
214
  main.pushToMain({ index, key: crossKey, value: cross });
232
-
233
- return true;
234
- };
215
+ };
@@ -1,14 +1,18 @@
1
- import { FasterEMA, FasterMACD } from 'trading-signals'
1
+ import { FasterEMA, FasterMACD } from '../core-indicators/index.js'
2
+ import { validateInputValues } from '../utilities/validators.js'
3
+ import { initializeColumns } from '../core-functions/initializeColumns.js'
2
4
 
3
5
  const defaultTarget = 'close'
4
6
 
5
7
  export const macd = (main, index, fast, slow, signal, { target, lag, precomputed }) => {
6
- const { verticalOhlcv, instances, priceBased, useFullNames } = main
8
+ const { verticalOhlcv, instances, useFullNames } = main
7
9
  const { instanceKey } = precomputed
8
10
 
9
11
  // Initialization on the first index.
10
12
  if (index === 0) {
11
- const { inputParams, len } = main
13
+ validateInputValues({ [target]: true }, verticalOhlcv, index, 'macd')
14
+
15
+ const { inputParams } = main
12
16
 
13
17
  if (!verticalOhlcv.hasOwnProperty(target)) {
14
18
  throw new Error(`Target property ${target} not found in verticalOhlcv for macd.`)
@@ -53,21 +57,8 @@ export const macd = (main, index, fast, slow, signal, { target, lag, precomputed
53
57
  new FasterEMA(signal)
54
58
  )
55
59
 
56
- Object.assign(verticalOhlcv, {
57
- [diffKey]: new Float64Array(len).fill(NaN),
58
- [deaKey]: new Float64Array(len).fill(NaN),
59
- [histogramKey]: new Float64Array(len).fill(NaN),
60
- })
61
-
62
60
  const keyNames = [diffKey, deaKey, histogramKey]
63
-
64
- for (const k of keyNames) {
65
- priceBased.add(k)
66
- }
67
-
68
- if (lag > 0) {
69
- main.lag(keyNames, lag)
70
- }
61
+ initializeColumns(main, keyNames.map(key => ({ key, priceBased: true })), { lag })
71
62
  }
72
63
 
73
64
  const { numberOfIndicators, settings } = instances.macd
@@ -94,17 +85,10 @@ export const macd = (main, index, fast, slow, signal, { target, lag, precomputed
94
85
  const value = verticalOhlcv[target][index]
95
86
  macdInstance.update(value)
96
87
 
97
- let macdResult = {}
98
- try {
99
- macdResult = macdInstance.getResult()
100
- } catch (err) {
101
- // If the result is unavailable, macdResult remains NaN.
102
- }
88
+ const macdResult = macdInstance.isStable ? macdInstance.getResult() : null
103
89
 
104
90
  // Always push values; use NaN as fallback when macdResult is missing.
105
91
  main.pushToMain({ index, key: diffKey, value: macdResult ? macdResult.macd : NaN })
106
92
  main.pushToMain({ index, key: deaKey, value: macdResult ? macdResult.signal : NaN })
107
93
  main.pushToMain({ index, key: histogramKey, value: macdResult ? macdResult.histogram : NaN })
108
-
109
- return true
110
- }
94
+ }
@@ -1,20 +1,22 @@
1
1
 
2
- import {FasterEMA, FasterSMA} from 'trading-signals';
2
+ import {FasterEMA, FasterSMA} from '../core-indicators/index.js';
3
+ import { validateInputValues } from '../utilities/validators.js';
4
+ import { initializeColumns } from '../core-functions/initializeColumns.js';
5
+ import { mathLog } from '../utilities/math.js';
3
6
 
4
7
  const indicatorClasses = {
5
8
  ema: FasterEMA,
6
9
  sma: FasterSMA
7
10
  }
8
11
 
9
- export const movingAverages = (main, index, methodName, size, { target, lag }) => {
12
+ export const movingAverages = (main, index, methodName, size, { target, lag, retLogs = false }) => {
10
13
 
11
14
  const { verticalOhlcv, instances, priceBased } = main
12
15
  const suffix = (target !== 'close') ? `_${target}` : ''
13
- const keyName = `${methodName}_${size}${suffix}`
16
+ const keyName = `${retLogs ? 'ret_log_' : ''}${methodName}_${size}${suffix}`
14
17
 
15
18
  if (index === 0) {
16
-
17
- const {len} = main
19
+ validateInputValues({ [target]: true }, verticalOhlcv, index, methodName)
18
20
 
19
21
  if (!verticalOhlcv.hasOwnProperty(target)) {
20
22
  throw new Error(
@@ -25,16 +27,7 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
25
27
  // Create the main moving average instance.
26
28
  instances[keyName] = new indicatorClasses[methodName](size)
27
29
 
28
- verticalOhlcv[keyName] = new Float64Array(len).fill(NaN)
29
-
30
- if(priceBased.has(target)){
31
- priceBased.add(keyName)
32
- }
33
-
34
- if(lag > 0)
35
- {
36
- main.lag([keyName], lag)
37
- }
30
+ initializeColumns(main, [{ key: keyName, priceBased: !retLogs && priceBased.has(target) }], { lag })
38
31
  }
39
32
 
40
33
  // Retrieve the current price value
@@ -44,16 +37,17 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
44
37
  // Update the moving average instance.
45
38
  instance.update(value)
46
39
 
47
- let currMa = NaN
48
-
49
- try {
50
- currMa = instance.getResult()
51
- } catch (err) {
40
+ const currMa = instance.isStable ? instance.getResult() : NaN
52
41
 
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
53
49
  }
54
50
 
55
- // Always push the MA value (even if NaN).
56
- main.pushToMain({ index, key: keyName, value: currMa })
57
-
58
- return true
59
- }
51
+ // Always push the selected value (even if NaN).
52
+ main.pushToMain({ index, key: keyName, value: output })
53
+ }
@@ -1,48 +1,54 @@
1
- import { FasterSMA } from 'trading-signals';
1
+ import { FasterSMA } from '../core-indicators/index.js';
2
+ import { mathLog } from '../utilities/math.js';
3
+ import { validateInputValues } from '../utilities/validators.js';
4
+ import { initializeColumns } from '../core-functions/initializeColumns.js';
2
5
 
3
- export const relativeVolume = (main, index, size, {lag}) => {
4
6
 
5
- const key = `relative_volume_${size}`;
7
+ export const relativeVolume = (main, index, size, {lag, retLogs = false}) => {
8
+
9
+ const key = `${retLogs ? 'ret_log_' : ''}relative_volume_${size}`;
6
10
  const { instances, verticalOhlcv } = main;
11
+
7
12
 
8
13
  if (index === 0) {
9
- const { len } = main;
14
+ validateInputValues({ volume: true }, verticalOhlcv, index, 'relativeVolume');
10
15
 
11
16
  instances[key] = {
12
17
  instance: new FasterSMA(size),
13
18
  prevRelativeVolumeSma: NaN
14
19
  };
15
20
 
16
- verticalOhlcv[key] = new Float64Array(len).fill(NaN);
17
-
18
- if(lag > 0)
19
- {
20
- main.lag([key], lag)
21
- }
21
+ initializeColumns(main, [{ key }], { lag });
22
22
 
23
23
  }
24
24
 
25
- const value = verticalOhlcv.volume[index];
26
- const smaInstance = instances[key].instance;
27
- smaInstance.update(value);
28
-
29
- let smaValue = NaN;
30
- try {
31
- smaValue = smaInstance.getResult();
32
- } catch (err) {
25
+ const volume = verticalOhlcv.volume[index];
33
26
 
27
+ if(volume === 0) {
28
+ return;
34
29
  }
30
+
31
+ const smaInstance = instances[key].instance;
32
+ smaInstance.update(volume);
33
+
34
+ const smaValue = smaInstance.isStable ? smaInstance.getResult() : NaN;
35
35
 
36
36
  const prevSma = instances[key].prevRelativeVolumeSma;
37
- let currRelativeVolume = NaN;
38
37
 
39
- // Only calculate relative volume if both current SMA and previous SMA are valid numbers and prevSma is not zero.
40
- if (!Number.isNaN(smaValue) && !Number.isNaN(prevSma)) {
41
- currRelativeVolume = value / prevSma;
38
+ // Save the current SMA for the next nonzero-volume row.
39
+ instances[key].prevRelativeVolumeSma = smaValue;
40
+
41
+ if(Number.isNaN(smaValue) || Number.isNaN(prevSma)) {
42
+ return
42
43
  }
43
44
 
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;
51
+
44
52
  main.pushToMain({ index, key, value: currRelativeVolume });
45
- instances[key].prevRelativeVolumeSma = smaValue;
46
53
 
47
- return true;
48
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
+ };
@@ -1,6 +1,7 @@
1
- import { FasterRSI } from 'trading-signals';
2
- import { FasterSMA } from 'trading-signals';
1
+ import { FasterRSI, FasterSMA } from '../core-indicators/index.js';
3
2
  import { mathLog } from '../utilities/math.js';
3
+ import { validateInputValues } from '../utilities/validators.js';
4
+ import { initializeColumns } from '../core-functions/initializeColumns.js';
4
5
 
5
6
  const defaultTarget = 'close'
6
7
  export const rsi = (main, index, size, { target, lag, retLogs }) => {
@@ -15,8 +16,7 @@ export const rsi = (main, index, size, { target, lag, retLogs }) => {
15
16
 
16
17
  // Initialization on the first index.
17
18
  if (index === 0) {
18
-
19
- const {len } = main;
19
+ validateInputValues({ [target]: true }, verticalOhlcv, index, 'rsi');
20
20
 
21
21
  if (!verticalOhlcv.hasOwnProperty(target)) {
22
22
  throw new Error(`Target property ${target} not found in verticalOhlcv for rsi.`);
@@ -27,32 +27,17 @@ export const rsi = (main, index, size, { target, lag, retLogs }) => {
27
27
  [rsiSmaKey]: new FasterSMA(size)
28
28
  })
29
29
 
30
- Object.assign(verticalOhlcv, {
31
- [rsiKey]: new Float64Array(len).fill(NaN),
32
- [rsiSmaKey]: new Float64Array(len).fill(NaN),
33
- })
34
-
35
- const baseKeys = [rsiKey, rsiSmaKey]
36
-
37
- if (lag > 0) {
38
- main.lag(baseKeys, lag);
39
- }
30
+ initializeColumns(main, [{ key: rsiKey }, { key: rsiSmaKey }], { lag });
40
31
 
41
32
 
42
33
  }
43
34
 
44
35
  const value = verticalOhlcv[target][index];
45
- let currentRsi = NaN;
46
- let smoothedRsi = NaN;
47
36
 
48
37
  // Update the RSI indicator.
49
38
  instances[rsiKey].update(value);
50
39
 
51
- try {
52
- currentRsi = instances[rsiKey].getResult();
53
- } catch (err) {
54
- currentRsi = NaN;
55
- }
40
+ const currentRsi = instances[rsiKey].isStable ? instances[rsiKey].getResult() : NaN;
56
41
 
57
42
 
58
43
  const rsiVal = Number.isNaN(currentRsi) ? NaN : (retLogs ? mathLog(currentRsi, 50) : currentRsi)
@@ -63,11 +48,7 @@ export const rsi = (main, index, size, { target, lag, retLogs }) => {
63
48
  instances[rsiSmaKey].update(currentRsi);
64
49
  }
65
50
 
66
- try {
67
- smoothedRsi = instances[rsiSmaKey].getResult();
68
- } catch (err) {
69
- smoothedRsi = NaN;
70
- }
51
+ const smoothedRsi = instances[rsiSmaKey].isStable ? instances[rsiSmaKey].getResult() : NaN;
71
52
 
72
53
  const smoothedRsiVal = Number.isNaN(smoothedRsi) ? NaN : (retLogs ? mathLog(smoothedRsi, 50) : smoothedRsi)
73
54