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.
- package/AGENTS.md +105 -0
- package/README.md +415 -0
- package/dist/ohlcv-indicators.min.js +2 -1
- package/dist/ohlcv-indicators.min.js.LICENSE.txt +23 -0
- package/index.js +93 -55
- package/package.json +1 -1
- package/src/core-functions/initializeColumns.js +40 -0
- package/src/core-functions/mainLoop.js +47 -22
- package/src/core-functions/pushToMain.js +1 -3
- package/src/core-indicators/README.md +61 -0
- package/src/core-indicators/composites.js +136 -0
- package/src/core-indicators/index.js +5 -0
- package/src/core-indicators/indicator.js +82 -0
- package/src/core-indicators/movingAverages.js +88 -0
- package/src/core-indicators/rsi.js +41 -0
- package/src/core-indicators/volatility.js +107 -0
- package/src/moving-averages/bollingerBands.js +11 -26
- package/src/moving-averages/donchianChannel.js +30 -29
- package/src/moving-averages/heikenAshi.js +23 -33
- package/src/moving-averages/macd.js +10 -26
- package/src/moving-averages/movingAverages.js +7 -23
- package/src/moving-averages/relativeVolume.js +23 -21
- package/src/oscillators/rsi.js +7 -26
- package/src/oscillators/stochastic.js +6 -19
- package/src/oscillators/volumeDelta.js +36 -28
- package/src/oscillators/volumeOscillator.js +29 -31
- package/src/studies/candleFeatures.js +12 -20
- package/src/studies/dateTime.js +22 -27
- package/src/studies/findCrosses.js +16 -7
- package/src/studies/lag.js +26 -13
- package/src/studies/mapCols.js +14 -15
- package/src/utilities/assignTypes.js +1 -1
- package/src/utilities/config.js +64 -0
- package/src/utilities/numberUtilities.js +2 -0
- package/src/utilities/validators.js +40 -1
- package/src/utilities/verticalToHorizontal.js +21 -16
- package/src/volatility/adx.js +7 -20
- package/src/volatility/atr.js +10 -23
- package/test/test.js +533 -0
- 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 '
|
|
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,
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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,
|
|
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] = {
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
while (dq.length &&
|
|
66
|
-
|
|
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[
|
|
74
|
-
const lower = hasBounds ? lows[minDeque[
|
|
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
|
-
|
|
82
|
-
}
|
|
83
|
+
}
|