ohlcv-indicators 3.5.78 → 3.5.79

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 (40) hide show
  1. package/AGENTS.md +105 -0
  2. package/README.md +415 -0
  3. package/dist/ohlcv-indicators.min.js +2 -1
  4. package/dist/ohlcv-indicators.min.js.LICENSE.txt +23 -0
  5. package/index.js +93 -55
  6. package/package.json +1 -1
  7. package/src/core-functions/initializeColumns.js +40 -0
  8. package/src/core-functions/mainLoop.js +47 -22
  9. package/src/core-functions/pushToMain.js +1 -3
  10. package/src/core-indicators/README.md +61 -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 +82 -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 +11 -26
  18. package/src/moving-averages/donchianChannel.js +30 -29
  19. package/src/moving-averages/heikenAshi.js +23 -33
  20. package/src/moving-averages/macd.js +10 -26
  21. package/src/moving-averages/movingAverages.js +7 -23
  22. package/src/moving-averages/relativeVolume.js +23 -21
  23. package/src/oscillators/rsi.js +7 -26
  24. package/src/oscillators/stochastic.js +6 -19
  25. package/src/oscillators/volumeDelta.js +36 -28
  26. package/src/oscillators/volumeOscillator.js +29 -31
  27. package/src/studies/candleFeatures.js +12 -20
  28. package/src/studies/dateTime.js +22 -27
  29. package/src/studies/findCrosses.js +16 -7
  30. package/src/studies/lag.js +26 -13
  31. package/src/studies/mapCols.js +14 -15
  32. package/src/utilities/assignTypes.js +1 -1
  33. package/src/utilities/config.js +64 -0
  34. package/src/utilities/numberUtilities.js +2 -0
  35. package/src/utilities/validators.js +40 -1
  36. package/src/utilities/verticalToHorizontal.js +21 -16
  37. package/src/volatility/adx.js +7 -20
  38. package/src/volatility/atr.js +10 -23
  39. package/test/test.js +533 -0
  40. package/src/moving-averages/vidya.js +0 -181
@@ -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,82 @@
1
+ /*!
2
+ * Portions adapted from trading-signals 5.0.4.
3
+ * MIT License
4
+ * Copyright (c) 2020 Benny Neugebauer
5
+ *
6
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ * of this software and associated documentation files (the "Software"), to deal
8
+ * in the Software without restriction, including without limitation the rights
9
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ * copies of the Software, and to permit persons to whom the Software is
11
+ * furnished to do so, subject to the following conditions:
12
+ *
13
+ * The above copyright notice and this permission notice shall be included in all
14
+ * copies or substantial portions of the Software.
15
+ *
16
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ * SOFTWARE.
23
+ */
24
+
25
+ export class NotEnoughDataError extends Error {
26
+ constructor(message = 'Not enough data') {
27
+ super(message)
28
+ this.name = 'NotEnoughDataError'
29
+ }
30
+ }
31
+
32
+ export class Indicator {
33
+ result
34
+
35
+ get isStable() {
36
+ return this.result !== undefined
37
+ }
38
+
39
+ getResult() {
40
+ if (!this.isStable) throw new NotEnoughDataError()
41
+ return this.result
42
+ }
43
+ }
44
+
45
+ // Keep the public result/extrema and replacement semantics without price history.
46
+ export class NumberIndicator extends Indicator {
47
+ previousResult
48
+ highest
49
+ lowest
50
+ previousHighest
51
+ previousLowest
52
+
53
+ setResult(value, replace = false) {
54
+ if (replace) {
55
+ this.highest = this.previousHighest
56
+ this.lowest = this.previousLowest
57
+ this.result = this.previousResult
58
+ }
59
+ if (this.highest === undefined) this.highest = value
60
+ else {
61
+ this.previousHighest = this.highest
62
+ if (value > this.highest) this.highest = value
63
+ }
64
+ if (this.lowest === undefined) this.lowest = value
65
+ else {
66
+ this.previousLowest = this.lowest
67
+ if (value < this.lowest) this.lowest = value
68
+ }
69
+ this.previousResult = this.result
70
+ this.result = value
71
+ return value
72
+ }
73
+
74
+ replace(input) {
75
+ return this.update(input, true)
76
+ }
77
+
78
+ updates(prices) {
79
+ prices.forEach(price => this.update(price))
80
+ return this.result
81
+ }
82
+ }
@@ -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,4 +1,6 @@
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'
2
4
 
3
5
  export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
4
6
  const { verticalOhlcv, instances, useFullNames } = main
@@ -8,7 +10,9 @@ export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
8
10
 
9
11
  // Initialization on the first call.
10
12
  if (index === 0) {
11
- const { inputParams, verticalOhlcv, len, priceBased } = main
13
+ const { inputParams, verticalOhlcv, priceBased } = main
14
+
15
+ validateInputValues({ [target]: true }, verticalOhlcv, index, 'bollingerBands')
12
16
 
13
17
  if (!(target in verticalOhlcv)) {
14
18
  throw new Error(`bollingerBands could not find target "${target}"`)
@@ -37,21 +41,9 @@ export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
37
41
  `${prefix}_lower${suffix}`,
38
42
  ]
39
43
 
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
- }
44
+ initializeColumns(main, keyNames.map(key => ({
45
+ key, priceBased: priceBased.has(target)
46
+ })), { lag })
55
47
  }
