ohlcv-indicators 3.5.79 → 3.5.82
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 +24 -17
- package/README.md +104 -78
- package/dist/ohlcv-indicators.min.js +1 -2
- package/index.js +44 -54
- package/package.json +5 -15
- package/src/core-functions/mainLoop.js +26 -46
- package/src/core-functions/pushToMain.js +7 -24
- package/src/core-indicators/README.md +3 -22
- package/src/core-indicators/indicator.js +11 -30
- package/src/moving-averages/bollingerBands.js +29 -6
- package/src/moving-averages/donchianChannel.js +34 -9
- package/src/moving-averages/heikenAshi.js +81 -100
- package/src/moving-averages/movingAverages.js +44 -9
- package/src/moving-averages/relativeVolume.js +9 -5
- package/src/oscillators/mfi.js +56 -0
- package/src/oscillators/volumeOscillator.js +5 -11
- package/src/studies/candleFeatures.js +28 -8
- package/src/studies/dateTime.js +41 -38
- package/src/studies/findCrosses.js +2 -9
- package/src/studies/lag.js +11 -12
- package/src/utilities/assignTypes.js +1 -1
- package/src/utilities/config.js +4 -37
- package/src/utilities/numberUtilities.js +3 -3
- package/src/utilities/precisionMultiplier.js +11 -6
- package/src/utilities/validators.js +1 -1
- package/src/utilities/verticalToHorizontal.js +6 -1
- package/src/volatility/atr.js +1 -1
- package/webpack.config.js +2 -42
- package/dist/ohlcv-indicators.min.js.LICENSE.txt +0 -23
- package/src/studies/mapCols.js +0 -45
- package/test/test.js +0 -533
|
@@ -1,65 +1,53 @@
|
|
|
1
1
|
import { FasterEMA } from '../core-indicators/index.js';
|
|
2
2
|
import { validateInputValues } from '../utilities/validators.js';
|
|
3
3
|
import { initializeColumns } from '../core-functions/initializeColumns.js';
|
|
4
|
-
|
|
4
|
+
import { mathLog } from '../utilities/math.js';
|
|
5
5
|
const isBadNumber = v => v == null || !Number.isFinite(v);
|
|
6
|
+
const arithmeticReturn = (next, prev) => (next - prev) / prev;
|
|
7
|
+
|
|
6
8
|
|
|
7
9
|
export const heikenAshi = (
|
|
8
10
|
main,
|
|
9
11
|
index,
|
|
10
12
|
smoothLength,
|
|
11
13
|
afterSmoothLength,
|
|
12
|
-
{ lag = 0,
|
|
14
|
+
{ lag = 0, noSmoothing = false, retLogs = false } = {}
|
|
13
15
|
) => {
|
|
14
16
|
const { verticalOhlcv, instances, scaledGroups } = main;
|
|
15
|
-
const
|
|
16
|
-
const ohlcKeys = ['open', 'high', 'low', 'close'];
|
|
17
|
-
|
|
18
|
-
const getRet = (next, prev) => retLogs
|
|
19
|
-
? Math.log(next / prev)
|
|
20
|
-
: (next - prev) / prev;
|
|
21
|
-
|
|
22
|
-
const prefix = retLogs ? 'ret_log_' : 'ret_';
|
|
23
|
-
|
|
24
|
-
const paramsKey = bothNull ? '' : `_${indicatorKey}`;
|
|
25
|
-
|
|
26
|
-
const getKey = key => `${prefix}heiken_ashi_${key}${paramsKey}`;
|
|
27
|
-
|
|
28
|
-
const featureKeys = [
|
|
29
|
-
'body',
|
|
30
|
-
'upper_wick',
|
|
31
|
-
'lower_wick',
|
|
32
|
-
'range'
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
const crossKey = bothNull
|
|
36
|
-
? 'heiken_ashi_cross'
|
|
37
|
-
: `heiken_ashi_cross_${indicatorKey}`;
|
|
38
|
-
|
|
39
|
-
const instanceKey = bothNull
|
|
17
|
+
const instanceKey = noSmoothing
|
|
40
18
|
? 'heiken_ashi'
|
|
41
|
-
: `heiken_ashi_${
|
|
19
|
+
: `heiken_ashi_${smoothLength}_${afterSmoothLength}`;
|
|
42
20
|
|
|
43
21
|
// ---- INIT ----
|
|
44
22
|
if (index === 0) {
|
|
45
23
|
validateInputValues({ open: true, high: true, low: true, close: true }, verticalOhlcv, index, 'heikenAshi');
|
|
24
|
+
const ohlcKeys = ['open', 'high', 'low', 'close'];
|
|
25
|
+
const featureKeys = ['body', 'upper_wick', 'lower_wick', 'range'];
|
|
26
|
+
const paramsKey = noSmoothing ? '' : `_${smoothLength}_${afterSmoothLength}`;
|
|
27
|
+
const prefix = retLogs ? 'ret_log_' : 'ret_';
|
|
28
|
+
const outputKeys = Object.fromEntries(
|
|
29
|
+
featureKeys.map(key => [key, `${prefix}heiken_ashi_${key}${paramsKey}`])
|
|
30
|
+
);
|
|
31
|
+
const crossKey = `heiken_ashi_cross${paramsKey}`;
|
|
32
|
+
const keyNames = Object.values(outputKeys);
|
|
46
33
|
|
|
47
34
|
instances[instanceKey] = {
|
|
48
|
-
emaPre: !
|
|
35
|
+
emaPre: !noSmoothing
|
|
49
36
|
? Object.fromEntries(ohlcKeys.map(k => [k, new FasterEMA(smoothLength)]))
|
|
50
37
|
: null,
|
|
51
|
-
emaPost: !
|
|
38
|
+
emaPost: !noSmoothing
|
|
52
39
|
? Object.fromEntries(ohlcKeys.map(k => [k, new FasterEMA(afterSmoothLength)]))
|
|
53
40
|
: null,
|
|
54
41
|
prevHaOpen: NaN,
|
|
55
42
|
prevHaClose: NaN,
|
|
56
43
|
prevSmOpen: NaN,
|
|
57
44
|
prevSmClose: NaN,
|
|
58
|
-
|
|
45
|
+
trendDirection: 0,
|
|
46
|
+
getRet: retLogs ? mathLog : arithmeticReturn,
|
|
47
|
+
outputKeys,
|
|
48
|
+
crossKey
|
|
59
49
|
};
|
|
60
50
|
|
|
61
|
-
const keyNames = featureKeys.map(getKey);
|
|
62
|
-
|
|
63
51
|
initializeColumns(main, [...keyNames, crossKey].map(key => ({ key })), { lag });
|
|
64
52
|
|
|
65
53
|
if (scaledGroups) {
|
|
@@ -73,6 +61,7 @@ export const heikenAshi = (
|
|
|
73
61
|
const low = verticalOhlcv.low[index];
|
|
74
62
|
const close = verticalOhlcv.close[index];
|
|
75
63
|
const inst = instances[instanceKey];
|
|
64
|
+
const { crossKey, emaPre, emaPost, getRet, outputKeys } = inst;
|
|
76
65
|
|
|
77
66
|
if (
|
|
78
67
|
isBadNumber(open) ||
|
|
@@ -85,19 +74,19 @@ export const heikenAshi = (
|
|
|
85
74
|
|
|
86
75
|
let sOpen, sHigh, sLow, sClose;
|
|
87
76
|
|
|
88
|
-
if (
|
|
77
|
+
if (emaPre !== null) {
|
|
89
78
|
// ---- PRE-SMOOTHING EMA ----
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!
|
|
96
|
-
!
|
|
97
|
-
sOpen =
|
|
98
|
-
sHigh =
|
|
99
|
-
sLow =
|
|
100
|
-
sClose =
|
|
79
|
+
emaPre.open.update(open);
|
|
80
|
+
emaPre.high.update(high);
|
|
81
|
+
emaPre.low.update(low);
|
|
82
|
+
emaPre.close.update(close);
|
|
83
|
+
|
|
84
|
+
if (!emaPre.open.isStable || !emaPre.high.isStable ||
|
|
85
|
+
!emaPre.low.isStable || !emaPre.close.isStable) return;
|
|
86
|
+
sOpen = emaPre.open.getResult();
|
|
87
|
+
sHigh = emaPre.high.getResult();
|
|
88
|
+
sLow = emaPre.low.getResult();
|
|
89
|
+
sClose = emaPre.close.getResult();
|
|
101
90
|
} else {
|
|
102
91
|
sOpen = open;
|
|
103
92
|
sHigh = high;
|
|
@@ -132,19 +121,19 @@ export const heikenAshi = (
|
|
|
132
121
|
|
|
133
122
|
let smOpen, smHigh, smLow, smClose;
|
|
134
123
|
|
|
135
|
-
if (
|
|
124
|
+
if (emaPost !== null) {
|
|
136
125
|
// ---- POST-SMOOTHING EMA ----
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (!
|
|
143
|
-
!
|
|
144
|
-
smOpen =
|
|
145
|
-
smHigh =
|
|
146
|
-
smLow =
|
|
147
|
-
smClose =
|
|
126
|
+
emaPost.open.update(haOpen);
|
|
127
|
+
emaPost.high.update(haHigh);
|
|
128
|
+
emaPost.low.update(haLow);
|
|
129
|
+
emaPost.close.update(haClose);
|
|
130
|
+
|
|
131
|
+
if (!emaPost.open.isStable || !emaPost.high.isStable ||
|
|
132
|
+
!emaPost.low.isStable || !emaPost.close.isStable) return;
|
|
133
|
+
smOpen = emaPost.open.getResult();
|
|
134
|
+
smHigh = emaPost.high.getResult();
|
|
135
|
+
smLow = emaPost.low.getResult();
|
|
136
|
+
smClose = emaPost.close.getResult();
|
|
148
137
|
} else {
|
|
149
138
|
smOpen = haOpen;
|
|
150
139
|
smHigh = haHigh;
|
|
@@ -167,57 +156,49 @@ export const heikenAshi = (
|
|
|
167
156
|
|
|
168
157
|
// ---- TREND/CROSS LOGIC ----
|
|
169
158
|
// Cross uses raw smoothed HA values, not the returned log/return features.
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (inst.isTrendUp) {
|
|
195
|
-
cross = prevCross > 0 ? prevCross + 1 : 1;
|
|
196
|
-
} else {
|
|
197
|
-
cross = prevCross < 0 ? prevCross - 1 : -1;
|
|
198
|
-
}
|
|
159
|
+
const hasPrevious = (
|
|
160
|
+
Number.isFinite(inst.prevSmOpen) &&
|
|
161
|
+
Number.isFinite(inst.prevSmClose)
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const prevCross = index > 0
|
|
165
|
+
? verticalOhlcv[crossKey][index - 1]
|
|
166
|
+
: NaN
|
|
167
|
+
|
|
168
|
+
// A doji preserves the established direction.
|
|
169
|
+
// Before any direction exists, it remains neutral.
|
|
170
|
+
const direction = smClose > smOpen
|
|
171
|
+
? 1
|
|
172
|
+
: smClose < smOpen
|
|
173
|
+
? -1
|
|
174
|
+
: inst.trendDirection
|
|
175
|
+
|
|
176
|
+
let cross = 0
|
|
177
|
+
|
|
178
|
+
if (hasPrevious && direction !== 0) {
|
|
179
|
+
cross = Number.isFinite(prevCross) &&
|
|
180
|
+
Math.sign(prevCross) === direction
|
|
181
|
+
? prevCross + direction
|
|
182
|
+
: direction
|
|
199
183
|
}
|
|
200
184
|
|
|
201
|
-
|
|
202
|
-
inst.prevSmOpen = smOpen
|
|
203
|
-
inst.prevSmClose = smClose
|
|
185
|
+
inst.trendDirection = direction
|
|
186
|
+
inst.prevSmOpen = smOpen
|
|
187
|
+
inst.prevSmClose = smClose
|
|
204
188
|
|
|
205
189
|
// ---- HEIKEN ASHI STRUCTURE RETURNS ----
|
|
206
190
|
const bodyTop = Math.max(smOpen, smClose);
|
|
207
191
|
const bodyBottom = Math.min(smOpen, smClose);
|
|
208
192
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
[getKey('range')]: getRet(smHigh, smLow),
|
|
214
|
-
};
|
|
193
|
+
const body = getRet(smClose, smOpen);
|
|
194
|
+
const upperWick = getRet(smHigh, bodyTop);
|
|
195
|
+
const lowerWick = getRet(bodyBottom, smLow);
|
|
196
|
+
const range = getRet(smHigh, smLow);
|
|
215
197
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
}
|
|
198
|
+
if (!isBadNumber(body)) main.pushToMain({ index, key: outputKeys.body, value: body });
|
|
199
|
+
if (!isBadNumber(upperWick)) main.pushToMain({ index, key: outputKeys.upper_wick, value: upperWick });
|
|
200
|
+
if (!isBadNumber(lowerWick)) main.pushToMain({ index, key: outputKeys.lower_wick, value: lowerWick });
|
|
201
|
+
if (!isBadNumber(range)) main.pushToMain({ index, key: outputKeys.range, value: range });
|
|
221
202
|
|
|
222
203
|
// Do not add return logs to cross.
|
|
223
204
|
main.pushToMain({ index, key: crossKey, value: cross });
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
import {FasterEMA, FasterSMA} from '../core-indicators/index.js';
|
|
3
3
|
import { validateInputValues } from '../utilities/validators.js';
|
|
4
4
|
import { initializeColumns } from '../core-functions/initializeColumns.js';
|
|
5
|
+
import { mathLog } from '../utilities/math.js';
|
|
6
|
+
|
|
7
|
+
const isBadNumber = v => v == null || !Number.isFinite(v);
|
|
8
|
+
|
|
5
9
|
|
|
6
10
|
const indicatorClasses = {
|
|
7
11
|
ema: FasterEMA,
|
|
8
12
|
sma: FasterSMA
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
export const movingAverages = (main, index, methodName, size, { target, lag }) => {
|
|
15
|
+
export const movingAverages = (main, index, methodName, size, { target, lag, retLogs = false }) => {
|
|
12
16
|
|
|
13
17
|
const { verticalOhlcv, instances, priceBased } = main
|
|
14
18
|
const suffix = (target !== 'close') ? `_${target}` : ''
|
|
15
|
-
const keyName = `${methodName}_${size}${suffix}`
|
|
19
|
+
const keyName = `${retLogs ? 'ret_log_' : ''}${methodName}_${size}${suffix}`
|
|
16
20
|
|
|
17
21
|
if (index === 0) {
|
|
18
22
|
validateInputValues({ [target]: true }, verticalOhlcv, index, methodName)
|
|
@@ -26,18 +30,49 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
|
|
|
26
30
|
// Create the main moving average instance.
|
|
27
31
|
instances[keyName] = new indicatorClasses[methodName](size)
|
|
28
32
|
|
|
29
|
-
|
|
33
|
+
if(retLogs) {
|
|
34
|
+
initializeColumns(main, [
|
|
35
|
+
{ key: `${keyName}_distance`, priceBased: false },
|
|
36
|
+
{ key: `${keyName}_change`, priceBased: false }
|
|
37
|
+
], { lag })
|
|
38
|
+
} else {
|
|
39
|
+
initializeColumns(main, [{ key: keyName, priceBased: priceBased.has(target) }], { lag })
|
|
40
|
+
}
|
|
41
|
+
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
// Retrieve the current price value
|
|
33
|
-
const
|
|
34
|
-
|
|
45
|
+
const currVal = verticalOhlcv[target][index];
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
if(isBadNumber(currVal)) return;
|
|
49
|
+
|
|
50
|
+
const instance = instances[keyName];
|
|
51
|
+
|
|
52
|
+
//The easiest way to compute prevMa change is to retrieve the previous result before updating the indicator.
|
|
53
|
+
const prevMa = instance.isStable ? instance.getResult() : NaN;
|
|
35
54
|
|
|
36
55
|
// Update the moving average instance.
|
|
37
|
-
instance.update(
|
|
56
|
+
instance.update(currVal)
|
|
57
|
+
|
|
58
|
+
const currMa = instance.isStable ? instance.getResult() : NaN;
|
|
38
59
|
|
|
39
|
-
|
|
60
|
+
if(isBadNumber(currMa)) return;
|
|
61
|
+
|
|
62
|
+
if (retLogs) {
|
|
63
|
+
|
|
64
|
+
const ratio = currVal / currMa;
|
|
65
|
+
const distance = mathLog(currVal, currMa);
|
|
66
|
+
|
|
67
|
+
main.pushToMain({ index, key: `${keyName}_distance`, value: distance })
|
|
68
|
+
|
|
69
|
+
if(isBadNumber(prevMa)) return;
|
|
70
|
+
|
|
71
|
+
const change = mathLog(currMa, prevMa);
|
|
72
|
+
|
|
73
|
+
main.pushToMain({ index, key: `${keyName}_change`, value: change })
|
|
74
|
+
} else {
|
|
75
|
+
main.pushToMain({ index, key: keyName, value: currMa })
|
|
76
|
+
}
|
|
40
77
|
|
|
41
|
-
// Always push the MA value (even if NaN).
|
|
42
|
-
main.pushToMain({ index, key: keyName, value: currMa })
|
|
43
78
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { FasterSMA } from '../core-indicators/index.js';
|
|
2
|
-
import {
|
|
2
|
+
import { mathLog } from '../utilities/math.js';
|
|
3
3
|
import { validateInputValues } from '../utilities/validators.js';
|
|
4
4
|
import { initializeColumns } from '../core-functions/initializeColumns.js';
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
export const relativeVolume = (main, index, size, {lag}) => {
|
|
7
|
+
export const relativeVolume = (main, index, size, {lag, retLogs = false}) => {
|
|
8
8
|
|
|
9
|
-
const key =
|
|
9
|
+
const key = `${retLogs ? 'ret_log_' : ''}relative_volume_${size}`;
|
|
10
10
|
const { instances, verticalOhlcv } = main;
|
|
11
11
|
|
|
12
12
|
|
|
@@ -42,9 +42,13 @@ export const relativeVolume = (main, index, size, {lag}) => {
|
|
|
42
42
|
return
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
|
|
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;
|
|
46
51
|
|
|
47
52
|
main.pushToMain({ index, key, value: currRelativeVolume });
|
|
48
|
-
instances[key].prevRelativeVolumeSma = smaValue;
|
|
49
53
|
|
|
50
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
|
+
};
|
|
@@ -7,7 +7,7 @@ import { initializeColumns } from '../core-functions/initializeColumns.js';
|
|
|
7
7
|
export const volumeOscillator = (main, index, fast, slow, {lag, retLogs}) => {
|
|
8
8
|
|
|
9
9
|
const {verticalOhlcv, instances} = main
|
|
10
|
-
const key = `volume_oscillator_${fast}_${slow}`
|
|
10
|
+
const key = retLogs ? `ret_log_volume_oscillator_${fast}_${slow}` : `volume_oscillator_${fast}_${slow}`
|
|
11
11
|
|
|
12
12
|
if (index === 0) {
|
|
13
13
|
validateInputValues({ volume: true }, verticalOhlcv, index, 'volumeOscillator');
|
|
@@ -19,10 +19,7 @@ export const volumeOscillator = (main, index, fast, slow, {lag, retLogs}) => {
|
|
|
19
19
|
}
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
initializeColumns(main, [
|
|
23
|
-
{ key },
|
|
24
|
-
{ key: `ret_log_${key}`, enabled: retLogs }
|
|
25
|
-
], { lag });
|
|
22
|
+
initializeColumns(main, [{ key }], { lag });
|
|
26
23
|
}
|
|
27
24
|
|
|
28
25
|
const { fastEMA, slowEMA } = instances[key];
|
|
@@ -46,12 +43,9 @@ export const volumeOscillator = (main, index, fast, slow, {lag, retLogs}) => {
|
|
|
46
43
|
return;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
volumeOscValue =
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
volumeOscRetLog = mathLog(fastValue, slowValue)
|
|
53
|
-
main.pushToMain({index, key: `ret_log_${key}`, value: volumeOscRetLog})
|
|
54
|
-
}
|
|
46
|
+
volumeOscValue = retLogs
|
|
47
|
+
? mathLog(fastValue, slowValue)
|
|
48
|
+
: 100 * (fastValue - slowValue) / slowValue
|
|
55
49
|
|
|
56
50
|
main.pushToMain({index, key, value: volumeOscValue})
|
|
57
51
|
};
|
|
@@ -56,14 +56,34 @@ export const candleFeatures = (main, index, {lag, colKeys, retLogs}) => {
|
|
|
56
56
|
const upperWickTop = Math.max(currOpen, currClose)
|
|
57
57
|
const lowerWickTop = Math.min(currOpen, currClose)
|
|
58
58
|
|
|
59
|
+
const change = getRet(currClose, prevClose)
|
|
60
|
+
const midPriceChange = getRet(currMidPrice, prevMidPrice)
|
|
61
|
+
const upperWick = getRet(currHigh, upperWickTop)
|
|
62
|
+
const lowerWick = getRet(lowerWickTop, currLow)
|
|
63
|
+
const gap = getRet(currOpen, prevClose)
|
|
64
|
+
const body = getRet(currClose, currOpen)
|
|
65
|
+
const range = getRet(currHigh, currLow)
|
|
66
|
+
|
|
67
|
+
if (colKeys.length === 0) {
|
|
68
|
+
main.pushToMain({ index, key: `${prefix}change`, value: change })
|
|
69
|
+
main.pushToMain({ index, key: `${prefix}mid_price_change`, value: midPriceChange })
|
|
70
|
+
main.pushToMain({ index, key: `${prefix}upper_wick`, value: upperWick })
|
|
71
|
+
main.pushToMain({ index, key: `${prefix}lower_wick`, value: lowerWick })
|
|
72
|
+
main.pushToMain({ index, key: `${prefix}gap`, value: gap })
|
|
73
|
+
main.pushToMain({ index, key: `${prefix}body`, value: body })
|
|
74
|
+
main.pushToMain({ index, key: `${prefix}range`, value: range })
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Keep optional-target overwrite/order semantics and all-or-nothing validation.
|
|
59
79
|
const row = {
|
|
60
|
-
[`${prefix}change`]:
|
|
61
|
-
[`${prefix}mid_price_change`]:
|
|
62
|
-
[`${prefix}upper_wick`]:
|
|
63
|
-
[`${prefix}lower_wick`]:
|
|
64
|
-
[`${prefix}gap`]:
|
|
65
|
-
[`${prefix}body`]:
|
|
66
|
-
[`${prefix}range`]:
|
|
80
|
+
[`${prefix}change`]: change,
|
|
81
|
+
[`${prefix}mid_price_change`]: midPriceChange,
|
|
82
|
+
[`${prefix}upper_wick`]: upperWick,
|
|
83
|
+
[`${prefix}lower_wick`]: lowerWick,
|
|
84
|
+
[`${prefix}gap`]: gap,
|
|
85
|
+
[`${prefix}body`]: body,
|
|
86
|
+
[`${prefix}range`]: range
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
let hasInvalidTarget = false
|
|
@@ -86,4 +106,4 @@ export const candleFeatures = (main, index, {lag, colKeys, retLogs}) => {
|
|
|
86
106
|
main.pushToMain({index, key, value})
|
|
87
107
|
}
|
|
88
108
|
|
|
89
|
-
}
|
|
109
|
+
}
|
package/src/studies/dateTime.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {oneHotEncode} from '../machine-learning/ml-utilities.js'
|
|
2
2
|
import { initializeColumns } from '../core-functions/initializeColumns.js'
|
|
3
3
|
|
|
4
|
+
const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
|
|
5
|
+
|
|
4
6
|
export const dateTime = (main, index, {lag, oneHot, precompute}) => {
|
|
5
7
|
|
|
6
8
|
|
|
@@ -40,43 +42,44 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
|
|
|
40
42
|
|
|
41
43
|
const currDate = verticalOhlcv.date[index]
|
|
42
44
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
let yearPart, monthPart, hourPart, minutePart, weekdayPart, dayPart
|
|
46
|
+
for (const part of formatter.formatToParts(currDate)) {
|
|
47
|
+
switch (part.type) {
|
|
48
|
+
case 'year': yearPart = part.value; break
|
|
49
|
+
case 'month': monthPart = part.value; break
|
|
50
|
+
case 'hour': hourPart = part.value; break
|
|
51
|
+
case 'minute': minutePart = part.value; break
|
|
52
|
+
case 'weekday': weekdayPart = part.value; break
|
|
53
|
+
case 'day': dayPart = part.value; break
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const year = Number(yearPart)
|
|
57
|
+
const month = Number(monthPart) - 1
|
|
58
|
+
const hour = Number(hourPart)
|
|
59
|
+
const minute = Number(minutePart)
|
|
60
|
+
const dayOfWeek = weekdays.indexOf(weekdayPart)
|
|
61
|
+
const dayOfMonth = Number(dayPart) - 1
|
|
62
|
+
|
|
63
|
+
if (!oneHot) {
|
|
64
|
+
main.pushToMain({ index, key: 'year', value: year })
|
|
65
|
+
main.pushToMain({ index, key: 'month', value: month + 1 })
|
|
66
|
+
main.pushToMain({ index, key: 'hour', value: hour })
|
|
67
|
+
main.pushToMain({ index, key: 'minute', value: minute })
|
|
68
|
+
main.pushToMain({ index, key: 'day_of_the_week', value: ((dayOfWeek + 6) % 7) + 1 })
|
|
69
|
+
main.pushToMain({ index, key: 'day_of_the_month', value: dayOfMonth + 1 })
|
|
70
|
+
return
|
|
48
71
|
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const getDateInfo = (date, oneHot, colKeySizes, formatter) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const parts = Object.fromEntries(formatter.formatToParts(date).map(({ type, value }) => [type, value]));
|
|
57
|
-
const year = Number(parts.year);
|
|
58
|
-
const month = Number(parts.month) - 1;
|
|
59
|
-
const hour = Number(parts.hour);
|
|
60
|
-
const minute = Number(parts.minute);
|
|
61
|
-
const dayOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].indexOf(parts.weekday);
|
|
62
|
-
const dayOfMonth = Number(parts.day) - 1;
|
|
63
|
-
|
|
64
|
-
if (!oneHot) {
|
|
65
|
-
return {
|
|
66
|
-
year: year,
|
|
67
|
-
month: month + 1, //iso month
|
|
68
|
-
hour: hour,
|
|
69
|
-
minute: minute,
|
|
70
|
-
day_of_the_week: ((dayOfWeek + 6) % 7) + 1, //iso date of the week
|
|
71
|
-
day_of_the_month: dayOfMonth + 1, //iso date of the month
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
72
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
73
|
+
// Build a fresh vector for every field and row before writing.
|
|
74
|
+
const oneHotMonth = oneHotEncode(month, colKeySizes.one_hot_month)
|
|
75
|
+
const oneHotHour = oneHotEncode(hour, colKeySizes.one_hot_hour)
|
|
76
|
+
const oneHotMinute = oneHotEncode(minute, colKeySizes.one_hot_minute)
|
|
77
|
+
const oneHotWeekday = oneHotEncode(dayOfWeek, colKeySizes.one_hot_day_of_the_week)
|
|
78
|
+
const oneHotDay = oneHotEncode(dayOfMonth, colKeySizes.one_hot_day_of_the_month)
|
|
79
|
+
|
|
80
|
+
main.pushToMain({ index, key: 'one_hot_month', value: oneHotMonth })
|
|
81
|
+
main.pushToMain({ index, key: 'one_hot_hour', value: oneHotHour })
|
|
82
|
+
main.pushToMain({ index, key: 'one_hot_minute', value: oneHotMinute })
|
|
83
|
+
main.pushToMain({ index, key: 'one_hot_day_of_the_week', value: oneHotWeekday })
|
|
84
|
+
main.pushToMain({ index, key: 'one_hot_day_of_the_month', value: oneHotDay })
|
|
85
|
+
}
|
|
@@ -24,11 +24,7 @@ class CrossInstance {
|
|
|
24
24
|
prevSlow: NaN,
|
|
25
25
|
prevHigh: NaN,
|
|
26
26
|
prevLow: NaN,
|
|
27
|
-
areHighAndLowUndefined: false
|
|
28
|
-
crossIndexes: {
|
|
29
|
-
up: [],
|
|
30
|
-
down: []
|
|
31
|
-
}
|
|
27
|
+
areHighAndLowUndefined: false
|
|
32
28
|
})
|
|
33
29
|
|
|
34
30
|
}
|
|
@@ -115,9 +111,6 @@ class CrossInstance {
|
|
|
115
111
|
}
|
|
116
112
|
|
|
117
113
|
|
|
118
|
-
if(this.interval === 1) this.crossIndexes.up.push(index)
|
|
119
|
-
if(this.interval === -1) this.crossIndexes.down.push(index)
|
|
120
|
-
|
|
121
114
|
//save prev state
|
|
122
115
|
Object.assign(this, {
|
|
123
116
|
prevState: currState,
|
|
@@ -238,4 +231,4 @@ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
|
|
|
238
231
|
|
|
239
232
|
}
|
|
240
233
|
|
|
241
|
-
}
|
|
234
|
+
}
|