ohlcv-indicators 3.5.77 → 3.5.79
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 +105 -0
- package/README.md +415 -0
- package/dist/ohlcv-indicators.min.js +2 -1
- package/dist/ohlcv-indicators.min.js.LICENSE.txt +23 -0
- package/index.js +96 -127
- package/package.json +1 -1
- package/src/core-functions/initializeColumns.js +40 -0
- package/src/core-functions/mainLoop.js +49 -26
- package/src/core-functions/pushToMain.js +1 -3
- package/src/core-indicators/README.md +61 -0
- package/src/core-indicators/composites.js +136 -0
- package/src/core-indicators/index.js +5 -0
- package/src/core-indicators/indicator.js +82 -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/machine-learning/ml-utilities.js +0 -9
- package/src/moving-averages/bollingerBands.js +11 -26
- package/src/moving-averages/donchianChannel.js +30 -29
- package/src/moving-averages/heikenAshi.js +23 -33
- package/src/moving-averages/macd.js +10 -26
- package/src/moving-averages/movingAverages.js +7 -23
- package/src/moving-averages/relativeVolume.js +23 -21
- 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 +29 -31
- package/src/studies/{priceFeatures.js → candleFeatures.js} +15 -23
- package/src/studies/dateTime.js +22 -27
- package/src/studies/findCrosses.js +16 -7
- package/src/studies/lag.js +26 -13
- package/src/studies/mapCols.js +14 -15
- package/src/utilities/assignTypes.js +1 -1
- package/src/utilities/config.js +64 -0
- package/src/utilities/numberUtilities.js +2 -0
- package/src/utilities/validators.js +37 -21
- package/src/utilities/verticalToHorizontal.js +21 -16
- package/src/volatility/adx.js +7 -20
- package/src/volatility/atr.js +10 -23
- package/test/test.js +533 -0
- package/src/machine-learning/scaler.js +0 -165
- package/src/moving-averages/vidya.js +0 -181
|
@@ -1,59 +1,57 @@
|
|
|
1
|
-
import { FasterEMA } from '
|
|
1
|
+
import { FasterEMA } from '../core-indicators/index.js';
|
|
2
|
+
import { mathLog } from '../utilities/math.js';
|
|
3
|
+
import { isPositiveInteger } from '../utilities/numberUtilities.js';
|
|
4
|
+
import { validateInputValues } from '../utilities/validators.js';
|
|
5
|
+
import { initializeColumns } from '../core-functions/initializeColumns.js';
|
|
2
6
|
|
|
3
|
-
export const volumeOscillator = (main, index, fast, slow, {lag}) => {
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
export const volumeOscillator = (main, index, fast, slow, {lag, retLogs}) => {
|
|
6
8
|
|
|
7
9
|
const {verticalOhlcv, instances} = main
|
|
8
|
-
const value = verticalOhlcv.volume[index]
|
|
9
10
|
const key = `volume_oscillator_${fast}_${slow}`
|
|
10
11
|
|
|
11
12
|
if (index === 0) {
|
|
13
|
+
validateInputValues({ volume: true }, verticalOhlcv, index, 'volumeOscillator');
|
|
12
14
|
|
|
13
|
-
const {len} = main
|
|
14
15
|
Object.assign(instances, {
|
|
15
16
|
[key]: {
|
|
16
17
|
fastEMA: new FasterEMA(fast),
|
|
17
|
-
slowEMA: new FasterEMA(slow)
|
|
18
|
+
slowEMA: new FasterEMA(slow),
|
|
18
19
|
}
|
|
19
20
|
})
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
main.lag([key], lag)
|
|
26
|
-
}
|
|
22
|
+
initializeColumns(main, [
|
|
23
|
+
{ key },
|
|
24
|
+
{ key: `ret_log_${key}`, enabled: retLogs }
|
|
25
|
+
], { lag });
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
const { fastEMA, slowEMA } = instances[key];
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
slowEMA.update(value);
|
|
30
|
+
const volume = verticalOhlcv.volume[index];
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
try {
|
|
38
|
-
fastValue = fastEMA.getResult();
|
|
39
|
-
} catch (err) {
|
|
40
|
-
|
|
32
|
+
if(volume === 0) {
|
|
33
|
+
return;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
} catch (err) {
|
|
36
|
+
fastEMA.update(volume);
|
|
37
|
+
slowEMA.update(volume);
|
|
46
38
|
|
|
47
|
-
|
|
39
|
+
const fastValue = fastEMA.isStable ? fastEMA.getResult() : NaN;
|
|
40
|
+
const slowValue = slowEMA.isStable ? slowEMA.getResult() : NaN;
|
|
48
41
|
|
|
49
42
|
let volumeOscValue = NaN
|
|
43
|
+
let volumeOscRetLog = NaN
|
|
50
44
|
|
|
51
|
-
if(
|
|
52
|
-
|
|
53
|
-
volumeOscValue = 100 * (fastValue - slowValue) / slowValue
|
|
45
|
+
if(Number.isNaN(fastValue) || Number.isNaN(slowValue)) {
|
|
46
|
+
return;
|
|
54
47
|
}
|
|
55
48
|
|
|
56
|
-
|
|
49
|
+
volumeOscValue = 100 * (fastValue - slowValue) / slowValue
|
|
50
|
+
|
|
51
|
+
if(retLogs) {
|
|
52
|
+
volumeOscRetLog = mathLog(fastValue, slowValue)
|
|
53
|
+
main.pushToMain({index, key: `ret_log_${key}`, value: volumeOscRetLog})
|
|
54
|
+
}
|
|
57
55
|
|
|
58
|
-
|
|
56
|
+
main.pushToMain({index, key, value: volumeOscValue})
|
|
59
57
|
};
|
|
@@ -1,49 +1,41 @@
|
|
|
1
|
-
|
|
1
|
+
import { validateInputValues } from '../utilities/validators.js'
|
|
2
|
+
import { initializeColumns } from '../core-functions/initializeColumns.js'
|
|
2
3
|
|
|
3
4
|
const isBadNumber = (v) => v == null || !Number.isFinite(v)
|
|
4
5
|
|
|
5
|
-
export const
|
|
6
|
+
export const candleFeatures = (main, index, {lag, colKeys, retLogs}) => {
|
|
6
7
|
const { verticalOhlcv, priceBased, scaledGroups } = main
|
|
7
8
|
|
|
8
9
|
const getRet = (next, prev) => (retLogs) ? Math.log(next / prev) : (next - prev) / prev
|
|
9
10
|
const prefix = (retLogs) ? 'ret_log_' : 'ret_'
|
|
10
11
|
|
|
11
12
|
if(index === 0) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
[`${prefix}lower_wick`]: new Float64Array(len).fill(NaN),
|
|
19
|
-
[`${prefix}gap`]: new Float64Array(len).fill(NaN),
|
|
20
|
-
[`${prefix}body`]: new Float64Array(len).fill(NaN),
|
|
21
|
-
[`${prefix}range`]: new Float64Array(len).fill(NaN)
|
|
22
|
-
}
|
|
13
|
+
validateInputValues({ open: true, high: true, low: true, close: true }, verticalOhlcv, index, 'candleFeatures')
|
|
14
|
+
|
|
15
|
+
const columns = [
|
|
16
|
+
'change', 'mid_price_change', 'upper_wick', 'lower_wick',
|
|
17
|
+
'gap', 'body', 'range'
|
|
18
|
+
].map(name => ({ key: `${prefix}${name}` }))
|
|
23
19
|
|
|
24
20
|
for(const target of colKeys) {
|
|
21
|
+
validateInputValues({ [target]: true }, verticalOhlcv, index, 'candleFeatures')
|
|
22
|
+
|
|
25
23
|
if(!verticalOhlcv.hasOwnProperty(target)) {
|
|
26
24
|
throw new Error(
|
|
27
|
-
`Target property in "options.colKey" array "${target}" not found in verticalOhlcv for "
|
|
25
|
+
`Target property in "options.colKey" array "${target}" not found in verticalOhlcv for "candleFeatures".`
|
|
28
26
|
);
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
else if(!priceBased.has(target)) {
|
|
32
30
|
throw new Error(
|
|
33
|
-
`Target property in "options.colKey" array "${target}" not found in priceBased for "
|
|
31
|
+
`Target property in "options.colKey" array "${target}" not found in priceBased for "candleFeatures".`
|
|
34
32
|
);
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
columns.push({ key: `${prefix}${target}` })
|
|
38
36
|
}
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
scaledGroups.priceFeatures = Object.keys(newCols)
|
|
43
|
-
|
|
44
|
-
if(lag) {
|
|
45
|
-
main.lag(Object.keys(newCols), lag)
|
|
46
|
-
}
|
|
38
|
+
scaledGroups.candleFeatures = initializeColumns(main, columns, { lag })
|
|
47
39
|
}
|
|
48
40
|
|
|
49
41
|
const prevClose = verticalOhlcv.close[index - 1]
|
package/src/studies/dateTime.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {oneHotEncode} from '../machine-learning/ml-utilities.js'
|
|
2
|
+
import { initializeColumns } from '../core-functions/initializeColumns.js'
|
|
2
3
|
|
|
3
4
|
export const dateTime = (main, index, {lag, oneHot, precompute}) => {
|
|
4
5
|
|
|
@@ -9,11 +10,16 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
|
|
|
9
10
|
|
|
10
11
|
if(index === 0)
|
|
11
12
|
{
|
|
12
|
-
const {
|
|
13
|
+
const {dateType} = main
|
|
13
14
|
if(!dateType) throw Error('dateTime method found and invalid "date" in input ohlcv')
|
|
14
15
|
|
|
15
16
|
Object.assign(instances, {
|
|
16
17
|
dateTime: {
|
|
18
|
+
formatter: new Intl.DateTimeFormat('en-US-u-ca-gregory-nu-latn', {
|
|
19
|
+
timeZone: main.timeZone,
|
|
20
|
+
year: 'numeric', month: 'numeric', day: 'numeric', weekday: 'short',
|
|
21
|
+
hour: 'numeric', minute: 'numeric', hourCycle: 'h23'
|
|
22
|
+
}),
|
|
17
23
|
colKeys: [...precompute.colKeys, `${prefix}year`].filter(v => v !== 'one_hot_year'),
|
|
18
24
|
colKeySizes: {
|
|
19
25
|
...precompute.colKeySizes
|
|
@@ -23,28 +29,18 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
|
|
|
23
29
|
|
|
24
30
|
const { colKeys } = instances.dateTime
|
|
25
31
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
for (const key of colKeys) {
|
|
32
|
-
|
|
33
|
-
verticalOhlcv[key] = new ctor(len).fill(fillVal)
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// finally, apply lag once
|
|
38
|
-
if (lag > 0) {
|
|
39
|
-
main.lag(colKeys, lag)
|
|
40
|
-
}
|
|
32
|
+
initializeColumns(main, colKeys.map(key => ({
|
|
33
|
+
key,
|
|
34
|
+
type: oneHot ? 'Array' : 'Int16Array',
|
|
35
|
+
fill: oneHot ? null : 0
|
|
36
|
+
})), { lag })
|
|
41
37
|
}
|
|
42
38
|
|
|
43
|
-
const {colKeySizes} = instances.dateTime
|
|
39
|
+
const {colKeySizes, formatter} = instances.dateTime
|
|
44
40
|
|
|
45
41
|
const currDate = verticalOhlcv.date[index]
|
|
46
42
|
|
|
47
|
-
const dateInfo = getDateInfo(currDate, oneHot, colKeySizes)
|
|
43
|
+
const dateInfo = getDateInfo(currDate, oneHot, colKeySizes, formatter)
|
|
48
44
|
|
|
49
45
|
for(const [key, value] of Object.entries(dateInfo))
|
|
50
46
|
{
|
|
@@ -54,17 +50,16 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
|
|
|
54
50
|
|
|
55
51
|
|
|
56
52
|
|
|
57
|
-
const getDateInfo = (date, oneHot, colKeySizes) => {
|
|
58
|
-
|
|
53
|
+
const getDateInfo = (date, oneHot, colKeySizes, formatter) => {
|
|
59
54
|
|
|
60
|
-
const year = date.getFullYear();
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
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;
|
|
68
63
|
|
|
69
64
|
if (!oneHot) {
|
|
70
65
|
return {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { oneHotEncode } from "../machine-learning/ml-utilities.js"
|
|
2
2
|
import { buildArray } from "../utilities/assignTypes.js"
|
|
3
3
|
import { addPrecisionAsNumber } from "../utilities/precisionMultiplier.js"
|
|
4
|
+
import { validateInputValues } from "../utilities/validators.js"
|
|
5
|
+
import { initializeColumns } from "../core-functions/initializeColumns.js"
|
|
4
6
|
const eq = (fast, slow) => fast === slow
|
|
5
7
|
const gt = (fast, slow) => fast > slow
|
|
6
8
|
const lt = (fast, slow) => fast < slow
|
|
@@ -143,6 +145,16 @@ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
|
|
|
143
145
|
|
|
144
146
|
if(index === 0)
|
|
145
147
|
{
|
|
148
|
+
validateInputValues(
|
|
149
|
+
{
|
|
150
|
+
...(fast === 'price' ? { high: true, low: true, close: true } : { [fast]: true }),
|
|
151
|
+
...(typeof slow === 'number' ? {} : { [slow]: true })
|
|
152
|
+
},
|
|
153
|
+
verticalOhlcv,
|
|
154
|
+
index,
|
|
155
|
+
'crossPairs'
|
|
156
|
+
)
|
|
157
|
+
|
|
146
158
|
// allow numeric 'slow' as a constant column
|
|
147
159
|
if (typeof slow === 'number') {
|
|
148
160
|
|
|
@@ -173,12 +185,10 @@ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
|
|
|
173
185
|
|
|
174
186
|
const crossArrType = (limit !== null && limit <= 125) ? 'Int8Array' : 'Float64Array'
|
|
175
187
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
verticalOhlcv[`one_hot_${crossName}`] = buildArray('Array', len, null)
|
|
181
|
-
}
|
|
188
|
+
initializeColumns(main, [
|
|
189
|
+
{ key: crossName, type: crossArrType, fill: 0 },
|
|
190
|
+
{ key: `one_hot_${crossName}`, type: 'Array', enabled: oneHot }
|
|
191
|
+
])
|
|
182
192
|
|
|
183
193
|
} else if(index + 1 === len) {
|
|
184
194
|
// sanity checks
|
|
@@ -228,5 +238,4 @@ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
|
|
|
228
238
|
|
|
229
239
|
}
|
|
230
240
|
|
|
231
|
-
return true
|
|
232
241
|
}
|
package/src/studies/lag.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { initializeColumns } from "../core-functions/initializeColumns.js"
|
|
2
|
+
|
|
3
|
+
const lagColumnsKey = Symbol('lagColumns')
|
|
2
4
|
|
|
3
5
|
export const lag = (main, index, colKeys, lookback) => {
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const { verticalOhlcv, len, priceBased } = main
|
|
7
|
+
const { verticalOhlcv, priceBased, instances } = main
|
|
8
8
|
|
|
9
9
|
if(index === 0)
|
|
10
10
|
{
|
|
11
|
+
const lagColumns = new Map()
|
|
12
|
+
|
|
11
13
|
for (const targetKey of colKeys) {
|
|
12
14
|
if (!verticalOhlcv.hasOwnProperty(targetKey)) {
|
|
13
15
|
throw new Error(
|
|
@@ -16,29 +18,41 @@ export const lag = (main, index, colKeys, lookback) => {
|
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
const addToPriceBased = priceBased.has(targetKey)
|
|
21
|
+
const type = Array.isArray(verticalOhlcv[targetKey]) ? 'Array' : 'Float64Array'
|
|
22
|
+
const columns = []
|
|
23
|
+
const keyNames = []
|
|
19
24
|
|
|
20
25
|
for (let step = 1; step <= lookback; step++) {
|
|
21
26
|
const key = `${targetKey}_lag_${step}`
|
|
22
27
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const thisArrType = getArrayType(targetKey, verticalOhlcv[targetKey])
|
|
26
|
-
|
|
27
|
-
verticalOhlcv[key] = buildArray(thisArrType, len)
|
|
28
|
+
columns.push({ key, type, priceBased: addToPriceBased, includeInLag: false })
|
|
29
|
+
keyNames.push(key)
|
|
28
30
|
}
|
|
31
|
+
|
|
32
|
+
initializeColumns(main, columns)
|
|
33
|
+
lagColumns.set(targetKey, keyNames)
|
|
29
34
|
}
|
|
35
|
+
|
|
36
|
+
// colKeys belongs to the runtime queue; keep cached names out of exported params.
|
|
37
|
+
if (!instances[lagColumnsKey]) instances[lagColumnsKey] = new WeakMap()
|
|
38
|
+
instances[lagColumnsKey].set(colKeys, lagColumns)
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
const lagColumns = instances[lagColumnsKey]?.get(colKeys)
|
|
42
|
+
|
|
32
43
|
for (const targetKey of colKeys) {
|
|
44
|
+
const keyNames = lagColumns?.get(targetKey)
|
|
33
45
|
const currentColumn = verticalOhlcv[targetKey]
|
|
46
|
+
const missingValue = Array.isArray(currentColumn) ? null : NaN
|
|
34
47
|
|
|
35
48
|
// Populate the lagged values each tick:
|
|
36
49
|
for (let step = 1; step <= lookback; step++) {
|
|
37
|
-
|
|
50
|
+
// Runtime callbacks may change the target list or lookback after initialization.
|
|
51
|
+
const key = keyNames?.[step - 1] ?? `${targetKey}_lag_${step}`
|
|
38
52
|
const laggedIndex = index - step
|
|
39
53
|
const value =
|
|
40
|
-
laggedIndex < 0 || currentColumn[laggedIndex]
|
|
41
|
-
?
|
|
54
|
+
laggedIndex < 0 || currentColumn[laggedIndex] == null
|
|
55
|
+
? missingValue
|
|
42
56
|
: currentColumn[laggedIndex]
|
|
43
57
|
|
|
44
58
|
main.pushToMain({ index, key, value })
|
|
@@ -46,5 +60,4 @@ export const lag = (main, index, colKeys, lookback) => {
|
|
|
46
60
|
}
|
|
47
61
|
}
|
|
48
62
|
|
|
49
|
-
return true
|
|
50
63
|
}
|
package/src/studies/mapCols.js
CHANGED
|
@@ -1,32 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
import { validateInputValues } from '../utilities/validators.js'
|
|
2
|
+
import { initializeColumns } from '../core-functions/initializeColumns.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
export const mapCols = (main, index, newCols, callback, {lag, isPriceBased, callbackParams = {}}) => {
|
|
5
|
+
|
|
6
|
+
const {verticalOhlcv, precision} = main
|
|
4
7
|
|
|
5
8
|
if(index === 0)
|
|
6
9
|
{
|
|
10
|
+
if (callback === defaultMapColsCallback) {
|
|
11
|
+
validateInputValues({ open: true, close: true }, verticalOhlcv, index, 'mapCols')
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
for(const key of newCols)
|
|
8
15
|
{
|
|
9
16
|
if(verticalOhlcv.hasOwnProperty(key)) {
|
|
10
17
|
throw new Error(`New property "${key}" already exist in "verticalOhlcv" and can not be modified using mapCols.`)
|
|
11
18
|
}
|
|
12
|
-
|
|
13
|
-
verticalOhlcv[key] = new Array(len).fill(0)
|
|
14
|
-
|
|
15
|
-
if(precision && isPriceBased)
|
|
16
|
-
{
|
|
17
|
-
priceBased.add(key)
|
|
18
|
-
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
21
|
+
initializeColumns(main, newCols.map(key => ({
|
|
22
|
+
key, type: 'Array', priceBased: precision && isPriceBased
|
|
23
|
+
})), { lag })
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
const cols = callback({index, main})
|
|
26
|
+
const cols = callback({index, main, params: callbackParams})
|
|
28
27
|
|
|
29
|
-
if(cols == null) return
|
|
28
|
+
if(cols == null) return;
|
|
30
29
|
|
|
31
30
|
for(const [key, value] of Object.entries(cols))
|
|
32
31
|
{
|
|
@@ -29,7 +29,7 @@ export const buildArray = (arrayType, len, fallbackValue) => {
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
// Ctor.from({ length }, mapFn) works for Array *and* all TypedArrays
|
|
32
|
-
return Ctor
|
|
32
|
+
return new Ctor(len).fill(fillValue)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export const assignTypes = main => {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { defaultMapColsCallback } from '../studies/mapCols.js';
|
|
2
|
+
|
|
3
|
+
export const CONFIG_VERSION = 1;
|
|
4
|
+
const mapCallbacks = new Map([['default', defaultMapColsCallback]]);
|
|
5
|
+
|
|
6
|
+
// Copy configuration without JSON's silent conversion of functions/NaN to null.
|
|
7
|
+
export const copyConfig = (value, allowFunctions = false, ancestors = new Set()) => {
|
|
8
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') return value;
|
|
9
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
10
|
+
if (typeof value === 'function' && allowFunctions) return value;
|
|
11
|
+
if (typeof value !== 'object' || value === null) {
|
|
12
|
+
throw new TypeError('Configuration values must be JSON-safe: no undefined, non-finite numbers, or unregistered functions.');
|
|
13
|
+
}
|
|
14
|
+
if (ancestors.has(value)) throw new TypeError('Configuration must not contain circular references.');
|
|
15
|
+
const prototype = Object.getPrototypeOf(value);
|
|
16
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
17
|
+
throw new TypeError('Configuration must contain only plain objects and arrays.');
|
|
18
|
+
}
|
|
19
|
+
if (Object.getOwnPropertySymbols(value).length) throw new TypeError('Configuration must not contain symbol keys.');
|
|
20
|
+
|
|
21
|
+
ancestors.add(value);
|
|
22
|
+
const result = Array.isArray(value)
|
|
23
|
+
? Array.from(value, item => copyConfig(item, allowFunctions, ancestors))
|
|
24
|
+
: Object.fromEntries(Object.entries(value).map(([key, item]) => [key, copyConfig(item, allowFunctions, ancestors)]));
|
|
25
|
+
ancestors.delete(value);
|
|
26
|
+
return result;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const freezeConfig = value => {
|
|
30
|
+
if (value !== null && typeof value === 'object') {
|
|
31
|
+
for (const item of Object.values(value)) freezeConfig(item);
|
|
32
|
+
Object.freeze(value);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
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,7 +5,14 @@ export const validateInputParams = (inputParams, len) => {
|
|
|
5
5
|
throw new Error('Property "inputParams" must be an "Array" or "Set".')
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
for (const
|
|
8
|
+
for (const job of inputParams) {
|
|
9
|
+
if (!job || typeof job.key !== 'string' || !Array.isArray(job.params)) {
|
|
10
|
+
throw new TypeError('Each inputParams entry must have an indicator key and a params array.')
|
|
11
|
+
}
|
|
12
|
+
if (job.order !== undefined && !Number.isFinite(job.order)) {
|
|
13
|
+
throw new TypeError('Indicator order must be a finite number when provided.')
|
|
14
|
+
}
|
|
15
|
+
const { params } = job
|
|
9
16
|
for (const v of params) {
|
|
10
17
|
if (typeof v === 'number' && v > len) {
|
|
11
18
|
console.log(v, len);
|
|
@@ -16,6 +23,7 @@ export const validateInputParams = (inputParams, len) => {
|
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
export const isAlreadyComputed = main => {
|
|
26
|
+
if(main.isComputing === true) throw Error('You cannot add indicators while computation is in progress.')
|
|
19
27
|
if(main.isComputed === true) throw Error('ohlcv is already computed, you can not add new indicators after "compute", "getLastValues" or "getData" methods are called.')
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -111,32 +119,40 @@ export const validateNumber = (value, options, paramName, callerName) => {
|
|
|
111
119
|
};
|
|
112
120
|
|
|
113
121
|
|
|
114
|
-
export const
|
|
122
|
+
export const validateString = (txt, paramName, callerName) => {
|
|
115
123
|
|
|
116
|
-
|
|
124
|
+
if(typeof txt !== 'string' || !txt.length) throw new Error(`Invalid "${paramName}" property in "${callerName}". Only strings are accepted.`)
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
`Invalid "${paramName}" array length: expected 2 items, but got ${range.length} in "${callerName}.${paramName}".`
|
|
121
|
-
);
|
|
122
|
-
}
|
|
126
|
+
return true
|
|
127
|
+
}
|
|
123
128
|
|
|
124
|
-
const [min, max] = range
|
|
125
129
|
|
|
126
|
-
|
|
127
|
-
|
|
130
|
+
export const validateInputValues = (
|
|
131
|
+
requiredParams,
|
|
132
|
+
verticalOhlcv,
|
|
133
|
+
index,
|
|
134
|
+
callbackName
|
|
135
|
+
) => {
|
|
136
|
+
if (index !== 0) return true;
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
{
|
|
131
|
-
throw new Error(`Invalid "min" can not be equal to "max" property in "${callerName}.${paramName}".`)
|
|
132
|
-
}
|
|
138
|
+
const invalidParams = [];
|
|
133
139
|
|
|
134
|
-
|
|
135
|
-
|
|
140
|
+
for (const key of Object.keys(requiredParams)) {
|
|
141
|
+
const value = verticalOhlcv[key]?.[index];
|
|
136
142
|
|
|
137
|
-
|
|
143
|
+
const isValid = key === "volume"
|
|
144
|
+
? Number.isInteger(value) && value >= 0
|
|
145
|
+
: Number.isFinite(value) && value > 0;
|
|
138
146
|
|
|
139
|
-
|
|
147
|
+
if (!isValid) invalidParams.push(key);
|
|
148
|
+
}
|
|
140
149
|
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
if (invalidParams.length > 0) {
|
|
151
|
+
throw new TypeError(
|
|
152
|
+
`Invalid or missing input value(s) [${invalidParams.join(", ")}] ` +
|
|
153
|
+
`at index ${index} in "${callbackName}".`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
};
|
|
@@ -13,29 +13,34 @@ export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, da
|
|
|
13
13
|
const maxStartIndex = Math.max(skipNullIndex, startIndex)
|
|
14
14
|
const diffLen = len - maxStartIndex
|
|
15
15
|
const result = Array.from({ length: diffLen }, () => ({}))
|
|
16
|
-
const formatter = {}
|
|
17
|
-
|
|
18
|
-
for(const key of verticalOhlcvKeyNames) {
|
|
19
|
-
if(precision && priceBased.has(key)) {
|
|
20
|
-
formatter[key] = (num, mul) => outputNumberFormatter.precisionNumberCleanString(num, mul)
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
formatter[key] = v => v
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
16
|
|
|
27
17
|
for(const [key, arr] of Object.entries(verticalOhlcv)){
|
|
28
18
|
if(verticalOhlcvTempCols.has(key)) continue
|
|
29
19
|
|
|
30
|
-
|
|
20
|
+
if(key === 'date')
|
|
31
21
|
{
|
|
32
|
-
|
|
22
|
+
const formatter = dateOutputFormaters[dateFormat]
|
|
23
|
+
for (let i = maxStartIndex; i < len; i++)
|
|
33
24
|
{
|
|
34
|
-
result[i - maxStartIndex][key] =
|
|
35
|
-
}
|
|
36
|
-
|
|
25
|
+
result[i - maxStartIndex][key] = formatter(arr[i])
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if(precision && priceBased.has(key))
|
|
29
|
+
{
|
|
30
|
+
const formatter = outputNumberFormatter.precisionNumberCleanString
|
|
31
|
+
for (let i = maxStartIndex; i < len; i++)
|
|
32
|
+
{
|
|
33
|
+
const value = arr[i]
|
|
34
|
+
result[i - maxStartIndex][key] = value == null || Number.isNaN(value)
|
|
35
|
+
? value
|
|
36
|
+
: formatter(value, precisionMultiplier)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
{
|
|
41
|
+
for (let i = maxStartIndex; i < len; i++)
|
|
37
42
|
{
|
|
38
|
-
result[i - maxStartIndex][key] =
|
|
43
|
+
result[i - maxStartIndex][key] = arr[i]
|
|
39
44
|
}
|
|
40
45
|
}
|
|
41
46
|
}
|