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