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 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, decimals = null} = options
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, decimals}]});
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, decimals = null} = options;
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, decimals}] });
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.53",
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 { numberFormater } from "../utilities/numberUtilities.js";
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(numberFormater.hasOwnProperty(formaterKey))
81
+ if(inputNumberFormatter.hasOwnProperty(formaterKey))
79
82
  {
80
- const formatedValue = numberFormater[formaterKey](value)
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(typeof val === 'undefined' || val === null || Number.isNaN(val))
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, decimals }) => {
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: (decimals === null) ? heightValue : roundDecimalPlaces(heightValue, decimals) });
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: (decimals === null) ? rangeValue : roundDecimalPlaces(rangeValue, decimals) });
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, decimals } = options
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: (decimals === null) ? heightVal : roundDecimalPlaces(heightVal, decimals) });
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: (decimals === null) ? val : roundDecimalPlaces(val, decimals) });
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
- const numArrType = (slow >= -125 || slow <= 125) ? 'Int8Array' : 'Int16Array'
151
- verticalOhlcv[col] = buildArray(numArrType, len, slow)
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' : 'Int16Array'
174
+ const crossArrType = (limit !== null && limit <= 125) ? 'Int8Array' : 'Float64Array'
167
175
 
168
176
  verticalOhlcv[crossName] = buildArray(crossArrType, len, 0)
169
177
 
@@ -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 colKey of colKeys) {
12
- if (!verticalOhlcv.hasOwnProperty(colKey)) {
13
- throw new Error(
14
- `Lag processing invoked by col "${colKey}" was not found in "verticalOhlcv".`
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
- const key = `${colKey}_lag_${step}`
20
- const thisArrType = getArrayType(colKey, verticalOhlcv[colKey])
21
+ const key = `${targetKey}_lag_${step}`
22
+
23
+ if(addToPriceBased) priceBased.add(key)
24
+
25
+ const thisArrType = getArrayType(targetKey, verticalOhlcv[targetKey])
21
26
 
22
- verticalOhlcv[key] = buildArray(thisArrType, len)
27
+ verticalOhlcv[key] = buildArray(thisArrType, len)
23
28
  }
24
29
  }
25
30
  }
26
31
 
27
- for (const colKey of colKeys) {
28
- const currentColumn = verticalOhlcv[colKey]
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 = `${colKey}_lag_${step}`
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(numberKeys.has(colName))
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
- // Helper to clean non-numeric characters (except "-" at the start and decimal point)
2
- export const sanitizeDirtyNumberString = str => str.replace(/(?!^-)[^0-9.]/g, '');
1
+ import { addPrecisionAsNumber, revertPrecisionAsNumber } from "./precisionMultiplier.js";
3
2
 
4
- export const numberFormater = {
3
+ export const inputNumberFormatter = {
5
4
  number: n => n,
6
5
  numberCleanString: n => Number(n),
7
- numberDirtyString: n => Number(sanitizeDirtyNumberString(n))
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
- // 2) strings
18
- if (typeof num === 'string') {
19
- const str = num.trim();
20
- const cleanRe = /^-?\d+(\.\d+)?$/;
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
- // a) clean numeric string: only digits and an optional single period
23
- if (cleanRe.test(str)) {
24
- return 'numberCleanString';
25
- }
22
+ if(precision === true) {
23
+ if(isCleanString) return 'precisionNumberCleanString'
26
24
 
27
- // b) dirty numeric string: contains non-digits (dollar, commas, letters, etc.)
28
- // but once you strip those out, you're left with a valid number
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
- if(throwError)
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
- export const calcPrecisionMultiplier = (main, firstRow) => {
2
- const {precision} = main
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
- if(precision === false) return 1
8
+ const multipliers = []
5
9
 
6
- let output = 1
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
- for(const [key, value] of Object.entries(firstRow))
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
- if(multiplier > output)
16
- {
17
- output = multiplier
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
- return output
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" , "getDataAsCols" or "getData" methods are called.')
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
- for(const [key, arr] of Object.entries(verticalOhlcv)){
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
- }