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
@@ -2,34 +2,15 @@ export const pushToMain = ({main, index, key, value}) => {
2
2
 
3
3
  const {verticalOhlcv} = main
4
4
 
5
- verticalOhlcv[key][index] = value
6
-
7
- return true
5
+ verticalOhlcv[key][index] = value
8
6
  }
9
7
 
10
- export const areKeyValuesValid = (main, index, keyNames) => {
11
-
12
- if(keyNames.length === 0) return false
13
-
14
- const {verticalOhlcv} = main
15
-
16
- let output = true
8
+ export const areKeyValuesValid = (columns, index) => {
17
9
 
18
- for(let x = 0 ; x < keyNames.length; x++)
19
- {
20
- const key = keyNames[x]
21
- if(!verticalOhlcv.hasOwnProperty(key)) {
22
- output = false
23
- break
24
- }
25
-
26
- const val = verticalOhlcv[key][index]
27
-
28
- if (val == null || Number.isNaN(val)) {
29
- output = false
30
- break
31
- }
10
+ if (columns.length === 0) return false
11
+ for (let x = 0; x < columns.length; x++) {
12
+ const val = columns[x][index]
13
+ if (val == null || Number.isNaN(val)) return false
32
14
  }
33
-
34
- return output
35
- }
15
+ return true
16
+ }
@@ -0,0 +1,60 @@
1
+ # Core indicators
2
+
3
+ Local numeric `Faster*` indicator implementations. The existing constructor arguments, `update(value,
4
+ replace?)`, `isStable`, and `getResult()` interface remain available. The
5
+ numeric series also retain result/extrema and replacement semantics. These
6
+ use ordinary JavaScript numbers, not arbitrary-precision decimal arithmetic.
7
+
8
+ ```js
9
+ import { FasterEMA } from './index.js'
10
+
11
+ const ema = new FasterEMA(14)
12
+ ema.update(price)
13
+ const value = ema.isStable ? ema.getResult() : NaN
14
+ ```
15
+
16
+ `getResult()` still throws `NotEnoughDataError` during warm-up for API
17
+ compatibility. All readiness checks are non-throwing; project handlers check
18
+ readiness before reading. EMA still returns its provisional result from
19
+ `update()` before it becomes stable, so do not substitute that return value
20
+ for a readiness-checked read.
21
+
22
+ ## Storage and calculation choices
23
+
24
+ - EMA uses scalar state and a readiness counter.
25
+ - SMA uses a bounded circular `Float64Array`. It sums oldest to newest to
26
+ preserve floating-point rounding and recovery after NaN/Infinity. It remains
27
+ O(period) per update rather than adopting a different running-sum formula.
28
+ - WSMA releases its seed SMA after initialization, then updates in O(1).
29
+ - RSI retains two prices instead of the entire input history.
30
+ - MACD uses a capped counter instead of retaining an unused price window.
31
+ - Bollinger uses a bounded numeric ring and two ordered passes, without
32
+ intermediate deviation arrays. Its size + 1 warm-up is intentional.
33
+ - Stochastic uses a bounded candle-reference ring, scans extrema without
34
+ temporary arrays, and preserves the original zero/NaN smoothing behavior.
35
+ - TR/ATR/DX/ADX retain the existing arithmetic and replacement behavior.
36
+
37
+ Scalar rings switch once to a bounded ordinary Array if a non-number is
38
+ provided. This preserves existing JavaScript coercion for custom mapped
39
+ columns without adding object storage for normal numeric input. NaN and
40
+ Infinity remain numeric values and do not trigger this fallback. Candle
41
+ references are retained where the reference implementation observes mutations
42
+ of previously supplied objects. Private buffer layouts intentionally differ.
43
+
44
+ ## Verification
45
+
46
+ `npm test` checks known core results and fixed behavior traces captured before
47
+ runtime optimizations, checks bounded retained storage, and exercises all
48
+ public indicators, precision/log modes, lags, and config replay.
49
+ These checks are self-contained: no third-party indicator library or test
50
+ oracle is installed. Preserve the trace expectations unless intentionally
51
+ changing the corresponding behavior.
52
+
53
+ `node test/test.js --baseline /path/to/original/index.js --benchmark` also
54
+ compares the full pipeline with a pre-change checkout. Benchmarks report
55
+ local timings and retained array slots/buffer bytes, not total heap usage or
56
+ performance guarantees. Results must remain exact; changing summation order,
57
+ warm-up, or quirks is a separate behavior change.
58
+
59
+ Adapted portions retain the upstream MIT notice in `indicator.js`, also
60
+ preserved in the generated distribution's license notice.
@@ -0,0 +1,136 @@
1
+ import { Indicator } from './indicator.js'
2
+ import { FasterSMA } from './movingAverages.js'
3
+
4
+ export class FasterBollingerBands extends Indicator {
5
+ constructor(interval, deviationMultiplier = 2) {
6
+ super()
7
+ this.interval = interval
8
+ this.deviationMultiplier = deviationMultiplier
9
+ this.prices = new Float64Array(interval)
10
+ this.count = 0
11
+ this.head = 0
12
+ }
13
+
14
+ update(price) {
15
+ // Custom Array-backed columns may contain raw values whose addition must not be coerced.
16
+ if (typeof price !== 'number' && !Array.isArray(this.prices)) {
17
+ this.prices = Array.from(this.prices)
18
+ }
19
+
20
+ if (this.count < this.interval) {
21
+ this.prices[this.count++] = price
22
+ return
23
+ }
24
+
25
+ // The first full window is deliberately skipped, matching the original warm-up.
26
+ this.prices[this.head] = price
27
+ this.head = (this.head + 1) % this.interval
28
+
29
+ let sum = 0
30
+ let slot = this.head
31
+ for (let i = 0; i < this.count; i++) {
32
+ sum += this.prices[slot]
33
+ if (++slot === this.interval) slot = 0
34
+ }
35
+ const middle = this.count ? sum / this.count : 0
36
+ let deviationMiddle = middle
37
+ if (!deviationMiddle && Array.isArray(this.prices)) {
38
+ // Match the original average || fallback, including repeated raw-value coercion.
39
+ sum = 0
40
+ slot = this.head
41
+ for (let i = 0; i < this.count; i++) {
42
+ sum += this.prices[slot]
43
+ if (++slot === this.interval) slot = 0
44
+ }
45
+ deviationMiddle = this.count ? sum / this.count : 0
46
+ }
47
+
48
+ // Re-sum in chronological order; a rolling variance changes floating-point results.
49
+ let squaredSum = 0
50
+ slot = this.head
51
+ for (let i = 0; i < this.count; i++) {
52
+ const difference = this.prices[slot] - deviationMiddle
53
+ squaredSum += difference * difference
54
+ if (++slot === this.interval) slot = 0
55
+ }
56
+ const standardDeviation = Math.sqrt(this.count ? squaredSum / this.count : 0)
57
+
58
+ return (this.result = {
59
+ lower: middle - standardDeviation * this.deviationMultiplier,
60
+ middle,
61
+ upper: middle + standardDeviation * this.deviationMultiplier
62
+ })
63
+ }
64
+ }
65
+
66
+ export class FasterMACD extends Indicator {
67
+ constructor(short, long, signal) {
68
+ super()
69
+ this.short = short
70
+ this.long = long
71
+ this.signal = signal
72
+ this.pricesCounter = 0
73
+ }
74
+
75
+ update(price, replace = false) {
76
+ if (!replace || this.pricesCounter === 0) this.pricesCounter++
77
+
78
+ const short = this.short.update(price, replace)
79
+ const long = this.long.update(price, replace)
80
+
81
+ if (this.pricesCounter > this.long.interval) this.pricesCounter--
82
+ if (this.pricesCounter === this.long.interval) {
83
+ const macd = short - long
84
+ const signal = this.signal.update(macd, replace)
85
+ return (this.result = { histogram: macd - signal, macd, signal })
86
+ }
87
+ }
88
+ }
89
+
90
+ export class FasterStochasticOscillator extends Indicator {
91
+ constructor(n, m, p) {
92
+ super()
93
+ this.n = n
94
+ this.m = m
95
+ this.p = p
96
+ this.periodM = new FasterSMA(m)
97
+ this.periodP = new FasterSMA(p)
98
+ // Keep candle references: callers may reuse or mutate objects retained in the window.
99
+ this.candles = new Array(n)
100
+ this.count = 0
101
+ this.head = 0
102
+ }
103
+
104
+ update(candle) {
105
+ if (this.count < this.n) {
106
+ this.candles[this.count++] = candle
107
+ } else {
108
+ this.candles[this.head] = candle
109
+ this.head = (this.head + 1) % this.n
110
+ }
111
+
112
+ if (this.count === this.n) {
113
+ let highest = -Infinity
114
+ let lowest = Infinity
115
+ let slot = this.head
116
+ for (let i = 0; i < this.count; i++) {
117
+ highest = Math.max(highest, this.candles[slot].high)
118
+ if (++slot === this.n) slot = 0
119
+ }
120
+ slot = this.head
121
+ for (let i = 0; i < this.count; i++) {
122
+ lowest = Math.min(lowest, this.candles[slot].low)
123
+ if (++slot === this.n) slot = 0
124
+ }
125
+
126
+ const divisor = highest - lowest
127
+ const fastK = ((candle.close - lowest) * 100) / (divisor === 0 ? 1 : divisor)
128
+ const stochK = this.periodM.update(fastK)
129
+ // Zero and NaN deliberately skip periodP, while still producing a result.
130
+ const stochD = stochK && this.periodP.update(stochK)
131
+ if (stochK !== undefined && stochD !== undefined) {
132
+ return (this.result = { stochD, stochK })
133
+ }
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,5 @@
1
+ export { NotEnoughDataError } from './indicator.js'
2
+ export { FasterEMA, FasterSMA, FasterWSMA } from './movingAverages.js'
3
+ export { FasterRSI } from './rsi.js'
4
+ export { FasterTR, FasterATR, FasterDX, FasterADX } from './volatility.js'
5
+ export { FasterBollingerBands, FasterMACD, FasterStochasticOscillator } from './composites.js'
@@ -0,0 +1,81 @@
1
+ /*!
2
+ * MIT License
3
+ * Copyright (c) 2020 Benny Neugebauer
4
+ *
5
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ * of this software and associated documentation files (the "Software"), to deal
7
+ * in the Software without restriction, including without limitation the rights
8
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ * copies of the Software, and to permit persons to whom the Software is
10
+ * furnished to do so, subject to the following conditions:
11
+ *
12
+ * The above copyright notice and this permission notice shall be included in all
13
+ * copies or substantial portions of the Software.
14
+ *
15
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ * SOFTWARE.
22
+ */
23
+
24
+ export class NotEnoughDataError extends Error {
25
+ constructor(message = 'Not enough data') {
26
+ super(message)
27
+ this.name = 'NotEnoughDataError'
28
+ }
29
+ }
30
+
31
+ export class Indicator {
32
+ result
33
+
34
+ get isStable() {
35
+ return this.result !== undefined
36
+ }
37
+
38
+ getResult() {
39
+ if (!this.isStable) throw new NotEnoughDataError()
40
+ return this.result
41
+ }
42
+ }
43
+
44
+ // Keep the public result/extrema and replacement semantics without price history.
45
+ export class NumberIndicator extends Indicator {
46
+ previousResult
47
+ highest
48
+ lowest
49
+ previousHighest
50
+ previousLowest
51
+
52
+ setResult(value, replace = false) {
53
+ if (replace) {
54
+ this.highest = this.previousHighest
55
+ this.lowest = this.previousLowest
56
+ this.result = this.previousResult
57
+ }
58
+ if (this.highest === undefined) this.highest = value
59
+ else {
60
+ this.previousHighest = this.highest
61
+ if (value > this.highest) this.highest = value
62
+ }
63
+ if (this.lowest === undefined) this.lowest = value
64
+ else {
65
+ this.previousLowest = this.lowest
66
+ if (value < this.lowest) this.lowest = value
67
+ }
68
+ this.previousResult = this.result
69
+ this.result = value
70
+ return value
71
+ }
72
+
73
+ replace(input) {
74
+ return this.update(input, true)
75
+ }
76
+
77
+ updates(prices) {
78
+ prices.forEach(price => this.update(price))
79
+ return this.result
80
+ }
81
+ }
@@ -0,0 +1,88 @@
1
+ import { NumberIndicator } from './indicator.js'
2
+
3
+ export class FasterEMA extends NumberIndicator {
4
+ constructor(interval) {
5
+ super()
6
+ this.interval = interval
7
+ this.pricesCounter = 0
8
+ this.weightFactor = 2 / (interval + 1)
9
+ }
10
+
11
+ update(price, replace = false) {
12
+ if (!replace || this.pricesCounter === 0) this.pricesCounter++
13
+
14
+ const previous = replace && this.previousResult !== undefined
15
+ ? this.previousResult
16
+ : this.result !== undefined ? this.result : price
17
+
18
+ return this.setResult(price * this.weightFactor + previous * (1 - this.weightFactor), replace)
19
+ }
20
+
21
+ get isStable() {
22
+ return this.pricesCounter >= this.interval
23
+ }
24
+ }
25
+
26
+ export class FasterSMA extends NumberIndicator {
27
+ constructor(interval) {
28
+ super()
29
+ this.interval = interval
30
+ this.prices = new Float64Array(interval)
31
+ this.count = 0
32
+ this.head = 0
33
+ }
34
+
35
+ update(price, replace = false) {
36
+ // Custom columns may contain raw values after their validated first row.
37
+ // Retain JavaScript addition/coercion semantics without slowing numeric windows.
38
+ if (typeof price !== 'number' && !Array.isArray(this.prices)) {
39
+ this.prices = Array.from(this.prices)
40
+ }
41
+ const { interval, prices } = this
42
+
43
+ if (replace && this.count) {
44
+ let last = this.head + this.count - 1
45
+ if (last >= interval) last -= interval
46
+ prices[last] = price
47
+ } else if (this.count < interval) {
48
+ prices[this.count++] = price
49
+ } else {
50
+ prices[this.head] = price
51
+ if (++this.head === interval) this.head = 0
52
+ }
53
+
54
+ if (this.count === interval) {
55
+ // Preserve chronological addition order, including NaN/Infinity recovery.
56
+ let sum = 0
57
+ for (let i = this.head; i < interval; i++) sum += prices[i]
58
+ for (let i = 0; i < this.head; i++) sum += prices[i]
59
+ return this.setResult(sum / interval, replace)
60
+ }
61
+ }
62
+ }
63
+
64
+ export class FasterWSMA extends NumberIndicator {
65
+ constructor(interval) {
66
+ super()
67
+ this.interval = interval
68
+ this.indicator = new FasterSMA(interval)
69
+ this.smoothingFactor = 1 / interval
70
+ }
71
+
72
+ update(price, replace = false) {
73
+ if (this.result === undefined) {
74
+ // The existing FasterWSMA appends seed samples even for replacement updates.
75
+ const sma = this.indicator.update(price)
76
+ if (sma !== undefined) {
77
+ this.indicator = undefined
78
+ return this.setResult(sma, replace)
79
+ }
80
+ } else if (replace && this.previousResult !== undefined) {
81
+ const smoothed = (price - this.previousResult) * this.smoothingFactor
82
+ return this.setResult(smoothed + this.previousResult, replace)
83
+ } else if (!replace) {
84
+ const smoothed = (price - this.result) * this.smoothingFactor
85
+ return this.setResult(smoothed + this.result, replace)
86
+ }
87
+ }
88
+ }
@@ -0,0 +1,41 @@
1
+ import { NumberIndicator } from './indicator.js';
2
+ import { FasterWSMA } from './movingAverages.js';
3
+
4
+ export class FasterRSI extends NumberIndicator {
5
+ constructor(interval, SmoothingIndicator = FasterWSMA) {
6
+ super();
7
+ this.interval = interval;
8
+ this.avgGain = new SmoothingIndicator(interval);
9
+ this.avgLoss = new SmoothingIndicator(interval);
10
+ this.maxValue = 100;
11
+ this.priceCount = 0;
12
+ this.previousPrice = undefined;
13
+ this.currentPrice = undefined;
14
+ }
15
+
16
+ update(price, replace = false) {
17
+ // Only the latest two prices are needed, including repeated replacements.
18
+ if (!replace || this.priceCount === 0) {
19
+ this.previousPrice = this.currentPrice;
20
+ if (this.priceCount < 2) this.priceCount++;
21
+ }
22
+ this.currentPrice = price;
23
+ if (this.priceCount < 2) return;
24
+
25
+ const previousPrice = this.previousPrice;
26
+ if (price > previousPrice) {
27
+ this.avgLoss.update(0, replace);
28
+ this.avgGain.update(price - previousPrice, replace);
29
+ } else {
30
+ this.avgLoss.update(previousPrice - price, replace);
31
+ this.avgGain.update(0, replace);
32
+ }
33
+
34
+ if (this.avgGain.isStable) {
35
+ const avgLoss = this.avgLoss.getResult();
36
+ if (avgLoss === 0) return this.setResult(100, replace);
37
+ const relativeStrength = this.avgGain.getResult() / avgLoss;
38
+ return this.setResult(this.maxValue - this.maxValue / (relativeStrength + 1), replace);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,107 @@
1
+ import { NumberIndicator } from './indicator.js';
2
+ import { FasterWSMA } from './movingAverages.js';
3
+
4
+ export class FasterTR extends NumberIndicator {
5
+ constructor() {
6
+ super();
7
+ this.previousCandle = undefined;
8
+ this.twoPreviousCandle = undefined;
9
+ }
10
+
11
+ update(candle, replace = false) {
12
+ const { high, low } = candle;
13
+ const highLow = high - low;
14
+ if (this.previousCandle && replace) this.previousCandle = this.twoPreviousCandle;
15
+ if (this.previousCandle) {
16
+ const highClose = Math.abs(high - this.previousCandle.close);
17
+ const lowClose = Math.abs(low - this.previousCandle.close);
18
+ this.twoPreviousCandle = this.previousCandle;
19
+ this.previousCandle = candle;
20
+ return this.setResult(Math.max(highLow, highClose, lowClose), replace);
21
+ }
22
+ this.twoPreviousCandle = this.previousCandle;
23
+ this.previousCandle = candle;
24
+ return this.setResult(highLow, replace);
25
+ }
26
+ }
27
+
28
+ export class FasterATR extends NumberIndicator {
29
+ constructor(interval, SmoothingIndicator = FasterWSMA) {
30
+ super();
31
+ this.interval = interval;
32
+ this.tr = new FasterTR();
33
+ this.smoothing = new SmoothingIndicator(interval);
34
+ }
35
+
36
+ update(candle, replace = false) {
37
+ const trueRange = this.tr.update(candle, replace);
38
+ this.smoothing.update(trueRange, replace);
39
+ if (this.smoothing.isStable) return this.setResult(this.smoothing.getResult(), replace);
40
+ }
41
+ }
42
+
43
+ export class FasterDX extends NumberIndicator {
44
+ constructor(interval, SmoothingIndicator = FasterWSMA) {
45
+ super();
46
+ this.interval = interval;
47
+ this.atr = new FasterATR(interval, SmoothingIndicator);
48
+ this.movesDown = new SmoothingIndicator(interval);
49
+ this.movesUp = new SmoothingIndicator(interval);
50
+ this.previousCandle = undefined;
51
+ this.mdi = undefined;
52
+ this.pdi = undefined;
53
+ }
54
+
55
+ updateState(candle, pdm = 0, mdm = 0) {
56
+ this.atr.update(candle);
57
+ this.movesUp.update(pdm);
58
+ this.movesDown.update(mdm);
59
+ this.previousCandle = candle;
60
+ }
61
+
62
+ update(candle) {
63
+ if (!this.previousCandle) {
64
+ this.updateState(candle);
65
+ return;
66
+ }
67
+ const higherHigh = candle.high - this.previousCandle.high;
68
+ const lowerLow = this.previousCandle.low - candle.low;
69
+ const pdm = higherHigh < 0 || higherHigh < lowerLow ? 0 : higherHigh;
70
+ const mdm = lowerLow < 0 || lowerLow < higherHigh ? 0 : lowerLow;
71
+ this.updateState(candle, pdm, mdm);
72
+
73
+ if (this.movesUp.isStable) {
74
+ const atr = this.atr.getResult();
75
+ this.pdi = this.movesUp.getResult() / atr;
76
+ this.mdi = this.movesDown.getResult() / atr;
77
+ const dmDiff = Math.abs(this.pdi - this.mdi);
78
+ const dmSum = this.pdi + this.mdi;
79
+ if (dmSum === 0) return this.setResult(0, false);
80
+ return this.setResult((dmDiff / dmSum) * 100, false);
81
+ }
82
+ }
83
+ }
84
+
85
+ export class FasterADX extends NumberIndicator {
86
+ constructor(interval, SmoothingIndicator = FasterWSMA) {
87
+ super();
88
+ this.interval = interval;
89
+ this.adx = new SmoothingIndicator(interval);
90
+ this.dx = new FasterDX(interval, SmoothingIndicator);
91
+ }
92
+
93
+ get mdi() {
94
+ return this.dx.mdi;
95
+ }
96
+
97
+ get pdi() {
98
+ return this.dx.pdi;
99
+ }
100
+
101
+ update(candle, replace = false) {
102
+ // DX/smoothing intentionally advance on replace, matching the existing API.
103
+ const result = this.dx.update(candle);
104
+ if (result !== undefined) this.adx.update(result);
105
+ if (this.adx.isStable) return this.setResult(this.adx.getResult(), replace);
106
+ }
107
+ }
@@ -1,14 +1,20 @@
1
- import { FasterBollingerBands } from 'trading-signals'
1
+ import { FasterBollingerBands } from '../core-indicators/index.js'
2
+ import { validateInputValues } from '../utilities/validators.js'
3
+ import { initializeColumns } from '../core-functions/initializeColumns.js'
4
+ import { mathLog } from '../utilities/math.js'
2
5
 
3
- export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
6
+ export const bollingerBands = (main, index, size, stdDev, { lag, retLogs = false } = {}) => {
4
7
  const { verticalOhlcv, instances, useFullNames } = main
5
8
  const indicatorKey = `${size}_${stdDev}`
6
- const prefix = 'bollinger_bands'
9
+ const instanceKey = `${retLogs ? 'ret_log_' : ''}${indicatorKey}`
10
+ const prefix = `${retLogs ? 'ret_log_' : ''}bollinger_bands`
7
11
  const target = 'close'
8
12
 
9
13
  // Initialization on the first call.
10
14
  if (index === 0) {
11
- const { inputParams, verticalOhlcv, len, priceBased } = main
15
+ const { inputParams, verticalOhlcv, priceBased } = main
16
+
17
+ validateInputValues({ [target]: true }, verticalOhlcv, index, 'bollingerBands')
12
18
 
13
19
  if (!(target in verticalOhlcv)) {
14
20
  throw new Error(`bollingerBands could not find target "${target}"`)
@@ -27,54 +33,56 @@ export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
27
33
  }
28
34
  }
29
35
 
30
- instances.bollinger_bands.settings[indicatorKey] = new FasterBollingerBands(size, stdDev)
36
+ instances.bollinger_bands.settings[instanceKey] = new FasterBollingerBands(size, stdDev)
31
37
 
32
38
  const suffix = (numberOfIndicators > 1 || useFullNames) ? `_${indicatorKey}` : ''
33
39
 
34
- const keyNames = [
40
+ const keyNames = retLogs ? [
41
+ `${prefix}_width${suffix}`,
42
+ `${prefix}_position${suffix}`,
43
+ ] : [
35
44
  `${prefix}_upper${suffix}`,
36
45
  `${prefix}_middle${suffix}`,
37
46
  `${prefix}_lower${suffix}`,
38
47
  ]
39
48
 
40
- if (priceBased.has(target)) {
41
- for (const k of keyNames) {
42
- priceBased.add(k)
43
- }
44
- }
45
-
46
- const verticalOhlcvSetup = Object.fromEntries(
47
- keyNames.map(v => [v, new Float64Array(len).fill(NaN)])
48
- )
49
-
50
- Object.assign(verticalOhlcv, { ...verticalOhlcvSetup })
51
-
52
- if (lag > 0) {
53
- main.lag(keyNames, lag)
54
- }
49
+ initializeColumns(main, keyNames.map(key => ({
50
+ key, priceBased: !retLogs && priceBased.has(target)
51
+ })), { lag })
55
52
  }
56
53
 
57
54
  const { numberOfIndicators } = instances.bollinger_bands
58
55
  const suffix = (numberOfIndicators > 1 || useFullNames) ? `_${indicatorKey}` : ''
59
56
 
60
- const instance = instances.bollinger_bands.settings[indicatorKey]
57
+ const instance = instances.bollinger_bands.settings[instanceKey]
61
58
  const value = verticalOhlcv[target][index]
62
59
  instance.update(value)
63
60
 
64
- let result = {}
65
- try {
66
- result = instance.getResult()
67
- } catch (err) {
68
- // If not available, result stays {}.
69
- }
61
+ const result = instance.isStable ? instance.getResult() : null
70
62
 
71
63
  const upper = result?.upper ?? NaN
72
64
  const middle = result?.middle ?? NaN
73
65
  const lower = result?.lower ?? NaN
74
66
 
67
+ if (retLogs) {
68
+ let width = NaN
69
+ let position = NaN
70
+ const rangeRatio = upper / lower
71
+ if (lower > 0 && upper >= lower && Number.isFinite(upper) && Number.isFinite(rangeRatio)) {
72
+ const logRange = mathLog(rangeRatio, 1)
73
+ width = logRange / 2
74
+ const locationRatio = value / lower
75
+ if (logRange > 0 && value > 0 && locationRatio > 0 && Number.isFinite(locationRatio)) {
76
+ const normalized = 2 * mathLog(locationRatio, 1) / logRange - 1
77
+ if (Number.isFinite(normalized)) position = normalized
78
+ }
79
+ }
80
+ main.pushToMain({ index, key: `${prefix}_width${suffix}`, value: width })
81
+ main.pushToMain({ index, key: `${prefix}_position${suffix}`, value: position })
82
+ return
83
+ }
84
+
75
85
  main.pushToMain({ index, key: `${prefix}_upper${suffix}`, value: upper })
76
86
  main.pushToMain({ index, key: `${prefix}_middle${suffix}`, value: middle })
77
87
  main.pushToMain({ index, key: `${prefix}_lower${suffix}`, value: lower })
78
-
79
- return true
80
- }
88
+ }