ohlcv-indicators 3.5.53 → 3.5.54
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/index.js +37 -59
- package/package.json +1 -1
- package/src/core-functions/mainLoop.js +8 -3
- package/src/core-functions/pushToMain.js +1 -2
- package/src/machine-learning/classifier.js +56 -3
- package/src/moving-averages/bollingerBands.js +11 -8
- package/src/moving-averages/donchianChannel.js +8 -6
- package/src/moving-averages/heikenAshi.js +5 -1
- package/src/moving-averages/macd.js +5 -1
- package/src/moving-averages/movingAverages.js +5 -1
- package/src/moving-averages/vidya.js +7 -1
- package/src/oscillators/volumeDelta.js +74 -0
- package/src/studies/findCrosses.js +12 -4
- package/src/studies/lag.js +17 -12
- package/src/utilities/assignTypes.js +4 -6
- package/src/utilities/numberUtilities.js +22 -39
- package/src/utilities/precisionMultiplier.js +91 -16
- package/src/utilities/validators.js +1 -1
- package/src/utilities/verticalToHorizontal.js +16 -3
- package/src/studies/correlation.js +0 -98
package/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { mainLoop } from './src/core-functions/mainLoop.js'
|
|
2
|
-
import { correlation } from './src/studies/correlation.js'
|
|
3
2
|
import {
|
|
4
3
|
isAlreadyComputed,
|
|
5
4
|
validateArray,
|
|
@@ -14,10 +13,10 @@ import {
|
|
|
14
13
|
import { verticalToHorizontal } from './src/utilities/verticalToHorizontal.js'
|
|
15
14
|
import { pushToMain } from './src/core-functions/pushToMain.js'
|
|
16
15
|
import { assignTypes } from './src/utilities/assignTypes.js'
|
|
17
|
-
import { buildArray, getArrayType } from './src/utilities/assignTypes.js'
|
|
18
16
|
import { dateOutputFormaters } from './src/utilities/dateUtilities.js'
|
|
19
17
|
import { defaultMapColsCallback } from './src/studies/mapCols.js'
|
|
20
18
|
import { getOrderFromArray } from './src/utilities/order.js'
|
|
19
|
+
import { calcPrecisionMultiplier } from './src/utilities/precisionMultiplier.js'
|
|
21
20
|
|
|
22
21
|
import {
|
|
23
22
|
defaultYCallback,
|
|
@@ -38,24 +37,32 @@ import { normalizeMinMax } from './src/machine-learning/ml-utilities.js'
|
|
|
38
37
|
*/
|
|
39
38
|
|
|
40
39
|
export default class OHLCV_INDICATORS {
|
|
41
|
-
constructor({input, ticker = null, inputParams = null, chunkProcess = 2000, ML = {}}) {
|
|
40
|
+
constructor({input, ticker = null, inputParams = null, chunkProcess = 2000, ML = {}, precision = false}) {
|
|
42
41
|
|
|
43
42
|
validateArray(input, 'input', (ticker !== null) ? `contructor ${ticker}` : 'constuctor')
|
|
44
43
|
if(input.length === 0) throw Error('input OHLCV must not be empty: ' + ticker)
|
|
45
44
|
|
|
46
45
|
validateNumber(chunkProcess, {min: 100, max: 50000, allowDecimals: false}, 'chunkProcess', 'constructor')
|
|
47
46
|
validateObject(ML, 'ML', 'constructor')
|
|
47
|
+
validateBoolean(precision, 'precision', 'contructor')
|
|
48
48
|
|
|
49
49
|
this.chunkProcess = chunkProcess
|
|
50
50
|
|
|
51
51
|
this.input = [...input]
|
|
52
52
|
this.len = this.input.length
|
|
53
53
|
this.firstRow = this.input[0]
|
|
54
|
-
|
|
54
|
+
this.precision = precision
|
|
55
|
+
|
|
56
|
+
const initialPriceBasedArr = ['open', 'high', 'low', 'close']
|
|
57
|
+
this.initialPriceBased = new Set(initialPriceBasedArr)
|
|
58
|
+
this.priceBased = new Set(initialPriceBasedArr)
|
|
59
|
+
this.precisionMultiplier = calcPrecisionMultiplier(this)
|
|
60
|
+
|
|
55
61
|
const {inputTypes, arrayTypes} = assignTypes(this)
|
|
56
62
|
|
|
57
63
|
this.inputTypes = inputTypes
|
|
58
64
|
this.arrayTypes = arrayTypes
|
|
65
|
+
|
|
59
66
|
if(!this.firstRow.hasOwnProperty('close')) throw Error(`input OHLCV array objects require at least "close" property: ${ticker}`)
|
|
60
67
|
|
|
61
68
|
this.dateType = this.inputTypes.date ? this.inputTypes.date : null;
|
|
@@ -65,10 +72,7 @@ export default class OHLCV_INDICATORS {
|
|
|
65
72
|
this.verticalOhlcv = {}
|
|
66
73
|
this.verticalOhlcvKeyNames = []
|
|
67
74
|
this.verticalOhlcvTempCols = new Set()
|
|
68
|
-
|
|
69
|
-
this.utilities = {
|
|
70
|
-
correlation
|
|
71
|
-
}
|
|
75
|
+
this.utilities = {}
|
|
72
76
|
|
|
73
77
|
this.invalidValueIndex = -1
|
|
74
78
|
this.scaledGroups = {}
|
|
@@ -105,50 +109,6 @@ export default class OHLCV_INDICATORS {
|
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
|
|
108
|
-
getDataAsCols(options = {}) {
|
|
109
|
-
this.compute();
|
|
110
|
-
|
|
111
|
-
const {skipNull = true, dateFormat = 'string'} = options
|
|
112
|
-
|
|
113
|
-
validateArrayOptions(Object.keys(dateOutputFormaters), dateFormat, 'dateFormat', 'getDataAsCols')
|
|
114
|
-
validateObject(options, 'options', 'getDataAsCols')
|
|
115
|
-
|
|
116
|
-
const {
|
|
117
|
-
len,
|
|
118
|
-
invalidValueIndex,
|
|
119
|
-
verticalOhlcv,
|
|
120
|
-
verticalOhlcvTempCols,
|
|
121
|
-
} = this
|
|
122
|
-
const result = {}
|
|
123
|
-
|
|
124
|
-
const startIndex = skipNull ? invalidValueIndex + 1 : 0
|
|
125
|
-
const newLen = len - startIndex
|
|
126
|
-
|
|
127
|
-
for (const [key, arr] of Object.entries(verticalOhlcv)) {
|
|
128
|
-
|
|
129
|
-
if(verticalOhlcvTempCols.has(key)) continue
|
|
130
|
-
|
|
131
|
-
const thisArrType = getArrayType(key, verticalOhlcv[key])
|
|
132
|
-
|
|
133
|
-
result[key] = buildArray(thisArrType, newLen)
|
|
134
|
-
|
|
135
|
-
for (let x = startIndex; x < len; x++)
|
|
136
|
-
{
|
|
137
|
-
if(key === 'date')
|
|
138
|
-
{
|
|
139
|
-
result[key][x] = dateOutputFormaters[dateFormat](arr[x])
|
|
140
|
-
}
|
|
141
|
-
else{
|
|
142
|
-
result[key][x] = arr[x]
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return result;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
|
|
152
112
|
getData(options = {}) {
|
|
153
113
|
|
|
154
114
|
//getData method returns the last object (row) of the new OHLCV with indicators: {open, high, low, close, rsi_14, bollinger_bands_upper}
|
|
@@ -292,6 +252,27 @@ export default class OHLCV_INDICATORS {
|
|
|
292
252
|
return this
|
|
293
253
|
}
|
|
294
254
|
|
|
255
|
+
volumeDelta(options = {}) {
|
|
256
|
+
|
|
257
|
+
const methodName = 'volumeDelta'
|
|
258
|
+
|
|
259
|
+
if(!this.inputTypes.hasOwnProperty('volume')) {
|
|
260
|
+
throw new Error('If "volumeDelta" is called the input ohlcv must contain valid volume properties.')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
isAlreadyComputed(this)
|
|
264
|
+
|
|
265
|
+
validateObject(options, 'options', methodName)
|
|
266
|
+
|
|
267
|
+
const { lag = 0} = options;
|
|
268
|
+
|
|
269
|
+
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
270
|
+
|
|
271
|
+
this.inputParams.push({key: methodName, order: 0, params: [{lag}]})
|
|
272
|
+
|
|
273
|
+
return this
|
|
274
|
+
}
|
|
275
|
+
|
|
295
276
|
atr(size = 14, options = {}) {
|
|
296
277
|
|
|
297
278
|
const methodName = 'atr'
|
|
@@ -479,17 +460,16 @@ export default class OHLCV_INDICATORS {
|
|
|
479
460
|
validateNumber(stdDev, {min: 0.01, max: 50, allowDecimals: true}, 'stdDev', methodName)
|
|
480
461
|
validateObject(options, 'options', methodName)
|
|
481
462
|
|
|
482
|
-
const {target = 'close', height = false, range = [], lag = 0
|
|
463
|
+
const {target = 'close', height = false, range = [], lag = 0} = options
|
|
483
464
|
|
|
484
465
|
validateString(target, 'options.target', methodName)
|
|
485
466
|
validateArray(range, 'options.range', methodName)
|
|
486
467
|
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
487
468
|
validateBoolean(height, 'options.height', methodName)
|
|
488
|
-
if(decimals !== null) validateNumber(decimals, {min: 1, max: 15, allowDecimals: false}, 'decimals', methodName)
|
|
489
469
|
|
|
490
470
|
const order = getOrderFromArray([target], methodName)
|
|
491
471
|
|
|
492
|
-
this.inputParams.push({key: methodName, order, params: [size, stdDev, {target, height, range, lag
|
|
472
|
+
this.inputParams.push({key: methodName, order, params: [size, stdDev, {target, height, range, lag}]});
|
|
493
473
|
|
|
494
474
|
return this;
|
|
495
475
|
}
|
|
@@ -537,19 +517,17 @@ export default class OHLCV_INDICATORS {
|
|
|
537
517
|
|
|
538
518
|
isAlreadyComputed(this)
|
|
539
519
|
|
|
540
|
-
|
|
541
520
|
validateNumber(size, {min: 1, max: this.len, allowDecimals: false}, 'size', methodName)
|
|
542
521
|
validateNumber(offset, {min: 0, max: this.len, allowDecimals: false}, 'offset', methodName)
|
|
543
522
|
|
|
544
523
|
validateObject(options, 'options', methodName)
|
|
545
|
-
const { height = false, range = [], lag = 0
|
|
524
|
+
const { height = false, range = [], lag = 0} = options;
|
|
546
525
|
|
|
547
526
|
validateArray(range, 'options.range', methodName)
|
|
548
527
|
validateNumber(lag, {min: 0, max: this.len, allowDecimals: false}, 'options.lag', methodName)
|
|
549
528
|
validateBoolean(height, 'options.height', methodName)
|
|
550
|
-
if(decimals !== null) validateNumber(decimals, {min: 1, max: 15, allowDecimals: false}, 'decimals', methodName)
|
|
551
529
|
|
|
552
|
-
this.inputParams.push({ key: methodName, order: 0, params: [size, offset, { height, range, lag
|
|
530
|
+
this.inputParams.push({ key: methodName, order: 0, params: [size, offset, { height, range, lag}] });
|
|
553
531
|
|
|
554
532
|
return this;
|
|
555
533
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ohlcv-indicators",
|
|
3
|
-
"version": "3.5.
|
|
3
|
+
"version": "3.5.54",
|
|
4
4
|
"description": "A JavaScript library featuring optimized trading indicators for OHLCV arrays [{open, high, low, close, value}]. This library is optimized for backtesting and trading bot development.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,7 @@ import { relativeVolume } from "../moving-averages/relativeVolume.js";
|
|
|
10
10
|
import { donchianChannels } from "../moving-averages/donchianChannel.js";
|
|
11
11
|
import { bollingerBands } from "../moving-averages/bollingerBands.js";
|
|
12
12
|
import { volumeOscillator } from "../oscillators/volumeOscillator.js";
|
|
13
|
+
import { volumeDelta } from "../oscillators/volumeDelta.js";
|
|
13
14
|
import { lag } from "../studies/lag.js";
|
|
14
15
|
import { crossPairs } from "../studies/findCrosses.js";
|
|
15
16
|
import { dateTime } from "../studies/dateTime.js";
|
|
@@ -18,7 +19,7 @@ import { atr } from "../volatility/atr.js";
|
|
|
18
19
|
import { mapCols } from "../studies/mapCols.js";
|
|
19
20
|
|
|
20
21
|
import { buildArray } from "../utilities/assignTypes.js";
|
|
21
|
-
import {
|
|
22
|
+
import { inputNumberFormatter } from "../utilities/numberUtilities.js";
|
|
22
23
|
import { dateFormaters } from "../utilities/dateUtilities.js";
|
|
23
24
|
|
|
24
25
|
import { pca } from "../machine-learning/pca.js";
|
|
@@ -36,6 +37,7 @@ const mainFunctions = {
|
|
|
36
37
|
sma: movingAverages,
|
|
37
38
|
vidya,
|
|
38
39
|
macd,
|
|
40
|
+
volumeDelta,
|
|
39
41
|
relativeVolume,
|
|
40
42
|
donchianChannels,
|
|
41
43
|
bollingerBands,
|
|
@@ -55,6 +57,7 @@ export const mainLoop = (input, main) => {
|
|
|
55
57
|
inputTypes,
|
|
56
58
|
chunkProcess,
|
|
57
59
|
processSecondaryLoop,
|
|
60
|
+
precisionMultiplier
|
|
58
61
|
} = main;
|
|
59
62
|
|
|
60
63
|
validateInputParams(inputParams, len)
|
|
@@ -75,9 +78,11 @@ export const mainLoop = (input, main) => {
|
|
|
75
78
|
{
|
|
76
79
|
const value = curr[key]
|
|
77
80
|
|
|
78
|
-
if(
|
|
81
|
+
if(inputNumberFormatter.hasOwnProperty(formaterKey))
|
|
79
82
|
{
|
|
80
|
-
|
|
83
|
+
if(typeof value === 'undefined') console.log({index, formaterKey, key})
|
|
84
|
+
|
|
85
|
+
const formatedValue = inputNumberFormatter[formaterKey](value, precisionMultiplier)
|
|
81
86
|
|
|
82
87
|
main.pushToMain({index, key, value: formatedValue})
|
|
83
88
|
}
|
|
@@ -25,8 +25,7 @@ export const areKeyValuesValid = (main, index, keyNames) => {
|
|
|
25
25
|
|
|
26
26
|
const val = verticalOhlcv[key][index]
|
|
27
27
|
|
|
28
|
-
if(
|
|
29
|
-
{
|
|
28
|
+
if (val == null || Number.isNaN(val)) {
|
|
30
29
|
output = false
|
|
31
30
|
break
|
|
32
31
|
}
|
|
@@ -5,6 +5,51 @@ import { countUniqueLabels, oversampleXY } from "./balancing.js"
|
|
|
5
5
|
|
|
6
6
|
const algo = 'classifier'
|
|
7
7
|
|
|
8
|
+
class Store {
|
|
9
|
+
constructor(args = {}) {
|
|
10
|
+
Object.entries(args).forEach(([k, v]) => { this[k] = v })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// internal helpers
|
|
14
|
+
_unwrap = v => Array.isArray(v) ? v[0] : v
|
|
15
|
+
_num = (v, def = 0) => {
|
|
16
|
+
const n = Number(this._unwrap(v))
|
|
17
|
+
return Number.isFinite(n) ? n : def
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// set: supports value OR updater function
|
|
21
|
+
// if you pass a function, we feed it the current *numeric* value
|
|
22
|
+
set = (key, valueOrFn) => {
|
|
23
|
+
if (typeof valueOrFn === 'function') {
|
|
24
|
+
const next = valueOrFn(this._num(this[key], 0))
|
|
25
|
+
this[key] = next // <- change to [next] if you want to keep the array wrapper
|
|
26
|
+
return next
|
|
27
|
+
} else {
|
|
28
|
+
this[key] = valueOrFn // <- change to [valueOrFn] if you want to keep the array wrapper
|
|
29
|
+
return valueOrFn
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// convenience numerics
|
|
34
|
+
inc = (key, delta = 1) => this.set(key, v => v + delta)
|
|
35
|
+
add = (key, delta = 0) => this.inc(key, delta) // alias
|
|
36
|
+
getNum = (key, def = 0) => this._num(this[key], def)
|
|
37
|
+
|
|
38
|
+
get = (key) => this._unwrap(this[key])
|
|
39
|
+
getAll = () => {
|
|
40
|
+
|
|
41
|
+
const output = {}
|
|
42
|
+
|
|
43
|
+
for(const [key, valueOrFn] of Object.entries(this)) {
|
|
44
|
+
if(typeof valueOrFn === 'function') continue
|
|
45
|
+
output[key] = valueOrFn
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
return {...output}
|
|
50
|
+
} // shallow clone of data-only fields
|
|
51
|
+
}
|
|
52
|
+
|
|
8
53
|
export const classifier = (
|
|
9
54
|
main,
|
|
10
55
|
index,
|
|
@@ -39,7 +84,15 @@ export const classifier = (
|
|
|
39
84
|
const expectedLoops = (flatY) ? predictions : 1 //
|
|
40
85
|
|
|
41
86
|
instances[algo][prefix] = {
|
|
42
|
-
|
|
87
|
+
store: new Store({
|
|
88
|
+
strategy: null,
|
|
89
|
+
total: 0,
|
|
90
|
+
win: 0,
|
|
91
|
+
loss: 0,
|
|
92
|
+
strategyGainLoss: 0,
|
|
93
|
+
idx: 0,
|
|
94
|
+
initialBalance: verticalOhlcv.close[index]
|
|
95
|
+
}),
|
|
43
96
|
expectedLoops,
|
|
44
97
|
isTrained: new Array(expectedLoops).fill(false),
|
|
45
98
|
uniqueLabels: new Array(expectedLoops).fill(0),
|
|
@@ -66,7 +119,7 @@ export const classifier = (
|
|
|
66
119
|
{
|
|
67
120
|
return
|
|
68
121
|
}
|
|
69
|
-
else if(filterCallback(verticalOhlcv, index) === false)
|
|
122
|
+
else if(filterCallback(verticalOhlcv, index, instances[algo][prefix].store) === false)
|
|
70
123
|
{
|
|
71
124
|
return
|
|
72
125
|
}
|
|
@@ -138,7 +191,7 @@ export const classifier = (
|
|
|
138
191
|
if(!trainX) return
|
|
139
192
|
|
|
140
193
|
//if univariable Y (flatY) a model is created for each prediction
|
|
141
|
-
const trainY = yCallback(index, verticalOhlcv, horizon)
|
|
194
|
+
const trainY = yCallback(index, verticalOhlcv, horizon, instances[algo][prefix].store)
|
|
142
195
|
|
|
143
196
|
if (trainY !== null && (!Array.isArray(trainY) || trainY.length !== predictions)) {
|
|
144
197
|
|
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { FasterBollingerBands } from 'trading-signals'
|
|
2
|
-
import { roundDecimalPlaces } from '../utilities/numberUtilities.js';
|
|
3
|
-
|
|
4
2
|
const defaultTarget = 'close'
|
|
5
3
|
|
|
6
|
-
export const bollingerBands = (main, index, size, stdDev, { height, range = [], target, lag
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
export const bollingerBands = (main, index, size, stdDev, { height, range = [], target, lag }) => {
|
|
9
5
|
|
|
10
6
|
const { verticalOhlcv, instances } = main;
|
|
11
7
|
const indicatorKey = `${size}_${stdDev}`;
|
|
@@ -14,7 +10,7 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
14
10
|
// Initialization on the first call.
|
|
15
11
|
if (index === 0) {
|
|
16
12
|
|
|
17
|
-
const {inputParams, verticalOhlcv, len } = main;
|
|
13
|
+
const {inputParams, verticalOhlcv, len, priceBased } = main;
|
|
18
14
|
|
|
19
15
|
if (!(target in verticalOhlcv)) {
|
|
20
16
|
throw new Error(`bollingerBands could not find target "${target}"`);
|
|
@@ -62,6 +58,13 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
62
58
|
|
|
63
59
|
}
|
|
64
60
|
|
|
61
|
+
if(priceBased.has(target)) {
|
|
62
|
+
for(const k of keyNames) {
|
|
63
|
+
priceBased.add(k)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
65
68
|
const verticalOhlcvSetup = Object.fromEntries(keyNames.map(v => [v, new Float64Array(len).fill(NaN)]))
|
|
66
69
|
Object.assign(verticalOhlcv, {...verticalOhlcvSetup})
|
|
67
70
|
|
|
@@ -106,7 +109,7 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
106
109
|
if (!Number.isNaN(lower) && !Number.isNaN(upper)) {
|
|
107
110
|
heightValue = ((upper - lower) / lower)
|
|
108
111
|
}
|
|
109
|
-
main.pushToMain({ index, key: `${prefix}_height${suffix}`, value:
|
|
112
|
+
main.pushToMain({ index, key: `${prefix}_height${suffix}`, value: heightValue });
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
// Process each range property.
|
|
@@ -116,7 +119,7 @@ export const bollingerBands = (main, index, size, stdDev, { height, range = [],
|
|
|
116
119
|
if (!Number.isNaN(priceValue) && !Number.isNaN(lower) && !Number.isNaN(upper)) {
|
|
117
120
|
rangeValue = (priceValue - lower) / (upper - lower)
|
|
118
121
|
}
|
|
119
|
-
main.pushToMain({ index, key: `${prefix}_range_${rangeKey}${suffix}`, value:
|
|
122
|
+
main.pushToMain({ index, key: `${prefix}_range_${rangeKey}${suffix}`, value: rangeValue });
|
|
120
123
|
}
|
|
121
124
|
|
|
122
125
|
return true;
|
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
|
|
2
|
-
import { roundDecimalPlaces } from "../utilities/numberUtilities.js";
|
|
3
|
-
|
|
4
2
|
export const donchianChannels = (main, index, size, offset, options) => {
|
|
5
3
|
|
|
6
4
|
|
|
7
5
|
|
|
8
|
-
const {verticalOhlcv, instances, len, inputParams, lag} = main
|
|
9
|
-
const { height: includeHeight, range: rangeKeys
|
|
6
|
+
const {verticalOhlcv, instances, len, inputParams, lag, priceBased} = main
|
|
7
|
+
const { height: includeHeight, range: rangeKeys } = options
|
|
10
8
|
const indicatorKey = `${size}_${offset}`
|
|
11
9
|
|
|
12
10
|
// ---- INIT (only at first bar) ----
|
|
@@ -30,6 +28,10 @@ export const donchianChannels = (main, index, size, offset, options) => {
|
|
|
30
28
|
keys.push(`${prefix}_range_${rk}`);
|
|
31
29
|
}
|
|
32
30
|
|
|
31
|
+
for(const k of keys) {
|
|
32
|
+
priceBased.add(k)
|
|
33
|
+
}
|
|
34
|
+
|
|
33
35
|
// bootstrap instance
|
|
34
36
|
if (!instances.donchian_channel) {
|
|
35
37
|
instances.donchian_channel = { numberOfIndicators, settings: {} };
|
|
@@ -96,7 +98,7 @@ export const donchianChannels = (main, index, size, offset, options) => {
|
|
|
96
98
|
const heightVal = hasBounds && lower
|
|
97
99
|
? (upper - lower) / lower
|
|
98
100
|
: NaN;
|
|
99
|
-
main.pushToMain({ index, key: `${prefix}_height`, value:
|
|
101
|
+
main.pushToMain({ index, key: `${prefix}_height`, value: heightVal });
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
// precompute spread once
|
|
@@ -111,7 +113,7 @@ export const donchianChannels = (main, index, size, offset, options) => {
|
|
|
111
113
|
const val = hasBounds && price === price && spread
|
|
112
114
|
? (price - lower) / spread
|
|
113
115
|
: NaN;
|
|
114
|
-
main.pushToMain({ index, key: rangeOut[i], value:
|
|
116
|
+
main.pushToMain({ index, key: rangeOut[i], value: val });
|
|
115
117
|
}
|
|
116
118
|
|
|
117
119
|
return true;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { FasterEMA } from 'trading-signals';
|
|
2
2
|
|
|
3
3
|
export const heikenAshi = (main, index, smoothLength, afterSmoothLength, { lag = 0, bothNull = false } = {}) => {
|
|
4
|
-
const { verticalOhlcv, instances, len } = main;
|
|
4
|
+
const { verticalOhlcv, instances, len, priceBased } = main;
|
|
5
5
|
const prefix = (bothNull) ? 'heiken_ashi' : `heiken_ashi_${smoothLength}_${afterSmoothLength}`;
|
|
6
6
|
const keys = ['open', 'high', 'low', 'close'];
|
|
7
7
|
|
|
@@ -26,6 +26,10 @@ export const heikenAshi = (main, index, smoothLength, afterSmoothLength, { lag =
|
|
|
26
26
|
[...keyNames, `${prefix}_cross`].map(v => [v, new Float64Array(len).fill(NaN)])
|
|
27
27
|
);
|
|
28
28
|
|
|
29
|
+
for(const k of keyNames) {
|
|
30
|
+
priceBased.add(k)
|
|
31
|
+
}
|
|
32
|
+
|
|
29
33
|
Object.assign(verticalOhlcv, { ...verticalOhlcvSetup });
|
|
30
34
|
|
|
31
35
|
if (lag > 0) {
|
|
@@ -6,7 +6,7 @@ export const macd = (main, index, fast, slow, signal, {target, lag, precomputed}
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
const { verticalOhlcv, instances } = main
|
|
9
|
+
const { verticalOhlcv, instances, priceBased } = main
|
|
10
10
|
const {instanceKey} = precomputed
|
|
11
11
|
|
|
12
12
|
// Initialization on the first index.
|
|
@@ -52,6 +52,10 @@ export const macd = (main, index, fast, slow, signal, {target, lag, precomputed}
|
|
|
52
52
|
|
|
53
53
|
const keyNames = [diffKey, deaKey, histogramKey]
|
|
54
54
|
|
|
55
|
+
for(const k of keyNames) {
|
|
56
|
+
priceBased.add(k)
|
|
57
|
+
}
|
|
58
|
+
|
|
55
59
|
if(lag > 0)
|
|
56
60
|
{
|
|
57
61
|
main.lag(keyNames, lag)
|
|
@@ -10,7 +10,7 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
13
|
-
const { verticalOhlcv, instances } = main
|
|
13
|
+
const { verticalOhlcv, instances, priceBased } = main
|
|
14
14
|
const suffix = (target !== 'close') ? `_${target}` : ''
|
|
15
15
|
const keyName = `${methodName}_${size}${suffix}`
|
|
16
16
|
|
|
@@ -29,6 +29,10 @@ export const movingAverages = (main, index, methodName, size, { target, lag }) =
|
|
|
29
29
|
|
|
30
30
|
verticalOhlcv[keyName] = new Float64Array(len).fill(NaN)
|
|
31
31
|
|
|
32
|
+
if(priceBased.has(target)){
|
|
33
|
+
priceBased.add(keyName)
|
|
34
|
+
}
|
|
35
|
+
|
|
32
36
|
if(lag > 0)
|
|
33
37
|
{
|
|
34
38
|
main.lag([keyName], lag)
|
|
@@ -2,7 +2,7 @@ import { FasterSMA } from 'trading-signals';
|
|
|
2
2
|
// Variable Index Dynamic Average (VIDYA) indicator with volume stats
|
|
3
3
|
|
|
4
4
|
export const vidya = (main, index, size, momentum, options = {}) => {
|
|
5
|
-
const { verticalOhlcv, instances, len } = main;
|
|
5
|
+
const { verticalOhlcv, instances, len, priceBased } = main;
|
|
6
6
|
const { target, lag, atrLength = 200, bandDistance = 2, liquidityLookback = 5 } = options
|
|
7
7
|
const indicatorKey = `${size}_${momentum}`;
|
|
8
8
|
|
|
@@ -23,6 +23,12 @@ export const vidya = (main, index, size, momentum, options = {}) => {
|
|
|
23
23
|
liquidity: `${prefix}_liquidity`,
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
const priceBasedVidyaKeys = [keyBase, keys.upper, keys.lower, keys.liquidity]
|
|
27
|
+
|
|
28
|
+
for(const k of priceBasedVidyaKeys) {
|
|
29
|
+
priceBased.add(k)
|
|
30
|
+
}
|
|
31
|
+
|
|
26
32
|
// Prepare instances container
|
|
27
33
|
if (!instances.vidya) instances.vidya = { settings: {} };
|
|
28
34
|
instances.vidya.settings[indicatorKey] = {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const volumeDelta = (main, index, { lag = 0 }) => {
|
|
2
|
+
const { verticalOhlcv, instances, len } = main;
|
|
3
|
+
const groupName = 'volume_delta';
|
|
4
|
+
|
|
5
|
+
if (index === 0) {
|
|
6
|
+
if (!instances[groupName]) instances[groupName] = { isBuyVolume: true, cross: 0 };
|
|
7
|
+
|
|
8
|
+
const keys = [
|
|
9
|
+
`${groupName}_open`,
|
|
10
|
+
`${groupName}_high`,
|
|
11
|
+
`${groupName}_low`,
|
|
12
|
+
`${groupName}_close`,
|
|
13
|
+
`${groupName}_cross`,
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// allocate outputs
|
|
17
|
+
for (const k of keys) verticalOhlcv[k] = new Float64Array(len).fill(NaN);
|
|
18
|
+
|
|
19
|
+
// optional lag
|
|
20
|
+
if (lag > 0) main.lag(keys, lag);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const inst = instances[groupName];
|
|
24
|
+
|
|
25
|
+
// inputs
|
|
26
|
+
const vol = verticalOhlcv.volume[index];
|
|
27
|
+
const open = verticalOhlcv.open[index];
|
|
28
|
+
const close = verticalOhlcv.close[index];
|
|
29
|
+
const prevClose = index > 0 ? verticalOhlcv.close[index - 1] : NaN;
|
|
30
|
+
|
|
31
|
+
// TradingView tie-breaks:
|
|
32
|
+
// 1) close>open -> buy, 2) close<open -> sell,
|
|
33
|
+
// 3) equal: compare to prev close, 4) still equal: carry prior state
|
|
34
|
+
let isBuy = inst.isBuyVolume;
|
|
35
|
+
if (close > open) isBuy = true;
|
|
36
|
+
else if (close < open) isBuy = false;
|
|
37
|
+
else if (close > prevClose) isBuy = true;
|
|
38
|
+
else if (close < prevClose) isBuy = false;
|
|
39
|
+
|
|
40
|
+
inst.isBuyVolume = isBuy;
|
|
41
|
+
|
|
42
|
+
if(index < 1)
|
|
43
|
+
{
|
|
44
|
+
if(isBuy) inst.cross = 1
|
|
45
|
+
else inst.cross = -1
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
if(isBuy)
|
|
49
|
+
{
|
|
50
|
+
if(inst.cross > 0) inst.cross = inst.cross + 1
|
|
51
|
+
else inst.cross = 1 //reset
|
|
52
|
+
} else {
|
|
53
|
+
if(inst.cross > 0) inst.cross = -1 //reset
|
|
54
|
+
else inst.cross = inst.cross - 1
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
const delta = Number.isFinite(vol) ? (isBuy ? vol : -vol) : NaN;
|
|
60
|
+
|
|
61
|
+
// delta "candle"
|
|
62
|
+
const openVal = Number.isNaN(delta) ? NaN : 0;
|
|
63
|
+
const closeVal = delta;
|
|
64
|
+
const highVal = Number.isNaN(delta) ? NaN : Math.max(delta, 0);
|
|
65
|
+
const lowVal = Number.isNaN(delta) ? NaN : Math.min(delta, 0);
|
|
66
|
+
|
|
67
|
+
main.pushToMain({ index, key: `${groupName}_open`, value: openVal });
|
|
68
|
+
main.pushToMain({ index, key: `${groupName}_high`, value: highVal });
|
|
69
|
+
main.pushToMain({ index, key: `${groupName}_low`, value: lowVal });
|
|
70
|
+
main.pushToMain({ index, key: `${groupName}_close`, value: closeVal });
|
|
71
|
+
main.pushToMain({ index, key: `${groupName}_cross`, value: inst.cross });
|
|
72
|
+
|
|
73
|
+
return true;
|
|
74
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { oneHotEncode } from "../machine-learning/ml-utilities.js"
|
|
2
2
|
import { buildArray } from "../utilities/assignTypes.js"
|
|
3
|
+
import { addPrecisionAsNumber } from "../utilities/precisionMultiplier.js"
|
|
3
4
|
const eq = (fast, slow) => fast === slow
|
|
4
5
|
const gt = (fast, slow) => fast > slow
|
|
5
6
|
const lt = (fast, slow) => fast < slow
|
|
@@ -134,7 +135,7 @@ class CrossInstance {
|
|
|
134
135
|
|
|
135
136
|
export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
|
|
136
137
|
|
|
137
|
-
const {verticalOhlcv, verticalOhlcvTempCols, instances, len} = main
|
|
138
|
+
const {verticalOhlcv, verticalOhlcvTempCols, instances, len, precision, precisionMultiplier, priceBased} = main
|
|
138
139
|
|
|
139
140
|
for (const { fast, slow } of crossPairsList) {
|
|
140
141
|
|
|
@@ -147,8 +148,15 @@ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
|
|
|
147
148
|
|
|
148
149
|
const col = slow.toString()
|
|
149
150
|
verticalOhlcvTempCols.add(col)
|
|
150
|
-
|
|
151
|
-
|
|
151
|
+
|
|
152
|
+
const requiresPrecision = precision && (fast === 'price' || priceBased.has(fast))
|
|
153
|
+
|
|
154
|
+
const slowVal = requiresPrecision
|
|
155
|
+
? addPrecisionAsNumber(col, precisionMultiplier)
|
|
156
|
+
: slow
|
|
157
|
+
|
|
158
|
+
const numArrType = (slow >= -125 && slow <= 125 && requiresPrecision === false) ? 'Int8Array' : 'Float64Array'
|
|
159
|
+
verticalOhlcv[col] = buildArray(numArrType, len, slowVal)
|
|
152
160
|
}
|
|
153
161
|
|
|
154
162
|
if(!instances.hasOwnProperty('crossPairs'))
|
|
@@ -163,7 +171,7 @@ export const crossPairs = (main, index, crossPairsList, {oneHot, limit}) => {
|
|
|
163
171
|
max: -Infinity
|
|
164
172
|
}
|
|
165
173
|
|
|
166
|
-
const crossArrType = (limit !== null && limit <= 125) ? 'Int8Array' : '
|
|
174
|
+
const crossArrType = (limit !== null && limit <= 125) ? 'Int8Array' : 'Float64Array'
|
|
167
175
|
|
|
168
176
|
verticalOhlcv[crossName] = buildArray(crossArrType, len, 0)
|
|
169
177
|
|
package/src/studies/lag.js
CHANGED
|
@@ -4,32 +4,37 @@ export const lag = (main, index, colKeys, lookback) => {
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
const { verticalOhlcv, len } = main
|
|
7
|
+
const { verticalOhlcv, len, priceBased } = main
|
|
8
8
|
|
|
9
9
|
if(index === 0)
|
|
10
10
|
{
|
|
11
|
-
for (const
|
|
12
|
-
if (!verticalOhlcv.hasOwnProperty(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
for (const targetKey of colKeys) {
|
|
12
|
+
if (!verticalOhlcv.hasOwnProperty(targetKey)) {
|
|
13
|
+
throw new Error(
|
|
14
|
+
`Lag processing invoked by col "${targetKey}" was not found in "verticalOhlcv".`
|
|
15
|
+
)
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
const addToPriceBased = priceBased.has(targetKey)
|
|
19
|
+
|
|
18
20
|
for (let step = 1; step <= lookback; step++) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
+
const key = `${targetKey}_lag_${step}`
|
|
22
|
+
|
|
23
|
+
if(addToPriceBased) priceBased.add(key)
|
|
24
|
+
|
|
25
|
+
const thisArrType = getArrayType(targetKey, verticalOhlcv[targetKey])
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
verticalOhlcv[key] = buildArray(thisArrType, len)
|
|
23
28
|
}
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
for (const
|
|
28
|
-
const currentColumn = verticalOhlcv[
|
|
32
|
+
for (const targetKey of colKeys) {
|
|
33
|
+
const currentColumn = verticalOhlcv[targetKey]
|
|
29
34
|
|
|
30
35
|
// Populate the lagged values each tick:
|
|
31
36
|
for (let step = 1; step <= lookback; step++) {
|
|
32
|
-
const key = `${
|
|
37
|
+
const key = `${targetKey}_lag_${step}`
|
|
33
38
|
const laggedIndex = index - step
|
|
34
39
|
const value =
|
|
35
40
|
laggedIndex < 0 || currentColumn[laggedIndex] === undefined
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { classifyNum } from "./numberUtilities.js";
|
|
2
2
|
import { selectDateFormatter } from "./dateUtilities.js";
|
|
3
3
|
|
|
4
|
-
const numberKeys = new Set(['open', 'high', 'low', 'close'])
|
|
5
|
-
|
|
6
4
|
export const buildArray = (arrayType, len, fallbackValue) => {
|
|
7
5
|
const ctorMap = {
|
|
8
6
|
Array: Array,
|
|
@@ -35,7 +33,7 @@ export const buildArray = (arrayType, len, fallbackValue) => {
|
|
|
35
33
|
}
|
|
36
34
|
|
|
37
35
|
export const assignTypes = main => {
|
|
38
|
-
const {firstRow} = main
|
|
36
|
+
const {firstRow, precision, initialPriceBased} = main
|
|
39
37
|
const inputTypes = {}
|
|
40
38
|
const arrayTypes = {}
|
|
41
39
|
|
|
@@ -50,12 +48,12 @@ export const assignTypes = main => {
|
|
|
50
48
|
inputTypes[colName] = classifyNum(cellValue, true)
|
|
51
49
|
arrayTypes[colName] = 'Int32Array'
|
|
52
50
|
}
|
|
53
|
-
else if(
|
|
51
|
+
else if(initialPriceBased.has(colName))
|
|
54
52
|
{
|
|
55
|
-
inputTypes[colName] = classifyNum(cellValue, true)
|
|
53
|
+
inputTypes[colName] = classifyNum(cellValue, true, precision)
|
|
56
54
|
arrayTypes[colName] = 'Float64Array'
|
|
57
55
|
} else {
|
|
58
|
-
const thisType = classifyNum(cellValue, false)
|
|
56
|
+
const thisType = classifyNum(cellValue, false, precision)
|
|
59
57
|
inputTypes[colName] = thisType
|
|
60
58
|
arrayTypes[colName] = (thisType === 'number') ? 'Float64Array' : 'Array'
|
|
61
59
|
}
|
|
@@ -1,52 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
export const sanitizeDirtyNumberString = str => str.replace(/(?!^-)[^0-9.]/g, '');
|
|
1
|
+
import { addPrecisionAsNumber, revertPrecisionAsNumber } from "./precisionMultiplier.js";
|
|
3
2
|
|
|
4
|
-
export const
|
|
3
|
+
export const inputNumberFormatter = {
|
|
5
4
|
number: n => n,
|
|
6
5
|
numberCleanString: n => Number(n),
|
|
7
|
-
|
|
6
|
+
precisionNumberCleanString: (strNum, multiplier) => addPrecisionAsNumber(strNum, multiplier)
|
|
8
7
|
}
|
|
9
8
|
|
|
9
|
+
export const outputNumberFormatter = {
|
|
10
|
+
number: n => n,
|
|
11
|
+
numberCleanString: n => n,
|
|
12
|
+
precisionNumberCleanString: (strNum, multiplier) => revertPrecisionAsNumber(strNum, multiplier)
|
|
13
|
+
}
|
|
10
14
|
|
|
11
|
-
export const classifyNum = (num, throwError = false) => {
|
|
12
|
-
// 1) true numbers
|
|
13
|
-
if (typeof num === 'number' && !Number.isNaN(num)) {
|
|
14
|
-
return 'number';
|
|
15
|
-
}
|
|
15
|
+
export const classifyNum = (num, throwError = false, precision = false) => {
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
const cleanRe = /^-?\d+(\.\d+)?$/;
|
|
18
|
+
const isNumber = typeof num === 'number' && !Number.isNaN(num)
|
|
19
|
+
const isString = typeof num === 'string'
|
|
20
|
+
const isCleanString = isString && cleanRe.test(num)
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
if
|
|
24
|
-
return 'numberCleanString';
|
|
25
|
-
}
|
|
22
|
+
if(precision === true) {
|
|
23
|
+
if(isCleanString) return 'precisionNumberCleanString'
|
|
26
24
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const stripped = str.replace(/[^0-9.]/g, '');
|
|
30
|
-
if (stripped && cleanRe.test(stripped)) {
|
|
31
|
-
return 'numberDirtyString';
|
|
32
|
-
}
|
|
25
|
+
if(throwError) throw new Error('Invalid number forma')
|
|
26
|
+
return typeof num
|
|
33
27
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{
|
|
38
|
-
throw new TypeError(
|
|
39
|
-
'Invalid input: expected a number or numeric string'
|
|
40
|
-
);
|
|
28
|
+
else {
|
|
29
|
+
if(isNumber) return 'number'
|
|
30
|
+
else if(isCleanString) return 'numberCleanString'
|
|
41
31
|
}
|
|
42
32
|
|
|
33
|
+
if(throwError) throw new TypeError('Invalid input: expected a number or numeric string')
|
|
43
34
|
return typeof num
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
export const roundDecimalPlaces = (value, decimals) => {
|
|
48
|
-
|
|
49
|
-
const factor = 10 ** decimals
|
|
50
|
-
// add a tiny epsilon to mitigate floating-point quirks around .5
|
|
51
|
-
return Math.round((value + Number.EPSILON) * factor) / factor
|
|
52
|
-
};
|
|
35
|
+
}
|
|
@@ -1,23 +1,98 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// ------------------------------
|
|
2
|
+
// calcPrecisionMultiplier (unchanged semantics)
|
|
3
|
+
// ------------------------------
|
|
4
|
+
export const calcPrecisionMultiplier = main => {
|
|
5
|
+
const { precision, firstRow, initialPriceBased } = main;
|
|
6
|
+
if (precision === false) return 1
|
|
3
7
|
|
|
4
|
-
|
|
8
|
+
const multipliers = []
|
|
5
9
|
|
|
6
|
-
|
|
10
|
+
const err = 'If "precision" is set to true all the "open", "high", "low" and "close" values must be numeric strings like "123.45" with no commas or currency symbols.'
|
|
7
11
|
|
|
8
|
-
|
|
9
|
-
{
|
|
10
|
-
if(new Set(['open', 'high', 'low', 'close']).has(key) === false) continue
|
|
11
|
-
const [, decimals = ''] = String(value).split('.')
|
|
12
|
-
const decimalPrecision = Math.max(4, decimals.length)
|
|
13
|
-
const multiplier = decimalPrecision > 1 ? Math.pow(10, decimalPrecision - 1) : 1
|
|
12
|
+
for (const [key, strNum] of Object.entries(firstRow)) {
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
if(!initialPriceBased.has(key)) continue
|
|
15
|
+
|
|
16
|
+
if (typeof strNum !== 'string' || !/^-?\d+(\.\d+)?$/.test(strNum)) {
|
|
17
|
+
throw new Error(`${err}
|
|
18
|
+
Property "${key}" had the value "${strNum}" type ${typeof strNum}.`)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
if (!new Set(['open', 'high', 'low', 'close']).has(key)) continue;
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
const [, decimals = ''] = strNum.split('.')
|
|
24
|
+
const decimalPrecision = Math.max(4, decimals.length)
|
|
25
|
+
multipliers.push(Math.pow(10, decimalPrecision)) // returns Number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (multipliers.length === 0) return 1
|
|
29
|
+
return Math.max(...multipliers)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ------------------------------
|
|
33
|
+
// Precise BigInt-based helpers (no `n` literals)
|
|
34
|
+
// ------------------------------
|
|
35
|
+
export const addPrecisionExact = (strNum, mul) => {
|
|
36
|
+
if (typeof strNum !== 'string') throw new Error('addPrecisionExact expects a string when precision=true')
|
|
37
|
+
|
|
38
|
+
const negative = strNum.charAt(0) === '-'
|
|
39
|
+
const s = negative ? strNum.slice(1) : strNum
|
|
40
|
+
const [intPart, decPart = ''] = s.split('.')
|
|
41
|
+
|
|
42
|
+
// Compute decimals from mul (mul expected as 10^decimals)
|
|
43
|
+
const mulStr = String(mul);
|
|
44
|
+
const decimals = (mul === 1) ? 0 : (mulStr.length - 1)
|
|
45
|
+
|
|
46
|
+
const decPadded = decPart.padEnd(decimals, '0').slice(0, decimals); // pad or trim
|
|
47
|
+
const integerString = intPart + decPadded // e.g. "12" + "3400" => "123400"
|
|
48
|
+
|
|
49
|
+
// Use BigInt constructor (no n-literals)
|
|
50
|
+
let result = BigInt(integerString || '0'); // handle "0" or empty
|
|
51
|
+
if (negative) result = -result
|
|
52
|
+
return result
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const revertPrecisionExact = (num, mul) => {
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
// Convert input to BigInt without using literal suffixes
|
|
59
|
+
const big = (typeof num === 'bigint') ? num : BigInt(parseInt(num))
|
|
60
|
+
const neg = big < BigInt(0)
|
|
61
|
+
const abs = neg ? -big : big
|
|
62
|
+
|
|
63
|
+
const mulBig = BigInt(mul);
|
|
64
|
+
if (mulBig === BigInt(1)) {
|
|
65
|
+
const s = abs.toString()
|
|
66
|
+
return neg ? `-${s}` : s
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const intPart = abs / mulBig
|
|
70
|
+
const fracPart = abs % mulBig
|
|
71
|
+
|
|
72
|
+
const decimals = String(mul).length - 1
|
|
73
|
+
const fracStr = fracPart.toString().padStart(decimals, '0')
|
|
74
|
+
|
|
75
|
+
// Remove trailing zeros in fractional part (optional)
|
|
76
|
+
const trimmedFrac = fracStr.replace(/0+$/, '')
|
|
77
|
+
const result = trimmedFrac.length === 0
|
|
78
|
+
? intPart.toString()
|
|
79
|
+
: `${intPart.toString()}.${trimmedFrac}`
|
|
80
|
+
|
|
81
|
+
return neg ? `-${result}` : result
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ------------------------------
|
|
85
|
+
// Convenience wrappers (Numbers) — may lose precision
|
|
86
|
+
// ------------------------------
|
|
87
|
+
export const addPrecisionAsNumber = (strNum, mul) => {
|
|
88
|
+
|
|
89
|
+
const big = addPrecisionExact(strNum, mul)
|
|
90
|
+
|
|
91
|
+
return Number(big)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const revertPrecisionAsNumber = (num, mul) => {
|
|
95
|
+
|
|
96
|
+
const asStr = revertPrecisionExact(num, mul)
|
|
97
|
+
return Number(asStr)
|
|
98
|
+
}
|
|
@@ -16,7 +16,7 @@ export const validateInputParams = (inputParams, len) => {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export const isAlreadyComputed = main => {
|
|
19
|
-
if(main.isComputed === true) throw Error('ohlcv is already computed, you can not add new indicators after "compute", "getLastValues"
|
|
19
|
+
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
20
|
}
|
|
21
21
|
|
|
22
22
|
export const validateArray = (arr, paramName, callerName) => {
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { dateOutputFormaters } from "./dateUtilities.js"
|
|
2
|
+
import { outputNumberFormatter } from "./numberUtilities.js"
|
|
2
3
|
|
|
3
4
|
export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, dateFormat}) => {
|
|
4
5
|
|
|
5
|
-
const {verticalOhlcv, invalidValueIndex, len, verticalOhlcvKeyNames, verticalOhlcvTempCols} = main
|
|
6
|
+
const {verticalOhlcv, invalidValueIndex, len, verticalOhlcvKeyNames, verticalOhlcvTempCols, priceBased, precision, precisionMultiplier} = main
|
|
7
|
+
|
|
8
|
+
|
|
6
9
|
|
|
7
10
|
if (verticalOhlcvKeyNames.length === 0) return []
|
|
8
11
|
|
|
@@ -10,8 +13,18 @@ export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, da
|
|
|
10
13
|
const maxStartIndex = Math.max(skipNullIndex, startIndex)
|
|
11
14
|
const diffLen = len - maxStartIndex
|
|
12
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
|
+
}
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
for(const [key, arr] of Object.entries(verticalOhlcv)){
|
|
15
28
|
if(verticalOhlcvTempCols.has(key)) continue
|
|
16
29
|
|
|
17
30
|
for (let i = maxStartIndex; i < len; i++)
|
|
@@ -22,7 +35,7 @@ export const verticalToHorizontal = ({main, skipNull = false, startIndex = 0, da
|
|
|
22
35
|
}
|
|
23
36
|
else
|
|
24
37
|
{
|
|
25
|
-
result[i - maxStartIndex][key] = arr[i]
|
|
38
|
+
result[i - maxStartIndex][key] = formatter[key](arr[i], precisionMultiplier)
|
|
26
39
|
}
|
|
27
40
|
}
|
|
28
41
|
}
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { FasterSMA } from "trading-signals";
|
|
2
|
-
|
|
3
|
-
// Normalize the array (z-score normalization)
|
|
4
|
-
const smoothNormalize = (arr, smoothing) => {
|
|
5
|
-
const len = arr.length;
|
|
6
|
-
const instance = new FasterSMA(smoothing);
|
|
7
|
-
const smoothedArr = new Array(len);
|
|
8
|
-
let sum = 0;
|
|
9
|
-
let sumSq = 0;
|
|
10
|
-
let count = 0;
|
|
11
|
-
|
|
12
|
-
for (let x = 0; x < len; x++) {
|
|
13
|
-
instance.update(arr[x]);
|
|
14
|
-
|
|
15
|
-
if (instance.isStable) {
|
|
16
|
-
const value = instance.getResult();
|
|
17
|
-
smoothedArr[x] = value;
|
|
18
|
-
sum += value;
|
|
19
|
-
sumSq += value * value;
|
|
20
|
-
count++;
|
|
21
|
-
} else {
|
|
22
|
-
smoothedArr[x] = null;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const meanValue = sum / count;
|
|
27
|
-
const variance = (sumSq / count) - (meanValue * meanValue);
|
|
28
|
-
const stdDev = Math.sqrt(variance);
|
|
29
|
-
|
|
30
|
-
return smoothedArr.map(val => (val !== null ? (val - meanValue) / stdDev : null));
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Calculate Pearson Correlation
|
|
34
|
-
export const correlation = (x, y, smoothing = 5) => {
|
|
35
|
-
if (x.length !== y.length) {
|
|
36
|
-
throw new Error('Both arrays must have the same length');
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const normX = smoothNormalize(x, smoothing);
|
|
40
|
-
const normY = smoothNormalize(y, smoothing);
|
|
41
|
-
const n = x.length;
|
|
42
|
-
const result = [];
|
|
43
|
-
|
|
44
|
-
let sumX = 0, sumY = 0, sumXY = 0;
|
|
45
|
-
let sumXSquare = 0, sumYSquare = 0;
|
|
46
|
-
let count = 0;
|
|
47
|
-
|
|
48
|
-
// Find the first index where both normX and normY have valid values
|
|
49
|
-
let firstValidIndex = -1;
|
|
50
|
-
for (let i = 0; i < n; i++) {
|
|
51
|
-
if (normX[i] !== null && normY[i] !== null) {
|
|
52
|
-
firstValidIndex = i;
|
|
53
|
-
break;
|
|
54
|
-
} else {
|
|
55
|
-
result.push(0); // Fill initial invalid indices with 0
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (firstValidIndex === -1) {
|
|
60
|
-
// No valid data points
|
|
61
|
-
return new Array(n).fill(0);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
for (let i = firstValidIndex; i < n; i++) {
|
|
65
|
-
const xVal = normX[i];
|
|
66
|
-
const yVal = normY[i];
|
|
67
|
-
|
|
68
|
-
if (xVal === null || yVal === null) {
|
|
69
|
-
// Reset sums and counts when encountering null values
|
|
70
|
-
sumX = sumY = sumXY = sumXSquare = sumYSquare = count = 0;
|
|
71
|
-
result.push(0);
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
sumX += xVal;
|
|
76
|
-
sumY += yVal;
|
|
77
|
-
sumXY += xVal * yVal;
|
|
78
|
-
sumXSquare += xVal * xVal;
|
|
79
|
-
sumYSquare += yVal * yVal;
|
|
80
|
-
count++;
|
|
81
|
-
|
|
82
|
-
if (count < 2) {
|
|
83
|
-
result.push(0); // Not enough data points to calculate correlation
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
const numerator = (count * sumXY) - (sumX * sumY);
|
|
88
|
-
const denominator = Math.sqrt(
|
|
89
|
-
(count * sumXSquare - sumX * sumX) *
|
|
90
|
-
(count * sumYSquare - sumY * sumY)
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
const correlation = denominator === 0 ? 0 : numerator / denominator;
|
|
94
|
-
result.push(correlation);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return result;
|
|
98
|
-
}
|