56
48
 
57
49
  const { numberOfIndicators } = instances.bollinger_bands
@@ -61,12 +53,7 @@ export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
61
53
  const value = verticalOhlcv[target][index]
62
54
  instance.update(value)
63
55
 
64
- let result = {}
65
- try {
66
- result = instance.getResult()
67
- } catch (err) {
68
- // If not available, result stays {}.
69
- }
56
+ const result = instance.isStable ? instance.getResult() : null
70
57
 
71
58
  const upper = result?.upper ?? NaN
72
59
  const middle = result?.middle ?? NaN
@@ -75,6 +62,4 @@ export const bollingerBands = (main, index, size, stdDev, { lag } = {}) => {
75
62
  main.pushToMain({ index, key: `${prefix}_upper${suffix}`, value: upper })
76
63
  main.pushToMain({ index, key: `${prefix}_middle${suffix}`, value: middle })
77
64
  main.pushToMain({ index, key: `${prefix}_lower${suffix}`, value: lower })
78
-
79
- return true
80
- }
65
+ }
@@ -1,10 +1,15 @@
1
+ import { validateInputValues } from '../utilities/validators.js'
2
+ import { initializeColumns } from '../core-functions/initializeColumns.js'
3
+
1
4
  export const donchianChannels = (main, index, size, offset, options) => {
2
- const { verticalOhlcv, instances, len, inputParams, priceBased, useFullNames } = main
5
+ const { verticalOhlcv, instances, len, inputParams, useFullNames } = main
3
6
  const { lag: outputLag = 0 } = options
4
7
  const indicatorKey = `${size}_${offset}`
5
8
 
6
9
  // ---- INIT (only at first bar) ----
7
10
  if (index === 0) {
11
+ validateInputValues({ high: true, low: true }, verticalOhlcv, index, 'donchianChannels')
12
+
8
13
  const numberOfIndicators = inputParams.filter(o => o.key === 'donchianChannels').length
9
14
  const useIndexedKeys = numberOfIndicators > 1 || useFullNames
10
15
 
@@ -15,25 +20,17 @@ export const donchianChannels = (main, index, size, offset, options) => {
15
20
 
16
21
  const keys = ['upper', 'basis', 'lower'].map(getKey)
17
22
 
18
- for (const k of keys) {
19
- priceBased.add(k)
20
- }
21
-
22
23
  if (!instances.donchian_channel) {
23
24
  instances.donchian_channel = { numberOfIndicators, settings: {} }
24
25
  }
25
26
 
26
27
  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)
28
+ instances.donchian_channel.settings[indicatorKey] = {
29
+ maxDeque: { indices: [], head: 0, length: 0 },
30
+ minDeque: { indices: [], head: 0, length: 0 }
36
31
  }
32
+
33
+ initializeColumns(main, keys.map(key => ({ key, priceBased: true })), { lag: outputLag })
37
34
  }
38
35
 
39
36
  // ---- PER-BAR COMPUTATION ----
@@ -49,34 +46,38 @@ export const donchianChannels = (main, index, size, offset, options) => {
49
46
  const current = index - offset
50
47
  const start = current - size + 1
51
48
 
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
- }
49
+ // No source candle is available yet.
50
+ if (current < 0 || current >= len) return
60
51
 
61
52
  const { high: highs, low: lows } = verticalOhlcv
62
53
 
63
54
  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)
55
+ const { indices } = dq
56
+ while (dq.length && indices[dq.head] < start) {
57
+ dq.head = (dq.head + 1) % size
58
+ dq.length--
59
+ }
60
+ while (dq.length && cmp(arr[indices[(dq.head + dq.length - 1) % size]], arr[current])) {
61
+ dq.length--
62
+ }
63
+ // Reuse at most size slots instead of moving entries or growing with the input.
64
+ indices[(dq.head + dq.length) % size] = current
65
+ dq.length++
67
66
  }
68
67
 
69
68
  update(maxDeque, highs, (a, b) => a <= b)
70
69
  update(minDeque, lows, (a, b) => a >= b)
71
70
 
71
+ // Collect warm-up candles, but leave outputs as NaN until the window is complete.
72
+ if (start < 0) return
73
+
72
74
  const hasBounds = maxDeque.length && minDeque.length
73
- const upper = hasBounds ? highs[maxDeque[0]] : NaN
74
- const lower = hasBounds ? lows[minDeque[0]] : NaN
75
+ const upper = hasBounds ? highs[maxDeque.indices[maxDeque.head]] : NaN
76
+ const lower = hasBounds ? lows[minDeque.indices[minDeque.head]] : NaN
75
77
  const basis = hasBounds ? (upper + lower) / 2 : NaN
76
78
 
77
79
  main.pushToMain({ index, key: getKey('upper'), value: upper })
78
80
  main.pushToMain({ index, key: getKey('basis'), value: basis })
79
81
  main.pushToMain({ index, key: getKey('lower'), value: lower })
80
82
 
81
- return true
82
- }
83
+ }