ohlcv-indicators 3.3.1 → 3.3.4
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/dist/ohlcv-indicators.min.js +1634 -1719
- package/index.js +163 -268
- package/package.json +1 -3
- package/src/core-functions/mainLoop.js +100 -0
- package/src/core-functions/pushToMain.js +15 -0
- package/src/moving-averages/bollingerBands.js +22 -30
- package/src/moving-averages/donchianChannel.js +26 -34
- package/src/moving-averages/macd.js +27 -15
- package/src/moving-averages/movingAverages.js +16 -134
- package/src/moving-averages/relativeVolume.js +20 -14
- package/src/oscillators/rsi.js +21 -25
- package/src/oscillators/volumeOscillator.js +20 -19
- package/src/studies/dateTime.js +22 -20
- package/src/studies/findCrosses.js +17 -17
- package/src/studies/lag.js +21 -12
- package/src/studies/scaler.js +112 -0
- package/src/utilities/assignTypes.js +39 -0
- package/src/utilities/dateUtilities.js +79 -0
- package/src/utilities/numberUtilities.js +50 -9
- package/src/utilities/precisionMultiplier.js +24 -0
- package/src/utilities/validators.js +76 -5
- package/src/utilities/{dataParsingUtilities.js → verticalToHorizontal.js} +8 -8
- package/src/studies/candleVectors.js +0 -121
- package/src/studies/priceVariations.js +0 -23
- package/src/utilities/classification.js +0 -75
- package/src/utilities/parsing-utilities.js +0 -189
- package/src/utilities/pushToMain.js +0 -63
package/src/oscillators/rsi.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { FasterRSI } from 'trading-signals';
|
|
2
2
|
import { FasterSMA } from 'trading-signals';
|
|
3
|
-
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
4
3
|
|
|
5
|
-
const defaultTarget = 'close'
|
|
4
|
+
const defaultTarget = 'close'
|
|
5
|
+
export const rsi = (main, index, size, { target, lag }) => {
|
|
6
|
+
const { verticalOhlcv, instances } = main;
|
|
6
7
|
|
|
7
|
-
export const rsi = (main, index, size, { scale, target, lag, autoMinMax }) => {
|
|
8
|
-
const { verticalOhlcv, instances, lastIndexReplace } = main;
|
|
9
8
|
const suffix = target === defaultTarget ? '' : `_${target}`;
|
|
10
9
|
const rsiKey = `rsi_${size}${suffix}`;
|
|
11
10
|
const rsiSmaKey = `rsi_sma_${size}${suffix}`;
|
|
@@ -13,7 +12,7 @@ export const rsi = (main, index, size, { scale, target, lag, autoMinMax }) => {
|
|
|
13
12
|
// Initialization on the first index.
|
|
14
13
|
if (index === 0) {
|
|
15
14
|
|
|
16
|
-
const { crossPairsList,
|
|
15
|
+
const { crossPairsList, len, arrayTypes } = main;
|
|
17
16
|
|
|
18
17
|
if (!verticalOhlcv.hasOwnProperty(target)) {
|
|
19
18
|
throw new Error(`Target property ${target} not found in verticalOhlcv for rsi.`);
|
|
@@ -27,55 +26,52 @@ export const rsi = (main, index, size, { scale, target, lag, autoMinMax }) => {
|
|
|
27
26
|
});
|
|
28
27
|
|
|
29
28
|
Object.assign(verticalOhlcv, {
|
|
30
|
-
[rsiKey]:
|
|
31
|
-
[rsiSmaKey]:
|
|
29
|
+
[rsiKey]: new Float64Array(len).fill(NaN),
|
|
30
|
+
[rsiSmaKey]: new Float64Array(len).fill(NaN),
|
|
32
31
|
});
|
|
33
32
|
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
const keyNames = [rsiKey, rsiSmaKey]
|
|
36
|
+
|
|
34
37
|
if(lag > 0)
|
|
35
38
|
{
|
|
36
|
-
main.lag(
|
|
39
|
+
main.lag(keyNames, lag)
|
|
37
40
|
}
|
|
38
41
|
|
|
39
|
-
|
|
42
|
+
for(const key of keyNames)
|
|
40
43
|
{
|
|
41
|
-
|
|
44
|
+
arrayTypes[key] = 'Float64Array'
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
const value = verticalOhlcv[target][index];
|
|
47
|
-
let currentRsi =
|
|
48
|
-
let smoothedRsi =
|
|
50
|
+
let currentRsi = NaN;
|
|
51
|
+
let smoothedRsi = NaN;
|
|
49
52
|
|
|
50
53
|
// Update the RSI indicator.
|
|
51
|
-
instances[rsiKey].update(value
|
|
54
|
+
instances[rsiKey].update(value);
|
|
52
55
|
|
|
53
56
|
try {
|
|
54
57
|
currentRsi = instances[rsiKey].getResult();
|
|
55
58
|
} catch (err) {
|
|
56
|
-
currentRsi =
|
|
59
|
+
currentRsi = NaN;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
|
-
if (currentRsi !== null && typeof currentRsi === 'number' && scale) {
|
|
60
|
-
currentRsi = calcMagnitude(currentRsi, scale);
|
|
61
|
-
}
|
|
62
62
|
|
|
63
|
-
// Always push the RSI value, using
|
|
63
|
+
// Always push the RSI value, using NaN as a fallback.
|
|
64
64
|
main.pushToMain({ index, key: rsiKey, value: currentRsi });
|
|
65
65
|
|
|
66
66
|
// Update the SMA indicator only if a valid RSI value is available.
|
|
67
|
-
if (currentRsi
|
|
68
|
-
instances[rsiSmaKey].update(currentRsi
|
|
67
|
+
if (!Number.isNaN(currentRsi)) {
|
|
68
|
+
instances[rsiSmaKey].update(currentRsi);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
try {
|
|
72
72
|
smoothedRsi = instances[rsiSmaKey].getResult();
|
|
73
73
|
} catch (err) {
|
|
74
|
-
smoothedRsi =
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (smoothedRsi !== null && typeof smoothedRsi === 'number' && scale) {
|
|
78
|
-
smoothedRsi = calcMagnitude(smoothedRsi, scale);
|
|
74
|
+
smoothedRsi = NaN;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
77
|
// Always push the smoothed RSI value.
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { FasterEMA } from 'trading-signals';
|
|
2
|
-
import { calcMagnitude } from '../utilities/numberUtilities.js';
|
|
3
2
|
|
|
4
|
-
export const volumeOscillator = (main, index, fast, slow, {
|
|
3
|
+
export const volumeOscillator = (main, index, fast, slow, {lag}) => {
|
|
5
4
|
|
|
6
|
-
const {verticalOhlcv, instances
|
|
5
|
+
const {verticalOhlcv, instances} = main
|
|
7
6
|
const value = verticalOhlcv.volume[index]
|
|
8
7
|
const key = 'volume_oscillator'
|
|
9
8
|
|
|
10
9
|
if (index === 0) {
|
|
11
10
|
|
|
12
|
-
const {crossPairsList,
|
|
11
|
+
const {crossPairsList, len, arrayTypes} = main
|
|
13
12
|
Object.assign(instances, {
|
|
14
13
|
volume_oscillator: {
|
|
15
14
|
fastEMA: new FasterEMA(fast),
|
|
@@ -17,40 +16,42 @@ export const volumeOscillator = (main, index, fast, slow, {scale}) => {
|
|
|
17
16
|
}
|
|
18
17
|
})
|
|
19
18
|
|
|
20
|
-
verticalOhlcv[key] =
|
|
21
|
-
crossPairsList.push({ fast: key, slow: 0, isDefault: true })
|
|
19
|
+
verticalOhlcv[key] = new Float64Array(len).fill(NaN)
|
|
20
|
+
crossPairsList.push({ fast: key, slow: 0, isDefault: true })
|
|
21
|
+
|
|
22
|
+
if(lag > 0)
|
|
23
|
+
{
|
|
24
|
+
main.lag([key], lag)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
arrayTypes[key] = 'Float64Array'
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
const { fastEMA, slowEMA } = instances[key];
|
|
25
31
|
|
|
26
|
-
fastEMA.update(value
|
|
27
|
-
slowEMA.update(value
|
|
32
|
+
fastEMA.update(value);
|
|
33
|
+
slowEMA.update(value);
|
|
28
34
|
|
|
29
|
-
let fastValue =
|
|
30
|
-
let slowValue =
|
|
35
|
+
let fastValue = NaN;
|
|
36
|
+
let slowValue = NaN;
|
|
31
37
|
|
|
32
38
|
try {
|
|
33
39
|
fastValue = fastEMA.getResult();
|
|
34
40
|
} catch (err) {
|
|
35
|
-
|
|
41
|
+
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
try {
|
|
39
45
|
slowValue = slowEMA.getResult();
|
|
40
46
|
} catch (err) {
|
|
41
|
-
|
|
47
|
+
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
let volumeOscValue =
|
|
50
|
+
let volumeOscValue = NaN
|
|
45
51
|
|
|
46
|
-
if(
|
|
52
|
+
if(!Number.isNaN(fastValue) && !Number.isNaN(slowValue))
|
|
47
53
|
{
|
|
48
54
|
volumeOscValue = 100 * (fastValue - slowValue) / slowValue
|
|
49
|
-
|
|
50
|
-
if(scale)
|
|
51
|
-
{
|
|
52
|
-
volumeOscValue = calcMagnitude(volumeOscValue, scale)
|
|
53
|
-
}
|
|
54
55
|
}
|
|
55
56
|
|
|
56
57
|
main.pushToMain({index, key, value: volumeOscValue})
|
package/src/studies/dateTime.js
CHANGED
|
@@ -1,36 +1,39 @@
|
|
|
1
|
-
export const dateTime = (main, index) => {
|
|
1
|
+
export const dateTime = (main, index, {lag}) => {
|
|
2
2
|
|
|
3
3
|
const {instances, verticalOhlcv} = main
|
|
4
4
|
|
|
5
5
|
if(index === 0)
|
|
6
6
|
{
|
|
7
|
-
const {
|
|
8
|
-
if(!
|
|
9
|
-
|
|
10
|
-
const index0DateStr = verticalOhlcv.date[0].slice(0, 10)
|
|
7
|
+
const {len, dateType, arrayTypes} = main
|
|
8
|
+
if(!dateType) throw Error('dateTime method found and invalid "date" in input ohlcv')
|
|
11
9
|
|
|
12
10
|
Object.assign(instances, {
|
|
13
11
|
dateTime: {
|
|
14
|
-
prevDateStr:
|
|
12
|
+
prevDateStr: verticalOhlcv.date[0],
|
|
15
13
|
sessionDailyIndexCount: 0,
|
|
16
14
|
sessionIntradayIndexCount: 0,
|
|
17
|
-
cachedDayInfo:
|
|
15
|
+
cachedDayInfo: verticalOhlcv.date[0]
|
|
18
16
|
}
|
|
19
17
|
})
|
|
20
18
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
19
|
+
const colKeys = ['day_of_the_week', 'day_of_the_month', 'week_of_the_month', 'hour', 'month', 'year', 'session_daily_index', 'session_intraday_index']
|
|
20
|
+
|
|
21
|
+
for(const key of colKeys)
|
|
22
|
+
{
|
|
23
|
+
arrayTypes[key] = 'Int32Array'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Object.assign(verticalOhlcv, {...Object.fromEntries(colKeys.map(k => [k, new Int32Array(len).fill(NaN)]))})
|
|
27
|
+
|
|
28
|
+
if(lag > 0)
|
|
29
|
+
{
|
|
30
|
+
main.lag(colKeys, lag)
|
|
31
|
+
}
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
|
|
33
35
|
const currDate = verticalOhlcv.date[index]
|
|
36
|
+
|
|
34
37
|
const {
|
|
35
38
|
day_of_the_week,
|
|
36
39
|
day_of_the_month,
|
|
@@ -41,7 +44,7 @@ export const dateTime = (main, index) => {
|
|
|
41
44
|
} = getDateInfo(currDate)
|
|
42
45
|
|
|
43
46
|
|
|
44
|
-
const currDateStr = currDate
|
|
47
|
+
const currDateStr = currDate
|
|
45
48
|
|
|
46
49
|
if(currDateStr !== instances.dateTime.prevDateStr)
|
|
47
50
|
{
|
|
@@ -68,8 +71,7 @@ export const dateTime = (main, index) => {
|
|
|
68
71
|
|
|
69
72
|
|
|
70
73
|
|
|
71
|
-
const getDateInfo =
|
|
72
|
-
const date = new Date(dateString)
|
|
74
|
+
const getDateInfo = date => {
|
|
73
75
|
const month = date.getMonth()
|
|
74
76
|
const year = date.getFullYear()
|
|
75
77
|
const hour = date.getHours()
|
|
@@ -15,12 +15,11 @@ class CrossInstance {
|
|
|
15
15
|
Object.assign(this, {
|
|
16
16
|
interval: 0,
|
|
17
17
|
prevState: 0.5,
|
|
18
|
-
prevFast:
|
|
19
|
-
prevSlow:
|
|
20
|
-
prevHigh:
|
|
21
|
-
prevLow:
|
|
18
|
+
prevFast: NaN,
|
|
19
|
+
prevSlow: NaN,
|
|
20
|
+
prevHigh: NaN,
|
|
21
|
+
prevLow: NaN,
|
|
22
22
|
areHighAndLowUndefined: false,
|
|
23
|
-
index: 0,
|
|
24
23
|
crossIndexes: {
|
|
25
24
|
up: [],
|
|
26
25
|
down: []
|
|
@@ -28,7 +27,7 @@ class CrossInstance {
|
|
|
28
27
|
})
|
|
29
28
|
|
|
30
29
|
}
|
|
31
|
-
update({fast, high, slow, low})
|
|
30
|
+
update(index, {fast, high, slow, low})
|
|
32
31
|
{
|
|
33
32
|
let currState = this.prevState
|
|
34
33
|
|
|
@@ -40,7 +39,7 @@ class CrossInstance {
|
|
|
40
39
|
}
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
if(fast
|
|
42
|
+
if(Number.isNaN(fast) || Number.isNaN(slow)){
|
|
44
43
|
currState = 0.5
|
|
45
44
|
}
|
|
46
45
|
else if(eq(fast, slow)) {
|
|
@@ -54,7 +53,7 @@ class CrossInstance {
|
|
|
54
53
|
}
|
|
55
54
|
else
|
|
56
55
|
{
|
|
57
|
-
if ([low, slow, this.prevLow, this.prevSlow].every(v => v
|
|
56
|
+
if ([low, slow, this.prevLow, this.prevSlow].every(v => !Number.isNaN(v)) && lt(low, slow) && gt(this.prevLow, this.prevSlow))
|
|
58
57
|
{
|
|
59
58
|
currState = 0
|
|
60
59
|
}
|
|
@@ -72,7 +71,7 @@ class CrossInstance {
|
|
|
72
71
|
}
|
|
73
72
|
else
|
|
74
73
|
{
|
|
75
|
-
if([high, slow, this.prevHigh, this.prevSlow].every(v => v
|
|
74
|
+
if([high, slow, this.prevHigh, this.prevSlow].every(v => !Number.isNaN(v)) && gt(high, slow) && lt(this.prevHigh, this.prevSlow))
|
|
76
75
|
{
|
|
77
76
|
currState = 1
|
|
78
77
|
}
|
|
@@ -111,12 +110,11 @@ class CrossInstance {
|
|
|
111
110
|
}
|
|
112
111
|
|
|
113
112
|
|
|
114
|
-
if(this.interval === 1) this.crossIndexes.up.push(
|
|
115
|
-
if(this.interval === -1) this.crossIndexes.down.push(
|
|
113
|
+
if(this.interval === 1) this.crossIndexes.up.push(index)
|
|
114
|
+
if(this.interval === -1) this.crossIndexes.down.push(index)
|
|
116
115
|
|
|
117
116
|
//save prev state
|
|
118
117
|
Object.assign(this, {
|
|
119
|
-
index: this.index + 1,
|
|
120
118
|
prevState: currState,
|
|
121
119
|
prevFast: fast,
|
|
122
120
|
prevSlow: slow,
|
|
@@ -134,7 +132,7 @@ class CrossInstance {
|
|
|
134
132
|
|
|
135
133
|
export const crossPairs = (main, index) => {
|
|
136
134
|
|
|
137
|
-
const {
|
|
135
|
+
const {verticalOhlcv, instances, len, arrayTypes} = main
|
|
138
136
|
|
|
139
137
|
if(index === 0)
|
|
140
138
|
{
|
|
@@ -158,7 +156,7 @@ export const crossPairs = (main, index) => {
|
|
|
158
156
|
{
|
|
159
157
|
if(typeof slow === 'number')
|
|
160
158
|
{
|
|
161
|
-
verticalOhlcv[slow] = new
|
|
159
|
+
verticalOhlcv[slow] = new Int32Array(len).fill(slow)
|
|
162
160
|
}
|
|
163
161
|
|
|
164
162
|
if(fast !== 'price' && !verticalOhlcv.hasOwnProperty(fast)) throw Error(`fast "${fast} not found in crossPairs"`)
|
|
@@ -167,7 +165,9 @@ export const crossPairs = (main, index) => {
|
|
|
167
165
|
|
|
168
166
|
instances[crossName] = new CrossInstance()
|
|
169
167
|
|
|
170
|
-
verticalOhlcv[crossName] =
|
|
168
|
+
verticalOhlcv[crossName] = new Int32Array(len).fill(NaN)
|
|
169
|
+
|
|
170
|
+
arrayTypes[crossName] = 'Int32Array'
|
|
171
171
|
}
|
|
172
172
|
|
|
173
173
|
let fastValue
|
|
@@ -183,14 +183,14 @@ export const crossPairs = (main, index) => {
|
|
|
183
183
|
lowValue = verticalOhlcv.low[index]
|
|
184
184
|
slowValue = verticalOhlcv[slow][index]
|
|
185
185
|
|
|
186
|
-
instances[crossName].update({fast: closeValue, slow: slowValue, high: highValue, low: lowValue})
|
|
186
|
+
instances[crossName].update(index, {fast: closeValue, slow: slowValue, high: highValue, low: lowValue})
|
|
187
187
|
|
|
188
188
|
} else
|
|
189
189
|
{
|
|
190
190
|
fastValue = verticalOhlcv[fast][index]
|
|
191
191
|
slowValue = verticalOhlcv[slow][index]
|
|
192
192
|
|
|
193
|
-
instances[crossName].update({fast: fastValue, slow: slowValue})
|
|
193
|
+
instances[crossName].update(index, {fast: fastValue, slow: slowValue})
|
|
194
194
|
}
|
|
195
195
|
|
|
196
196
|
main.pushToMain({index, key: crossName, value: instances[crossName].getResult()})
|
package/src/studies/lag.js
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
+
import { buildArray } from "../utilities/assignTypes.js";
|
|
2
|
+
|
|
1
3
|
export const lag = (main, index) => {
|
|
2
4
|
|
|
3
|
-
const {instances, verticalOhlcv,
|
|
5
|
+
const {instances, verticalOhlcv, len, arrayTypes, priceBased} = main
|
|
4
6
|
|
|
5
7
|
if(index === 0)
|
|
6
8
|
{
|
|
7
|
-
const {inputParams
|
|
9
|
+
const {inputParams} = main
|
|
8
10
|
const findParams = inputParams.filter(o => o.key === 'lag')
|
|
9
11
|
|
|
10
12
|
if(typeof findParams !== 'object') return
|
|
11
13
|
|
|
12
14
|
const params = findParams.map(o => o.params)
|
|
13
15
|
|
|
14
|
-
for (const [colKeys,
|
|
16
|
+
for (const [colKeys, lookback] of params)
|
|
15
17
|
{
|
|
16
18
|
for (const colKey of colKeys)
|
|
17
19
|
{
|
|
18
20
|
if(priceBased.includes(colKey))
|
|
19
21
|
{
|
|
20
|
-
for (let
|
|
21
|
-
priceBased.push(`${colKey}_lag_${
|
|
22
|
+
for (let step = 1; step <= lookback; step++) {
|
|
23
|
+
priceBased.push(`${colKey}_lag_${step}`)
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -31,23 +33,30 @@ export const lag = (main, index) => {
|
|
|
31
33
|
|
|
32
34
|
const {lagParams} = instances.lag
|
|
33
35
|
|
|
34
|
-
for (const [colKeys,
|
|
36
|
+
for (const [colKeys, lookback] of lagParams) {
|
|
35
37
|
|
|
36
38
|
for (const colKey of colKeys) {
|
|
37
39
|
const currentColumn = verticalOhlcv[colKey];
|
|
38
40
|
|
|
39
41
|
// Initialize lagged arrays only on the first index
|
|
40
42
|
if (index === 0) {
|
|
41
|
-
for (let
|
|
42
|
-
const key = `${colKey}_lag_${
|
|
43
|
-
|
|
43
|
+
for (let step = 1; step <= lookback; step++) {
|
|
44
|
+
const key = `${colKey}_lag_${step}`;
|
|
45
|
+
|
|
46
|
+
if(!arrayTypes.hasOwnProperty(colKey))
|
|
47
|
+
{
|
|
48
|
+
throw new Error(`Lag processing invoked by "${colKey}" expected arrayTypes to have a "${colKey}" property, but it wasn’t found.`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
verticalOhlcv[key] = buildArray(arrayTypes[colKey], len)
|
|
52
|
+
arrayTypes[key] = arrayTypes[colKey]
|
|
44
53
|
}
|
|
45
54
|
}
|
|
46
55
|
|
|
47
56
|
// Populate lagged values
|
|
48
|
-
for (let
|
|
49
|
-
const key = `${colKey}_lag_${
|
|
50
|
-
const laggedIndex = index -
|
|
57
|
+
for (let step = 1; step <= lookback; step++) {
|
|
58
|
+
const key = `${colKey}_lag_${step}`;
|
|
59
|
+
const laggedIndex = index - step;
|
|
51
60
|
|
|
52
61
|
const value = (laggedIndex <= 0 || typeof currentColumn[laggedIndex] === 'undefined')
|
|
53
62
|
? null
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* General Scaler: supports "minmax" and "zscore" types */
|
|
2
|
+
|
|
3
|
+
const normalizeMinMax = (value, min, max, [validMin, validMax]) => {
|
|
4
|
+
const clamped = Math.min(Math.max(value, min), max);
|
|
5
|
+
return ((clamped - min) / (max - min)) * (validMax - validMin) + validMin;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const normalizeZScore = (value, mean, std) => {
|
|
9
|
+
return std === 0 ? 0 : (value - mean) / std;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const Scaler = (
|
|
13
|
+
main,
|
|
14
|
+
index,
|
|
15
|
+
size,
|
|
16
|
+
colKeys,
|
|
17
|
+
type,
|
|
18
|
+
group,
|
|
19
|
+
range, // used for minmax: [validMin, validMax]
|
|
20
|
+
lag,
|
|
21
|
+
) => {
|
|
22
|
+
const { verticalOhlcv, instances, arrayTypes } = main;
|
|
23
|
+
const prefix = `${type}_${size}`;
|
|
24
|
+
let groupKey = '';
|
|
25
|
+
|
|
26
|
+
if (index === 0) {
|
|
27
|
+
const { len, priceBased, groups } = main;
|
|
28
|
+
groupKey = `${prefix}_group_${colKeys.join('_')}`;
|
|
29
|
+
|
|
30
|
+
instances.scaler = {
|
|
31
|
+
groupKeyLen: colKeys.length,
|
|
32
|
+
groupKey,
|
|
33
|
+
windows: {} // stores arrays of past values for each key or group
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
for (const target of colKeys) {
|
|
37
|
+
if (!verticalOhlcv.hasOwnProperty(target)) {
|
|
38
|
+
throw new Error(`Target property "${target}" not found in verticalOhlcv`);
|
|
39
|
+
}
|
|
40
|
+
if (!priceBased.includes(target)) {
|
|
41
|
+
throw new Error(`Column "${target}" is not priceBased and cannot be scaled.`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const key = `${prefix}_${target}`;
|
|
45
|
+
verticalOhlcv[key] = new Float64Array(len).fill(NaN);
|
|
46
|
+
arrayTypes[key] = 'Float64Array'
|
|
47
|
+
|
|
48
|
+
const winKey = group ? groupKey : target;
|
|
49
|
+
instances.scaler.windows[winKey] = [];
|
|
50
|
+
|
|
51
|
+
if (group) {
|
|
52
|
+
if (!main.ScaledGroups[groupKey]) main.ScaledGroups[groupKey] = [];
|
|
53
|
+
main.ScaledGroups[groupKey].push(key);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (lag > 0) {
|
|
57
|
+
if (group) {
|
|
58
|
+
const lags = Array.from({ length: lag }).map((_, i) => `${key}_lag_${i + 1}`);
|
|
59
|
+
main.ScaledGroups[groupKey].push(...lags);
|
|
60
|
+
}
|
|
61
|
+
main.lag([key], lag);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { windows } = instances.scaler;
|
|
67
|
+
groupKey = instances.scaler.groupKey;
|
|
68
|
+
|
|
69
|
+
// update windows with current values
|
|
70
|
+
for (const target of colKeys) {
|
|
71
|
+
const val = verticalOhlcv[target][index];
|
|
72
|
+
const winKey = group ? groupKey : target;
|
|
73
|
+
const win = windows[winKey];
|
|
74
|
+
|
|
75
|
+
win.push(val);
|
|
76
|
+
|
|
77
|
+
if (win.length > (group ? size * instances.scaler.groupKeyLen : size)) {
|
|
78
|
+
win.shift();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const ready = index + 1 >= size;
|
|
84
|
+
|
|
85
|
+
// scale values once enough data
|
|
86
|
+
for (const target of colKeys) {
|
|
87
|
+
const val = verticalOhlcv[target][index];
|
|
88
|
+
const key = `${prefix}_${target}`;
|
|
89
|
+
let scaled = null;
|
|
90
|
+
|
|
91
|
+
if (ready) {
|
|
92
|
+
const winKey = group ? groupKey : target;
|
|
93
|
+
const win = windows[winKey];
|
|
94
|
+
|
|
95
|
+
if (type === 'minmax') {
|
|
96
|
+
const mn = Math.min(...win);
|
|
97
|
+
const mx = Math.max(...win);
|
|
98
|
+
scaled = normalizeMinMax(val, mn, mx, range);
|
|
99
|
+
} else if (type === 'zscore') {
|
|
100
|
+
const mean = win.reduce((sum, x) => sum + x, 0) / win.length;
|
|
101
|
+
const variance = win.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / win.length;
|
|
102
|
+
const std = Math.sqrt(variance);
|
|
103
|
+
scaled = normalizeZScore(val, mean, std);
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(`Unknown scaler type "${type}"`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
main.pushToMain({ index, key, value: scaled });
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { classifyNum } from "./numberUtilities.js";
|
|
2
|
+
import { selectDateFormatter } from "./dateUtilities.js";
|
|
3
|
+
|
|
4
|
+
const numberKeys = ['open', 'high', 'low', 'close']
|
|
5
|
+
|
|
6
|
+
export const buildArray = (arrayType, len) => {
|
|
7
|
+
if(arrayType === 'Array') return new Array(len).fill(null)
|
|
8
|
+
if(arrayType === 'Float64Array') return new Float64Array(len).fill(NaN)
|
|
9
|
+
if(arrayType === 'Int32Array') return new Int32Array(len).fill(NaN)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const assignTypes = firstRow => {
|
|
13
|
+
const inputTypes = {}
|
|
14
|
+
const arrayTypes = {}
|
|
15
|
+
|
|
16
|
+
for (const [colName, cellValue] of Object.entries(firstRow)) {
|
|
17
|
+
|
|
18
|
+
if (colName === 'date') {
|
|
19
|
+
inputTypes[colName] = selectDateFormatter(cellValue, true)
|
|
20
|
+
arrayTypes[colName] = 'Array'
|
|
21
|
+
}
|
|
22
|
+
else if(colName === 'volume')
|
|
23
|
+
{
|
|
24
|
+
inputTypes[colName] = classifyNum(cellValue, true)
|
|
25
|
+
arrayTypes[colName] = 'Int32Array'
|
|
26
|
+
}
|
|
27
|
+
else if(numberKeys.includes(colName))
|
|
28
|
+
{
|
|
29
|
+
inputTypes[colName] = classifyNum(cellValue, true)
|
|
30
|
+
arrayTypes[colName] = 'Float64Array'
|
|
31
|
+
} else {
|
|
32
|
+
const thisType = classifyNum(cellValue, false)
|
|
33
|
+
inputTypes[colName] = thisType
|
|
34
|
+
arrayTypes[colName] = (thisType === 'number') ? 'Float64Array' : 'Array'
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { inputTypes, arrayTypes }
|
|
39
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export const dateFormaters = {
|
|
2
|
+
dateObject: d => d,
|
|
3
|
+
dateMilliseconds: d => new Date(d),
|
|
4
|
+
dateSeconds: d => new Date(d * 1000),
|
|
5
|
+
dateIncompleteDayString: d => new Date(`${d} 00:00:00`),
|
|
6
|
+
dateCompleteDateString: d => new Date(d)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const selectDateFormatter = value => {
|
|
10
|
+
|
|
11
|
+
// Regex for a UTC Time Format where the string ends with "Z".
|
|
12
|
+
const utcRegex = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/i;
|
|
13
|
+
|
|
14
|
+
// Regex for a Timezone Offset Format,
|
|
15
|
+
// which includes a plus or minus sign followed by HH:MM at the end.
|
|
16
|
+
const offsetRegex = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?([+-]\d{2}:\d{2})$/;
|
|
17
|
+
|
|
18
|
+
// Regex for a Local Time Format that contains no timezone indicator.
|
|
19
|
+
const localRegex = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?$/;
|
|
20
|
+
|
|
21
|
+
// Regex for a YYYY-MM-DD Format.
|
|
22
|
+
const YYMMDDRegexDash = /^\d{4}-\d{2}-\d{2}$/;
|
|
23
|
+
const YYMMDDRegexSlash = /^\d{4}\/\d{2}\/\d{2}$/;
|
|
24
|
+
|
|
25
|
+
const validFormatNames = [
|
|
26
|
+
'Date -> object',
|
|
27
|
+
'Milliseconds Timestamp -> integer',
|
|
28
|
+
'Seconds Timestamp -> integer',
|
|
29
|
+
'UTC Time Format (ISO 8601) -> string',
|
|
30
|
+
'Including a Timezone Offset (ISO 8601) -> string',
|
|
31
|
+
'Local Time Format (ISO 8601) -> string',
|
|
32
|
+
'YYYY/MM/DD or YYYY-MM-DD -> string',
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
const errorMessage = () => {
|
|
36
|
+
throw new Error(`Invalid "${typeof value}" date value passed in selectDateFormatter. Supported formats: \n-${validFormatNames.join("\n- ")}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if(typeof value === 'object')
|
|
40
|
+
{
|
|
41
|
+
if(value instanceof Date && !isNaN(value.valueOf()))
|
|
42
|
+
{
|
|
43
|
+
return 'dateObject'
|
|
44
|
+
}
|
|
45
|
+
else
|
|
46
|
+
{
|
|
47
|
+
errorMessage()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (typeof value === 'number') {
|
|
51
|
+
// Use a numeric threshold based on the absolute value: typically,
|
|
52
|
+
// if the absolute value is below 1e11, then it's a seconds timestamp.
|
|
53
|
+
// Otherwise, it's a milliseconds timestamp.
|
|
54
|
+
if (Math.abs(value) < 1e11) {
|
|
55
|
+
return 'dateSeconds';
|
|
56
|
+
} else {
|
|
57
|
+
return 'dateMillseconds';
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
else if(typeof value === 'string')
|
|
61
|
+
{
|
|
62
|
+
if(utcRegex.test(value) || offsetRegex.test(value) || (localRegex.test(value) && value.length > 10) || YYMMDDRegexSlash.test(value))
|
|
63
|
+
{
|
|
64
|
+
return 'dateCompleteDateString'
|
|
65
|
+
}
|
|
66
|
+
else if(YYMMDDRegexDash.test(value))
|
|
67
|
+
{
|
|
68
|
+
return 'dateIncompleteDayString'
|
|
69
|
+
}
|
|
70
|
+
else
|
|
71
|
+
{
|
|
72
|
+
errorMessage()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
{
|
|
77
|
+
errorMessage()
|
|
78
|
+
}
|
|
79
|
+
}
|