ohlcv-indicators 3.5.79 → 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 +23 -15
- package/README.md +104 -63
- package/dist/ohlcv-indicators.min.js +1 -1
- package/dist/ohlcv-indicators.min.js.LICENSE.txt +0 -1
- package/index.js +41 -51
- package/package.json +2 -5
- package/src/core-functions/mainLoop.js +26 -46
- package/src/core-functions/pushToMain.js +7 -24
- package/src/core-indicators/README.md +8 -9
- package/src/core-indicators/indicator.js +0 -1
- package/src/moving-averages/bollingerBands.js +29 -6
- package/src/moving-averages/donchianChannel.js +34 -9
- package/src/moving-averages/heikenAshi.js +10 -19
- package/src/moving-averages/movingAverages.js +15 -5
- 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/config.js +4 -37
- package/src/utilities/numberUtilities.js +3 -3
- package/src/utilities/precisionMultiplier.js +11 -6
- package/src/utilities/verticalToHorizontal.js +6 -1
- package/src/volatility/atr.js +1 -1
- package/test/test.js +852 -155
- package/webpack.config.js +0 -11
- package/src/studies/mapCols.js +0 -45
|
@@ -2,17 +2,18 @@
|
|
|
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';
|
|
5
6
|
|
|
6
7
|
const indicatorClasses = {
|
|
7
8
|
ema: FasterEMA,
|
|
8
9
|
sma: FasterSMA
|
|
9
10
|
}
|
|
10
11
|
|
|
11
|
-
export const movingAverages = (main, index, methodName, size, { target, lag }) => {
|
|
12
|
+
export const movingAverages = (main, index, methodName, size, { target, lag, retLogs = false }) => {
|
|
12
13
|
|
|
13
14
|
const { verticalOhlcv, instances, priceBased } = main
|
|
14
15
|
const suffix = (target !== 'close') ? `_${target}` : ''
|
|
15
|
-
const keyName = `${methodName}_${size}${suffix}`
|
|
16
|
+
const keyName = `${retLogs ? 'ret_log_' : ''}${methodName}_${size}${suffix}`
|
|
16
17
|
|
|
17
18
|
if (index === 0) {
|
|
18
19
|
validateInputValues({ [target]: true }, verticalOhlcv, index, methodName)
|
|
@@ -26,7 +27,7 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
|
|
|
26
27
|
// Create the main moving average instance.
|
|
27
28
|
instances[keyName] = new indicatorClasses[methodName](size)
|
|
28
29
|
|
|
29
|
-
initializeColumns(main, [{ key: keyName, priceBased: priceBased.has(target) }], { lag })
|
|
30
|
+
initializeColumns(main, [{ key: keyName, priceBased: !retLogs && priceBased.has(target) }], { lag })
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
// Retrieve the current price value
|
|
@@ -38,6 +39,15 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
|
|
|
38
39
|
|
|
39
40
|
const currMa = instance.isStable ? instance.getResult() : NaN
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Always push the selected value (even if NaN).
|
|
52
|
+
main.pushToMain({ index, key: keyName, value: output })
|
|
43
53
|
}
|
|
@@ -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
|
+
}
|
package/src/studies/lag.js
CHANGED
|
@@ -10,27 +10,27 @@ export const lag = (main, index, colKeys, lookback) => {
|
|
|
10
10
|
{
|
|
11
11
|
const lagColumns = new Map()
|
|
12
12
|
|
|
13
|
-
for (const
|
|
14
|
-
if (!verticalOhlcv.hasOwnProperty(
|
|
13
|
+
for (const target of colKeys) {
|
|
14
|
+
if (!verticalOhlcv.hasOwnProperty(target)) {
|
|
15
15
|
throw new Error(
|
|
16
|
-
`Lag processing invoked by col "${
|
|
16
|
+
`Lag processing invoked by col "${target}" was not found in "verticalOhlcv".`
|
|
17
17
|
)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const addToPriceBased = priceBased.has(
|
|
21
|
-
const type = Array.isArray(verticalOhlcv[
|
|
20
|
+
const addToPriceBased = priceBased.has(target)
|
|
21
|
+
const type = Array.isArray(verticalOhlcv[target]) ? 'Array' : 'Float64Array'
|
|
22
22
|
const columns = []
|
|
23
23
|
const keyNames = []
|
|
24
24
|
|
|
25
25
|
for (let step = 1; step <= lookback; step++) {
|
|
26
|
-
const key = `${
|
|
26
|
+
const key = `${target}_lag_${step}`
|
|
27
27
|
|
|
28
28
|
columns.push({ key, type, priceBased: addToPriceBased, includeInLag: false })
|
|
29
29
|
keyNames.push(key)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
initializeColumns(main, columns)
|
|
33
|
-
lagColumns.set(
|
|
33
|
+
lagColumns.set(target, keyNames)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// colKeys belongs to the runtime queue; keep cached names out of exported params.
|
|
@@ -40,15 +40,14 @@ export const lag = (main, index, colKeys, lookback) => {
|
|
|
40
40
|
|
|
41
41
|
const lagColumns = instances[lagColumnsKey]?.get(colKeys)
|
|
42
42
|
|
|
43
|
-
for (const
|
|
44
|
-
const keyNames = lagColumns?.get(
|
|
45
|
-
const currentColumn = verticalOhlcv[
|
|
43
|
+
for (const target of colKeys) {
|
|
44
|
+
const keyNames = lagColumns?.get(target)
|
|
45
|
+
const currentColumn = verticalOhlcv[target]
|
|
46
46
|
const missingValue = Array.isArray(currentColumn) ? null : NaN
|
|
47
47
|
|
|
48
48
|
// Populate the lagged values each tick:
|
|
49
49
|
for (let step = 1; step <= lookback; step++) {
|
|
50
|
-
|
|
51
|
-
const key = keyNames?.[step - 1] ?? `${targetKey}_lag_${step}`
|
|
50
|
+
const key = keyNames?.[step - 1] ?? `${target}_lag_${step}`
|
|
52
51
|
const laggedIndex = index - step
|
|
53
52
|
const value =
|
|
54
53
|
laggedIndex < 0 || currentColumn[laggedIndex] == null
|
package/src/utilities/config.js
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
import { defaultMapColsCallback } from '../studies/mapCols.js';
|
|
2
|
-
|
|
3
1
|
export const CONFIG_VERSION = 1;
|
|
4
|
-
const mapCallbacks = new Map([['default', defaultMapColsCallback]]);
|
|
5
2
|
|
|
6
3
|
// Copy configuration without JSON's silent conversion of functions/NaN to null.
|
|
7
|
-
export const copyConfig = (value,
|
|
4
|
+
export const copyConfig = (value, ancestors = new Set()) => {
|
|
8
5
|
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
9
6
|
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
10
|
-
if (typeof value === 'function' && allowFunctions) return value;
|
|
11
7
|
if (typeof value !== 'object' || value === null) {
|
|
12
|
-
throw new TypeError('Configuration values must be JSON-safe: no undefined, non-finite numbers, or
|
|
8
|
+
throw new TypeError('Configuration values must be JSON-safe: no undefined, non-finite numbers, or functions.');
|
|
13
9
|
}
|
|
14
10
|
if (ancestors.has(value)) throw new TypeError('Configuration must not contain circular references.');
|
|
15
11
|
const prototype = Object.getPrototypeOf(value);
|
|
@@ -20,8 +16,8 @@ export const copyConfig = (value, allowFunctions = false, ancestors = new Set())
|
|
|
20
16
|
|
|
21
17
|
ancestors.add(value);
|
|
22
18
|
const result = Array.isArray(value)
|
|
23
|
-
? Array.from(value, item => copyConfig(item,
|
|
24
|
-
: Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyConfig(item,
|
|
19
|
+
? Array.from(value, item => copyConfig(item, ancestors))
|
|
20
|
+
: Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyConfig(item, ancestors)]));
|
|
25
21
|
ancestors.delete(value);
|
|
26
22
|
return result;
|
|
27
23
|
};
|
|
@@ -33,32 +29,3 @@ export const freezeConfig = value => {
|
|
|
33
29
|
}
|
|
34
30
|
return value;
|
|
35
31
|
};
|
|
36
|
-
|
|
37
|
-
export const registerMapCallback = (name, callback) => {
|
|
38
|
-
if (typeof name !== 'string' || !name || typeof callback !== 'function') {
|
|
39
|
-
throw new TypeError('registerMapCallback requires a nonempty name and a function.');
|
|
40
|
-
}
|
|
41
|
-
if (mapCallbacks.has(name) && mapCallbacks.get(name) !== callback) {
|
|
42
|
-
throw new Error(`mapCols callback "${name}" is already registered. Use a new versioned name.`);
|
|
43
|
-
}
|
|
44
|
-
mapCallbacks.set(name, callback);
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
export const resolveMapCallback = callback => {
|
|
48
|
-
if (typeof callback === 'function') return callback;
|
|
49
|
-
if (typeof callback === 'string' && mapCallbacks.has(callback)) return mapCallbacks.get(callback);
|
|
50
|
-
throw new Error(`Unknown mapCols callback "${callback}". Register it before loading the configuration.`);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const exportInputParams = inputParams => {
|
|
54
|
-
const result = copyConfig(inputParams, true);
|
|
55
|
-
for (const job of result) {
|
|
56
|
-
if (job.key !== 'mapCols' || typeof job.params[1] !== 'function') continue;
|
|
57
|
-
const entry = [...mapCallbacks].find(([, callback]) => callback === job.params[1]);
|
|
58
|
-
if (!entry) {
|
|
59
|
-
throw new Error('Cannot export an unregistered mapCols callback. Register it with OHLCV_INDICATORS.registerMapCallback(name, callback).');
|
|
60
|
-
}
|
|
61
|
-
job.params[1] = entry[0];
|
|
62
|
-
}
|
|
63
|
-
return copyConfig(result);
|
|
64
|
-
};
|
|
@@ -5,13 +5,13 @@ export const isPositiveInteger = n => Number.isInteger(n) && n >= 0;
|
|
|
5
5
|
export const inputNumberFormatter = {
|
|
6
6
|
number: n => n,
|
|
7
7
|
numberCleanString: n => Number(n),
|
|
8
|
-
precisionNumberCleanString: (strNum, multiplier) => addPrecisionAsNumber(strNum, multiplier)
|
|
8
|
+
precisionNumberCleanString: (strNum, multiplier, cachedDecimals) => addPrecisionAsNumber(strNum, multiplier, cachedDecimals)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const outputNumberFormatter = {
|
|
12
12
|
number: n => n,
|
|
13
13
|
numberCleanString: n => n,
|
|
14
|
-
precisionNumberCleanString: (strNum, multiplier) => revertPrecisionAsNumber(strNum, multiplier)
|
|
14
|
+
precisionNumberCleanString: (strNum, multiplier, cachedDecimals) => revertPrecisionAsNumber(strNum, multiplier, cachedDecimals)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export const classifyNum = (num, throwError = false, precision = false) => {
|
|
@@ -34,4 +34,4 @@ export const classifyNum = (num, throwError = false, precision = false) => {
|
|
|
34
34
|
|
|
35
35
|
if(throwError) throw new TypeError('Invalid input: expected a number or numeric string')
|
|
36
36
|
return typeof num
|
|
37
|
-
}
|
|
37
|
+
}
|
|
@@ -51,8 +51,14 @@ export const calcPrecisionMultiplier = main => {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
// Keep the existing decimal-width rule, including scientific-notation multipliers.
|
|
55
|
+
export const getPrecisionDecimals = multiplier => {
|
|
56
|
+
const mulStr = String(multiplier)
|
|
57
|
+
return multiplier === 1 ? 0 : mulStr.length - 1
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
//this function converts the strNum into a integer * multiplier
|
|
55
|
-
export const addPrecisionAsNumber = (strNum, multiplier) => {
|
|
61
|
+
export const addPrecisionAsNumber = (strNum, multiplier, cachedDecimals) => {
|
|
56
62
|
if (typeof strNum !== 'string') throw new Error('addPrecisionExact expects a string when precision=true')
|
|
57
63
|
|
|
58
64
|
const negative = strNum.charAt(0) === '-'
|
|
@@ -60,8 +66,7 @@ export const addPrecisionAsNumber = (strNum, multiplier) => {
|
|
|
60
66
|
const [intPart, decPart = ''] = s.split('.')
|
|
61
67
|
|
|
62
68
|
// Compute decimals from multiplier (multiplier expected as 10^decimals)
|
|
63
|
-
const
|
|
64
|
-
const decimals = (multiplier === 1) ? 0 : (mulStr.length - 1)
|
|
69
|
+
const decimals = cachedDecimals === undefined ? getPrecisionDecimals(multiplier) : cachedDecimals
|
|
65
70
|
|
|
66
71
|
const decPadded = decPart.padEnd(decimals, '0').slice(0, decimals); // pad or trim
|
|
67
72
|
const integerString = intPart + decPadded // e.g. "12" + "3400" => "123400"
|
|
@@ -75,7 +80,7 @@ export const addPrecisionAsNumber = (strNum, multiplier) => {
|
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
//this function converts the outputs the number generated in addPrecisionAsNumber / multiplier
|
|
78
|
-
export const revertPrecisionAsNumber = (num, multiplier) => {
|
|
83
|
+
export const revertPrecisionAsNumber = (num, multiplier, cachedDecimals) => {
|
|
79
84
|
if (typeof num !== 'number' || !Number.isFinite(num)) {
|
|
80
85
|
throw new Error('revertPrecisionAsNumber expects a finite number')
|
|
81
86
|
}
|
|
@@ -86,7 +91,7 @@ export const revertPrecisionAsNumber = (num, multiplier) => {
|
|
|
86
91
|
// Make sure we're working with an integer, mirroring addPrecisionAsNumber
|
|
87
92
|
abs = Math.trunc(abs)
|
|
88
93
|
|
|
89
|
-
const decimals =
|
|
94
|
+
const decimals = cachedDecimals === undefined ? getPrecisionDecimals(multiplier) : cachedDecimals
|
|
90
95
|
|
|
91
96
|
if (decimals === 0) {
|
|
92
97
|
// No fractional part; just restore sign
|
|
@@ -106,4 +111,4 @@ export const revertPrecisionAsNumber = (num, multiplier) => {
|
|
|
106
111
|
: `${intPart}.${fracStr}`
|
|
107
112
|
|
|
108
113
|
return Number(negative ? `-${resultStr}` : resultStr)
|
|
109
|
-
}
|
|
114
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dateOutputFormaters } from "./dateUtilities.js"
|
|
2
2
|
import { outputNumberFormatter } from "./numberUtilities.js"
|
|
3
|
+
import { getPrecisionDecimals } from "./precisionMultiplier.js"
|
|
3
4
|
|
|
4
5
|
export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, dateFormat}) => {
|
|
5
6
|
|
|
@@ -8,6 +9,10 @@ export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, da
|
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
if (verticalOhlcvKeyNames.length === 0) return []
|
|
12
|
+
|
|
13
|
+
const precisionDecimals = precision && typeof precisionMultiplier === 'number'
|
|
14
|
+
? getPrecisionDecimals(precisionMultiplier)
|
|
15
|
+
: undefined
|
|
11
16
|
|
|
12
17
|
const skipNullIndex = skipNull && invalidValueIndex >= 0 ? invalidValueIndex + 1 : 0
|
|
13
18
|
const maxStartIndex = Math.max(skipNullIndex, startIndex)
|
|
@@ -33,7 +38,7 @@ export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, da
|
|
|
33
38
|
const value = arr[i]
|
|
34
39
|
result[i - maxStartIndex][key] = value == null || Number.isNaN(value)
|
|
35
40
|
? value
|
|
36
|
-
: formatter(value, precisionMultiplier)
|
|
41
|
+
: formatter(value, precisionMultiplier, precisionDecimals)
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
else
|
package/src/volatility/atr.js
CHANGED
|
@@ -16,7 +16,7 @@ export const atr = (main, index, size, {lag, retLogs}) => {
|
|
|
16
16
|
|
|
17
17
|
instances[baseKeyName] = new FasterATR(size, FasterWSMA)
|
|
18
18
|
|
|
19
|
-
initializeColumns(main, [{ key: baseKeyName, priceBased:
|
|
19
|
+
initializeColumns(main, [{ key: baseKeyName, priceBased: !retLogs }], { lag })
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
// Retrieve the current price value.
|