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.
- package/AGENTS.md +113 -0
- package/README.md +456 -0
- package/dist/ohlcv-indicators.min.js +2 -1
- package/dist/ohlcv-indicators.min.js.LICENSE.txt +22 -0
- package/index.js +112 -84
- package/package.json +2 -5
- package/src/core-functions/initializeColumns.js +40 -0
- package/src/core-functions/mainLoop.js +35 -30
- package/src/core-functions/pushToMain.js +8 -27
- package/src/core-indicators/README.md +60 -0
- package/src/core-indicators/composites.js +136 -0
- package/src/core-indicators/index.js +5 -0
- package/src/core-indicators/indicator.js +81 -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 +39 -31
- package/src/moving-averages/donchianChannel.js +62 -36
- package/src/moving-averages/heikenAshi.js +33 -52
- package/src/moving-averages/macd.js +10 -26
- package/src/moving-averages/movingAverages.js +19 -25
- package/src/moving-averages/relativeVolume.js +30 -24
- package/src/oscillators/mfi.js +56 -0
- 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 +24 -32
- package/src/studies/candleFeatures.js +40 -28
- package/src/studies/dateTime.js +54 -56
- package/src/studies/findCrosses.js +18 -16
- package/src/studies/lag.js +32 -20
- package/src/utilities/assignTypes.js +1 -1
- package/src/utilities/config.js +31 -0
- package/src/utilities/numberUtilities.js +5 -3
- package/src/utilities/precisionMultiplier.js +11 -6
- package/src/utilities/validators.js +40 -1
- package/src/utilities/verticalToHorizontal.js +26 -16
- package/src/volatility/adx.js +7 -20
- package/src/volatility/atr.js +10 -23
- package/test/test.js +1230 -0
- package/webpack.config.js +0 -11
- package/src/moving-averages/vidya.js +0 -181
- 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 = (
|
|
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
|
-
|
|
19
|
-
{
|
|
20
|
-
const
|
|
21
|
-
if(
|
|
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
|
-
|
|
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 '
|
|
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
|
|
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,
|
|
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[
|
|
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
|
-
|
|
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
|
-
}
|
|
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[
|
|
57
|
+
const instance = instances.bollinger_bands.settings[instanceKey]
|
|
61
58
|
const value = verticalOhlcv[target][index]
|
|
62
59
|
instance.update(value)
|
|
63
60
|
|
|
64
|
-
|
|
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
|
+
}
|