ohlcv-indicators 3.5.55 → 3.5.59

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.
@@ -0,0 +1,78 @@
1
+
2
+ const getRet = (next, prev) => (next - prev) / prev
3
+ const isBadNumber = (v) => v == null || !Number.isFinite(v)
4
+
5
+ export const priceFeatures = (main, index, {lag, colKeys}) => {
6
+ const { verticalOhlcv } = main
7
+
8
+ if(index === 0) {
9
+ const {len} = main
10
+
11
+ const newCols = {
12
+ ret_change: new Float64Array(len).fill(NaN),
13
+ ret_upper_wick: new Float64Array(len).fill(NaN),
14
+ ret_lower_wick: new Float64Array(len).fill(NaN),
15
+ ret_gap: new Float64Array(len).fill(NaN),
16
+ ret_body: new Float64Array(len).fill(NaN),
17
+ ret_range: new Float64Array(len).fill(NaN)
18
+ }
19
+
20
+ for(const target of colKeys) {
21
+ if(!verticalOhlcv.hasOwnProperty(target)) {
22
+ throw new Error(
23
+ `Target property ${target} not found in verticalOhlcv for "priceFeatures".`
24
+ );
25
+ }
26
+
27
+ newCols[`ret_${target}`] = new Float64Array(len).fill(NaN)
28
+ }
29
+
30
+ Object.assign(verticalOhlcv, newCols)
31
+
32
+ if(lag) {
33
+ main.lag(Object.keys(newCols), lag)
34
+ }
35
+ }
36
+
37
+ const prevClose = verticalOhlcv.close[index - 1]
38
+
39
+ if(typeof prevClose === 'undefined') return
40
+
41
+ const currOpen = verticalOhlcv.open[index]
42
+ const currHigh = verticalOhlcv.high[index]
43
+ const currLow = verticalOhlcv.low[index]
44
+ const currClose = verticalOhlcv.close[index]
45
+
46
+ const upperWickTop = Math.max(currOpen, currClose)
47
+ const lowerWickTop = Math.min(currOpen, currClose)
48
+
49
+ const row = {
50
+ ret_change: getRet(currClose, prevClose),
51
+ ret_upper_wick: getRet(currHigh, upperWickTop),
52
+ ret_lower_wick: getRet(lowerWickTop, currLow),
53
+ ret_gap: getRet(currOpen, prevClose),
54
+ ret_body: getRet(currClose, currOpen),
55
+ ret_range: getRet(currHigh, currLow)
56
+ }
57
+
58
+ let hasInvalidTarget = false
59
+
60
+ for(const target of colKeys) {
61
+ const targetVal = verticalOhlcv[target][index]
62
+
63
+ if(isBadNumber(targetVal)) {
64
+ hasInvalidTarget = true
65
+ break
66
+ }
67
+
68
+ const targetRet = getRet(currClose, targetVal)
69
+ row[`ret_${target}`] = targetRet
70
+ }
71
+
72
+ if(hasInvalidTarget) return
73
+
74
+ for(const [key, value] of Object.entries(row)) {
75
+ main.pushToMain({index, key, value})
76
+ }
77
+
78
+ }
@@ -1,5 +1,5 @@
1
1
  export const dateOutputFormaters = {
2
- string: d => d.toISOString(),
2
+ string: d => d.toString(),
3
3
  milliseconds: d => d.getTime(),
4
4
  seconds: d => d.getTime() / 1000,
5
5
  object: d => d
@@ -1,39 +1,56 @@
1
1
 
2
- //calcPrecisionMultiplier is calculated once, using the open, high, low and close values of the first row
3
- //the multiplier (min 10000 and max 1000000000) is store in constructor
2
+ //calcPrecisionMultiplier is calculated once in the class constructor, using the open, high, low and close values of the first row
3
+ //the multiplier (min 10000) is store in constructor
4
4
 
5
5
  export const calcPrecisionMultiplier = main => {
6
- const { precision, firstRow, initialPriceBased, priceBased } = main;
6
+ const { precision, firstRow, initialPriceBased } = main
7
7
  if (precision === false) return 1
8
8
 
9
- const multipliers = []
10
-
11
- 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.'
9
+ let maxMultiplier = 1
12
10
 
13
11
  for (const [key, strNum] of Object.entries(firstRow)) {
12
+ if (!initialPriceBased.has(key)) continue
14
13
 
15
- if(!initialPriceBased.has(key)) continue
16
-
17
- if (typeof strNum !== 'string' || !/^-?\d+(\.\d+)?$/.test(strNum)) {
18
- throw new Error(`${err}
19
- Property "${key}" had the value "${strNum}" type ${typeof strNum}.`)
14
+ if (typeof strNum !== 'string' || !/^\d+(\.\d+)?$/.test(strNum)) {
15
+ throw new Error(
16
+ `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. Property "${key}" had the value "${strNum}" type ${typeof strNum}.`
17
+ )
20
18
  }
21
19
 
22
- if (!priceBased.has(key)) continue
23
-
24
20
  const [integerStr, decimals = ''] = strNum.split('.')
25
21
  const integer = Number(integerStr)
26
- const decimalPrecision = integer > 0 ? 4 : Math.max(4, decimals.length)
27
- multipliers.push(Math.pow(10, decimalPrecision))
28
- }
29
22
 
30
- if (multipliers.length === 0) return 1
23
+ let decimalPrecision = 4
24
+
25
+ // Treat any non-zero integer (positive or negative) as "normal"
26
+ if (Math.abs(integer) === 0) {
27
+ const decimalsLen = decimals.length
28
+ const countDecimals = Math.max(4, decimalsLen)
29
+
30
+ // index of first non-zero decimal digit, -1 if none
31
+ const firstNonZeroIndex = decimals.search(/[1-9]/)
32
+
33
+ if (firstNonZeroIndex === -1) {
34
+ // 0, 0.0, 0.0000... -> nothing special to preserve
35
+ decimalPrecision = countDecimals
36
+ } else {
37
+ // Give extra headroom after the first non-zero digit
38
+ const extra = 5 // tweak as you like
39
+ const needed = firstNonZeroIndex + 1 + extra
40
+ decimalPrecision = Math.max(countDecimals, needed)
41
+ }
42
+ }
31
43
 
32
- const maxMultiplier = 10 ** 9 //1000000000
44
+ // Optional: cap exponent to keep numbers sane
45
+ const multiplier = 10 ** decimalPrecision
33
46
 
34
- return Math.min(maxMultiplier, Math.max(...multipliers))
47
+ if (multiplier > maxMultiplier) maxMultiplier = multiplier
48
+ }
49
+
50
+ return maxMultiplier
35
51
  }
36
52
 
53
+
37
54
  //this function converts the strNum into a integer * multiplier
38
55
  export const addPrecisionAsNumber = (strNum, multiplier) => {
39
56
  if (typeof strNum !== 'string') throw new Error('addPrecisionExact expects a string when precision=true')
@@ -1,65 +0,0 @@
1
- export const countUniqueLabels = Y => {
2
- // 1. Guard against empty or non-array input
3
- if (!Array.isArray(Y) || Y.length === 0) {
4
- return 1
5
- }
6
-
7
- // 2. Detect if Y is 2D or 1D
8
- const is2D = Array.isArray(Y[0])
9
- const nCols = is2D ? Y[0].length : 1
10
-
11
- let total = 0
12
-
13
- // 3. For each column, collect uniques in a single pass
14
- for (let col = 0; col < nCols; col++) {
15
- const seen = new Set()
16
- for (let row = 0; row < Y.length; row++) {
17
- // pick the cell: Y[row][col] if 2D, or Y[row] if 1D
18
- const cell = is2D ? Y[row][col] : Y[row]
19
- seen.add(cell)
20
- }
21
- total += seen.size
22
- }
23
-
24
- // 4. Return “sum of uniques per column”
25
- return total
26
- }
27
-
28
- // X and Y can have multivariable or univariable rows
29
- export const oversampleXY = (X, Y) => {
30
- if (!Array.isArray(X) || !Array.isArray(Y)) {
31
- throw new TypeError('Both X and Y must be arrays');
32
- }
33
- if (X.length !== Y.length) {
34
- throw new Error('X and Y must have the same length');
35
- }
36
- if (Y.length === 0) {
37
- return { X: [], Y: [] };
38
- }
39
-
40
- // 2) Group X-values by label using a Map
41
- const groups = Y.reduce((map, label, i) => {
42
- if (!map.has(label)) map.set(label, []);
43
- map.get(label).push(X[i]);
44
- return map;
45
- }, new Map());
46
-
47
- // 3) Find the largest group size
48
- let maxSize = 0;
49
- for (const arr of groups.values()) {
50
- if (arr.length > maxSize) maxSize = arr.length;
51
- }
52
-
53
- // 4) Build the oversampled arrays
54
- const outX = [];
55
- const outY = [];
56
- for (const [label, xArr] of groups.entries()) {
57
- const sz = xArr.length;
58
- for (let i = 0; i < maxSize; i++) {
59
- outX.push(xArr[i % sz]);
60
- outY.push(label);
61
- }
62
- }
63
-
64
- return { X: outX, Y: outY };
65
- };
@@ -1,321 +0,0 @@
1
- import { getFeaturedKeys, computeFlatFeaturesLen, logMlTraining, updateClassifierMetrics, findGroupsFunc } from "./ml-utilities.js"
2
- import { buildTrainX } from "./trainX.js"
3
- import { areKeyValuesValid } from "../core-functions/pushToMain.js"
4
- import { countUniqueLabels, oversampleXY } from "./balancing.js"
5
-
6
- const algo = 'classifier'
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
-
53
- export const classifier = (
54
- main,
55
- index,
56
- trainingSize,
57
- {
58
- yCallback,
59
- trainingCols,
60
- findGroups = [],
61
- retrain,
62
- type,
63
- modelArgs,
64
- horizon,
65
- precompute,
66
- predictions,
67
- filterCallback,
68
- modelConfig,
69
- modelClass
70
- }
71
- ) => {
72
- const { lookbackAbs, prefix, flatY } = precompute
73
- const { verticalOhlcv, len, instances, scaledGroups, invalidValueIndex, ML } = main
74
- const allModels = ML.models
75
- const startIndex = (invalidValueIndex + 1)
76
-
77
- // ─── INITIALIZATION ───────────────────────────────────────────────
78
- if(index === startIndex) {
79
-
80
- // prepare instance storage
81
- if (!instances.hasOwnProperty(algo)) instances[algo] = {}
82
- if(!instances[algo].hasOwnProperty(prefix)) instances[algo][prefix] = {}
83
-
84
- const expectedLoops = (flatY) ? predictions : 1 //
85
-
86
- instances[algo][prefix] = {
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
- }),
96
- expectedLoops,
97
- isTrained: new Array(expectedLoops).fill(false),
98
- uniqueLabels: new Array(expectedLoops).fill(0),
99
- shouldRetrain: retrain,
100
- featureCols: [],
101
- flatFeaturesColLen: 0,
102
- X: [],
103
- Y: Array.from({ length: expectedLoops }, () => []),
104
- }
105
-
106
- for (let i = 0; i < predictions; i++) {
107
- const predictionKey = `${prefix}_${i + 1}`
108
-
109
- verticalOhlcv[predictionKey] = (type === 'KNN') ? new Array(len).fill(null) : new Float32Array(len).fill(NaN)
110
- ML.metrics[predictionKey] = {accuracy: {}, total: 0, correct: 0, labels: {}}
111
-
112
- if(!allModels.hasOwnProperty(predictionKey))
113
- {
114
- allModels[predictionKey] = []
115
- }
116
- }
117
- }
118
- else if(index < startIndex || index < lookbackAbs)
119
- {
120
- return
121
- }
122
- else if(filterCallback(verticalOhlcv, index, instances[algo][prefix].store) === false)
123
- {
124
- return
125
- }
126
- else if(index + 1 === len)
127
- {
128
-
129
- if(instances[algo][prefix].featureCols.length === 0)
130
- {
131
- const inputFeatures = [...trainingCols, ...(findGroupsFunc(findGroups, scaledGroups))]
132
-
133
- throw new Error(`Some of the provided features in ${algo} "${prefix}" features where not found in "verticalOhlcv": ${JSON.stringify(inputFeatures)}`)
134
- }
135
-
136
- //last execution
137
- for(const featureKey of instances[algo][prefix].featureCols)
138
- {
139
- if(!verticalOhlcv.hasOwnProperty(featureKey)) throw new Error(`Feature "${featureKey}" not found in verticalOhlcv for ${prefix}.`)
140
- }
141
-
142
- if(instances[algo][prefix].X.length < trainingSize)
143
- {
144
- const requiredDatapoints = instances[algo][prefix].X.length - trainingSize
145
- throw new Error(`The current "trainingSize" at ${algo} "${prefix}" requires at least ${requiredDatapoints} more datapoints. Try adding more input ohlcv rows or reducing the "trainingSize" by ${requiredDatapoints}.`)
146
- }
147
-
148
- }
149
-
150
- const dataSetInstance = instances[algo][prefix]
151
-
152
- if(dataSetInstance.flatFeaturesColLen === 0)
153
- {
154
- dataSetInstance.featureCols = getFeaturedKeys({trainingCols, findGroups, verticalOhlcv, scaledGroups})
155
-
156
- if(areKeyValuesValid(main, index, dataSetInstance.featureCols))
157
- {
158
- dataSetInstance.flatFeaturesColLen = computeFlatFeaturesLen(dataSetInstance.featureCols, verticalOhlcv, index)
159
-
160
- logMlTraining({
161
- featureCols: dataSetInstance.featureCols,
162
- flatFeaturesColLen: dataSetInstance.flatFeaturesColLen,
163
- type,
164
- trainingSize
165
- })
166
- } else {
167
- return
168
- }
169
- }
170
-
171
- if(dataSetInstance.flatFeaturesColLen === 0) return
172
-
173
- const {
174
- uniqueLabels,
175
- expectedLoops,
176
- featureCols,
177
- flatFeaturesColLen,
178
- shouldRetrain,
179
- X: xRows
180
- } = dataSetInstance
181
-
182
- const trainX = buildTrainX({
183
- featureCols,
184
- flatFeaturesColLen,
185
- type,
186
- index,
187
- lookbackAbs,
188
- main
189
- })
190
-
191
- if(!trainX) return
192
-
193
- //if univariable Y (flatY) a model is created for each prediction
194
- const trainY = yCallback(index, verticalOhlcv, horizon, instances[algo][prefix].store)
195
-
196
- if (trainY !== null && (!Array.isArray(trainY) || trainY.length !== predictions)) {
197
-
198
- throw new Error(`trainY must be null or an array with ${predictions} items, got "${JSON.stringify(trainY)}" at index ${index} for ${type}`);
199
- }
200
-
201
- for(let loopIdx = 0; loopIdx < expectedLoops; loopIdx++)
202
- {
203
- const predictionKey = `${prefix}_${(loopIdx+1)}`
204
- const yRows = dataSetInstance.Y[loopIdx]
205
- const currTrainY = (flatY) ? (trainY === null) ? null : trainY[loopIdx] : trainY
206
- const isTrained = dataSetInstance.isTrained[loopIdx]
207
-
208
- const shouldPredict = allModels.hasOwnProperty(predictionKey) && isTrained && (shouldRetrain === false || allModels[predictionKey].length === horizon)
209
-
210
- //predicts using previously saved models even if current currTrainY is not available
211
- if(shouldPredict)
212
- {
213
- const futureRow = allModels[predictionKey][0].predict([trainX])[0]
214
-
215
- if(flatY)
216
- {
217
- if(futureRow == null) throw new Error(`Prediction of ${prefix} at index ${index} was expecting a number.`)
218
-
219
- main.pushToMain({index, key: predictionKey, value: futureRow})
220
-
221
- if(currTrainY != null)
222
- {
223
- updateClassifierMetrics({
224
- metrics: ML.metrics[predictionKey],
225
- trueLabel: currTrainY,
226
- predictedLabel: futureRow
227
- })
228
- }
229
- }
230
- else {
231
-
232
- if (!Array.isArray(futureRow) || futureRow.length !== predictions) {
233
- throw new Error(
234
- `Prediction output of ${algo} "${prefix}" at index ${index} was expecting an array of values.`
235
- )
236
- }
237
-
238
- for(let preIdx = 0; preIdx < predictions; preIdx++) {
239
- main.pushToMain({index, key: `${prefix}_${(preIdx+1)}`, value: futureRow[preIdx]})
240
-
241
- if(Array.isArray(currTrainY) && currTrainY.length === predictions)
242
- {
243
- updateClassifierMetrics({
244
- metrics: ML.metrics[`${prefix}_${(preIdx+1)}`],
245
- trueLabel: currTrainY[preIdx],
246
- predictedLabel: futureRow[preIdx]
247
- })
248
- }
249
-
250
- }
251
- }
252
- }
253
-
254
- if((index + predictions + 1) > len) continue
255
- if(currTrainY === null) continue // future not defined
256
-
257
- if(flatY)
258
- {
259
- if (typeof currTrainY === 'undefined' || currTrainY == null)
260
- {
261
- throw new Error(`currTrainY must return number, string or boolean got ${typeof currTrainY} at index ${index}`)
262
- }
263
- } else {
264
-
265
- if ((currTrainY.length !== predictions)) {
266
-
267
- throw new Error(
268
- `The number of label columns returned (${currTrainY.length}) doesn’t match the options.predictions (${predictions}) setting for the ${algo} "${prefix}" classifier. ` +
269
- `Please update your yCallback function so its output array items align with options.predictions.`
270
- );
271
-
272
- }
273
- }
274
-
275
- // enqueue
276
-
277
- yRows.push(currTrainY)
278
- if (yRows.length > trainingSize) yRows.shift()
279
-
280
- if(loopIdx === 0)
281
- {
282
- xRows.push(trainX)
283
- if (xRows.length > trainingSize) xRows.shift()
284
- }
285
-
286
- const shouldTrainModel = shouldRetrain || !isTrained
287
-
288
- if (shouldTrainModel && xRows.length === trainingSize && yRows.length === trainingSize) {
289
-
290
- if(isTrained === false)
291
- {
292
- uniqueLabels[loopIdx] = countUniqueLabels(yRows)
293
-
294
- if(uniqueLabels[loopIdx] < 1)
295
- {
296
- throw new Error(`Invalid number or labels in ${prefix}. Check the logic of your "yCallback" function.`)
297
- }
298
- }
299
-
300
-
301
- const {train} = modelConfig
302
-
303
- const {X: overX, Y: overY} = oversampleXY(xRows, yRows)
304
-
305
- const trainedModel = train({modelClass, xRows: overX, yRows: overY, modelArgs, uniqueLabels: uniqueLabels[loopIdx]})
306
-
307
- allModels[predictionKey].push(
308
- trainedModel
309
- )
310
-
311
- if(allModels[predictionKey].length > horizon)
312
- {
313
- allModels[predictionKey].shift()
314
- }
315
-
316
- dataSetInstance.isTrained[loopIdx] = true
317
- }
318
- }
319
-
320
- return true
321
- }