ohlcv-indicators 3.5.41 → 3.5.43

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,148 @@
1
+ export const oneHotEncode = (idx, size) => {
2
+ const vec = new Uint8Array(size)
3
+ vec[idx] = 1
4
+ return vec
5
+ }
6
+
7
+ export const logMlTraining = ({featureCols, flatFeaturesColLen, type, trainingSize}) => {
8
+
9
+ const oneHotTxt = (featureCols.length !== flatFeaturesColLen) ? ' (including flat one-hot encoded array values) ' : ''
10
+ console.log(`---\nInitialized "${type}" with ${flatFeaturesColLen} features ${oneHotTxt} and ${trainingSize} rows: \n${JSON.stringify(featureCols)}\n\n`)
11
+ }
12
+
13
+ export const normalizeMinMax = (value, min, max, [validMin, validMax]) => {
14
+ const clamped = Math.min(Math.max(value, min), max)
15
+ return ((clamped - min) / (max - min)) * (validMax - validMin) + validMin
16
+ }
17
+
18
+ export const normalizeZScore = (value, mean, std) => {
19
+ return std === 0 ? 0 : (value - mean) / std
20
+ }
21
+
22
+ export const findGroupsFunc = (findGroups, scaledGroups) => {
23
+
24
+ const output = []
25
+
26
+ if(findGroups.length > 0)
27
+ {
28
+ for(let g = 0; g < findGroups.length; g++)
29
+ {
30
+ const group = findGroups[g]
31
+ const {type, size = null, groupName = null} = group
32
+
33
+ let groupKey = (size !== null) ? `${type}_${size}` : `${type}_${groupName}`
34
+
35
+ if(!scaledGroups.hasOwnProperty(groupKey)) throw new Error(`Scaled group (${groupKey}) not found for regressor.options.findGroups[${g}]: ${JSON.stringify(group)}`)
36
+ output.push(...scaledGroups[groupKey])
37
+ }
38
+ }
39
+
40
+ return output
41
+ }
42
+
43
+ export const computeFlatFeaturesLen = (featureCols, verticalOhlcv, index) => {
44
+
45
+ let flatFeaturesColLen = 0
46
+
47
+ for (const key of featureCols) {
48
+
49
+ if(!verticalOhlcv.hasOwnProperty(key)) throw new Error(`Property "${key}" not found in "verticalOhlcv".`)
50
+
51
+ const val = verticalOhlcv[key][index]
52
+
53
+ if(typeof val === 'undefined' || val === null || Number.isNaN(val)) throw new Error(`Invalid value of property "${key}".`)
54
+
55
+ if (key.startsWith('one_hot_') || key.startsWith('pca_')) {
56
+
57
+ const colSize = verticalOhlcv[key][index].length
58
+
59
+ flatFeaturesColLen += colSize
60
+
61
+ } else {
62
+
63
+ flatFeaturesColLen += 1
64
+
65
+ }
66
+ }
67
+
68
+ return flatFeaturesColLen
69
+ }
70
+
71
+
72
+ export const countUniqueLabels = Y => {
73
+ // If Y[0] isn't an array, treat Y as a single-column 2D array
74
+ const data = Array.isArray(Y[0]) ? Y : Y.map(v => [v]);
75
+
76
+ // Number of columns in our (possibly wrapped) data
77
+ const nCols = data[0].length;
78
+
79
+ // Sum up the unique values in each column
80
+ const totalLabels = Array.from({ length: nCols }).reduce((sum, _, col) => {
81
+ const uniques = new Set(data.map(row => row[col]));
82
+ return sum + uniques.size;
83
+ }, 0);
84
+
85
+ // Return count + 1 (same as before)
86
+ return totalLabels + 1;
87
+ }
88
+
89
+
90
+ export const getFeaturedKeys = ({trainingCols, findGroups, verticalOhlcv, scaledGroups}) => {
91
+
92
+ const featureCols = [...trainingCols, ...(findGroupsFunc(findGroups, scaledGroups))]
93
+
94
+
95
+ if (featureCols.length === 0) {
96
+ return []
97
+ }
98
+
99
+ let keyNotFound = false
100
+
101
+ // sanity‐check that all features exist
102
+ for(let x = 0; x < featureCols.length; x++)
103
+ {
104
+ const featureKey = featureCols[x]
105
+
106
+ if(!verticalOhlcv.hasOwnProperty(featureKey)) {
107
+ keyNotFound = true
108
+ break
109
+ }
110
+ }
111
+
112
+ if(keyNotFound) return []
113
+
114
+ return featureCols
115
+ }
116
+
117
+ export const updateClassifierMetrics = ({ metrics, trueLabel, predictedLabel }) => {
118
+ // Increment overall counters
119
+ metrics.total = (metrics.total || 0) + 1
120
+ metrics.correct = (metrics.correct || 0)
121
+
122
+ // Initialize per‐label stats container if needed
123
+ metrics.labels = metrics.labels || {}
124
+
125
+ const labelKey = predictedLabel.toString()
126
+ // Ensure we have a slot for this label
127
+ if (!metrics.labels[labelKey]) {
128
+ metrics.labels[labelKey] = { total: 0, correct: 0, accuracy: 0 }
129
+ }
130
+
131
+ // Update this label’s total predictions
132
+ metrics.labels[labelKey].total++
133
+
134
+ // If correct, bump both overall and per-label correct counters
135
+ if (predictedLabel === trueLabel) {
136
+ metrics.correct++
137
+ metrics.labels[labelKey].correct++
138
+ }
139
+
140
+ // Recompute overall accuracy
141
+ metrics.accuracy = metrics.correct / metrics.total
142
+
143
+ // Recompute accuracy for each label
144
+ for (const key in metrics.labels) {
145
+ const stats = metrics.labels[key]
146
+ stats.accuracy = stats.correct / stats.total
147
+ }
148
+ }
@@ -1,105 +1,96 @@
1
- import { PCA } from "ml-pca";
2
- import { validateBoolean, validateArrayOptions, validateNumber } from "../utilities/validators.js";
1
+ import { getFeaturedKeys, computeFlatFeaturesLen, findGroupsFunc } from "./ml-utilities.js";
2
+ import { buildTrainX } from "./trainX.js";
3
+ import { areKeyValuesValid } from "../core-functions/pushToMain.js";
3
4
 
4
- export const pca = main => {
5
+ const algo = 'pca'
5
6
 
6
- const {inputParams, invalidValueIndex, len, verticalOhlcv} = main
7
- const startIndex = invalidValueIndex + 1
7
+ export const pca = (main, index, {prefix, trainingSize, findGroups, trainingCols, lookbackAbs, modelArgs, filterCallback}) => {
8
8
 
9
- for(const rowParams of inputParams)
10
- {
11
- const {key: indicatorName, params} = rowParams
12
-
13
- if(indicatorName !== 'scaler') continue
14
-
15
- const [size, keyNames, options] = params
16
-
17
- const {type, group, lag, pca: pcaOptions} = options
18
-
19
- if(!group || pcaOptions === null) continue
9
+ const {verticalOhlcv, scaledGroups, len, instances, ML, invalidValueIndex} = main
20
10
 
21
- const {
22
- showSource = false,
23
- storeModel = false,
24
- isCovarianceMatrix = false,
25
- method = 'NIPALS', //SVD, covarianceMatrix or NIPALS
26
- center = true,
27
- scale = false,
28
- nCompNIPALS = 2,
29
- ignoreZeroVariance = false
30
- } = pcaOptions
11
+ const startIndex = (invalidValueIndex + 2)
12
+ const allModels = ML.models
31
13
 
32
-
14
+ if(index === startIndex)
15
+ {
16
+ if(!instances.hasOwnProperty(algo)) instances[algo] = {}
33
17
 
34
- validateBoolean(showSource, 'showSource', 'scaler.options.pca.showSource')
35
- validateBoolean(storeModel, 'storeModel', 'scaler.options.pca.storeModel')
36
- validateBoolean(isCovarianceMatrix, 'isCovarianceMatrix', 'scaler.options.pca.isCovarianceMatrix')
37
- validateBoolean(center, 'center', 'scaler.options.pca.center')
38
- validateBoolean(scale, 'scale', 'scaler.options.pca.scale')
39
- validateBoolean(ignoreZeroVariance, 'ignoreZeroVariance', 'scaler.options.pca.ignoreZeroVariance')
40
- validateArrayOptions(['SVD', 'NIPALS', 'covarianceMatrix'], method, 'scaler.options.pca.method')
41
- validateNumber(nCompNIPALS, {min: 1}, 'nCompNIPALS', 'scaler.options.pca.nCompNIPALS')
42
-
43
- const scaledKeyNames = keyNames.map(v => `${type}_${size}_${v}`)
44
- const laggedKeys = []
45
-
46
- for (const k of scaledKeyNames) {
47
- for (let step = 1; step <= lag; step++) {
48
- laggedKeys.push(`${k}_lag_${step}`)
49
- }
18
+ instances[algo][prefix] = {
19
+ flatFeaturesColLen: 0,
20
+ featureCols: [],
21
+ X: []
50
22
  }
23
+
24
+ verticalOhlcv[prefix] = new Array(len).fill(null)
25
+ main.notNumberKeys.add(prefix)
26
+ }
27
+ else if(index < startIndex || index < lookbackAbs)
28
+ {
29
+ return
30
+ }
31
+ else if(filterCallback(verticalOhlcv, index) === false)
32
+ {
33
+ return
34
+ }
35
+ else if(index + 1 === len)
36
+ {
51
37
 
52
- scaledKeyNames.push(...laggedKeys)
53
-
54
- const diffLen = len - startIndex
55
- const numCols = scaledKeyNames.length
56
- const tempArr = new Array(diffLen)
57
-
58
- for (let i = startIndex; i < len; i++) {
59
-
60
- const row = new Float64Array(numCols);
61
-
62
- let j = 0
38
+ if(instances[algo][prefix].featureCols.length === 0)
39
+ {
40
+ const inputFeatures = [...trainingCols, ...(findGroupsFunc(findGroups, scaledGroups))]
63
41
 
64
- for (const key of scaledKeyNames) {
65
- row[j++] = verticalOhlcv[key][i]
66
- }
42
+ throw new Error(`Some of the provided features in ${prefix} features where not found in "verticalOhlcv": ${JSON.stringify(inputFeatures)}`)
43
+ }
67
44
 
68
- tempArr[i - startIndex] = row
45
+ //last execution
46
+ for(const featureKey of instances[algo][prefix].featureCols)
47
+ {
48
+ if(!verticalOhlcv.hasOwnProperty(featureKey)) throw new Error(`Feature "${featureKey}" not found in verticalOhlcv for pca.`)
69
49
  }
70
50
 
71
- if(!showSource)
51
+ if(instances[algo][prefix].X.length < trainingSize)
72
52
  {
73
- for (const key of scaledKeyNames) {
74
- delete verticalOhlcv[key]
75
- }
53
+ const requiredDatapoints = instances[algo][prefix].X.length - trainingSize
54
+ throw new Error(`The current "trainingSize" at "${prefix}" requires at least ${requiredDatapoints} more datapoints. Try adding more input ohlcv rows or reducing the "trainingSize" by ${requiredDatapoints}.`)
76
55
  }
56
+ }
77
57
 
78
- const verticalKey = `pca_${type}_${size}`
58
+ const dataSetInstance = instances[algo][prefix]
79
59
 
80
- const pca = new PCA(tempArr, {
81
- isCovarianceMatrix,
82
- method,
83
- center,
84
- scale,
85
- nCompNIPALS,
86
- ignoreZeroVariance
87
- })
60
+ if(dataSetInstance.flatFeaturesColLen === 0)
61
+ {
62
+ dataSetInstance.featureCols = getFeaturedKeys({trainingCols, findGroups, verticalOhlcv, scaledGroups})
88
63
 
89
- if(storeModel)
64
+ if(areKeyValuesValid(main, index, dataSetInstance.featureCols))
90
65
  {
91
- main.models[verticalKey] = pca.toJSON()
66
+ dataSetInstance.flatFeaturesColLen = computeFlatFeaturesLen(dataSetInstance.featureCols, verticalOhlcv, index)
67
+ } else {
68
+ return
92
69
  }
93
-
94
- const predictions = pca.predict(tempArr).to2DArray()
70
+ }
95
71
 
96
- verticalOhlcv[verticalKey] = new Array(len).fill(null)
72
+ if(dataSetInstance.flatFeaturesColLen === 0) return
73
+
74
+ const {X: xRows, featureCols, flatFeaturesColLen} = dataSetInstance
97
75
 
98
- for(let x = 0; x < predictions.length; x++)
99
- {
100
- verticalOhlcv[verticalKey][x + startIndex] = predictions[x]
101
- }
76
+ const trainX = buildTrainX({featureCols, flatFeaturesColLen, type: algo, index, lookbackAbs, main})
102
77
 
78
+ if (trainX == null) return
79
+
80
+ if(allModels.hasOwnProperty(prefix))
81
+ {
82
+
83
+ const futureRow = allModels[prefix].predict([trainX]).to2DArray()
84
+
85
+ verticalOhlcv[prefix][index] = futureRow[0]
103
86
  }
104
87
 
88
+ xRows.push(trainX)
89
+
90
+ if (xRows.length > trainingSize) xRows.shift()
91
+
92
+ if(xRows.length === trainingSize)
93
+ {
94
+ allModels[prefix] = new ML.classes.PCA(xRows, modelArgs)
95
+ }
105
96
  }
@@ -0,0 +1,227 @@
1
+ import { getFeaturedKeys, computeFlatFeaturesLen, logMlTraining, findGroupsFunc } from './ml-utilities.js'
2
+ import { buildTrainX } from './trainX.js'
3
+ import { areKeyValuesValid } from '../core-functions/pushToMain.js'
4
+
5
+ const algo = 'regressor'
6
+
7
+ export const regressor = (main, index, trainingSize, {target, predictions, retrain, trainingCols, findGroups, type, modelArgs, precompute, filterCallback, modelConfig, modelClass}) => {
8
+
9
+ const {lookbackAbs, prefix, flatX, flatY} = precompute
10
+ const {verticalOhlcv, len, instances, scaledGroups, invalidValueIndex, ML} = main
11
+ const allModels = ML.models
12
+ const startIndex = (invalidValueIndex + 1)
13
+
14
+ if(index === startIndex)
15
+ {
16
+ if (!instances.hasOwnProperty(algo)) instances.regressor = {}
17
+ if(!instances.regressor.hasOwnProperty(prefix)) instances.regressor[prefix] = {}
18
+
19
+ const expectedLoops = (flatY) ? predictions : 1 //
20
+
21
+ instances.regressor[prefix] = {
22
+ expectedLoops,
23
+ trainingSize,
24
+ isTrained: new Array(expectedLoops).fill(false),
25
+ shouldRetrain: retrain,
26
+ featureCols: [],
27
+ flatFeaturesColLen: 0,
28
+ X: [],
29
+ Y: Array.from({ length: expectedLoops }, () => []),
30
+ }
31
+
32
+ for(let x = 0; x < predictions; x++)
33
+ {
34
+ const predictionKey = `${prefix}_${(x+1)}`
35
+ verticalOhlcv[predictionKey] = new Float64Array(len).fill(NaN)
36
+
37
+ if(!allModels.hasOwnProperty(predictionKey))
38
+ {
39
+ allModels[predictionKey] = []
40
+ }
41
+ }
42
+ }
43
+ else if(index < startIndex || index < lookbackAbs)
44
+ {
45
+ return
46
+ }
47
+ else if(filterCallback(verticalOhlcv, index) === false)
48
+ {
49
+ return
50
+ }
51
+ else if(index + 1 === len)
52
+ {
53
+ if(instances.regressor[prefix].featureCols.length === 0)
54
+ {
55
+ const inputFeatures = [...trainingCols, ...(findGroupsFunc(findGroups, scaledGroups))]
56
+
57
+ throw new Error(`Some of the provided features in ${prefix} features where not found in "verticalOhlcv": ${JSON.stringify(inputFeatures)}`)
58
+ }
59
+
60
+ //last execution
61
+ for(const featureKey of instances.regressor[prefix].featureCols)
62
+ {
63
+ if(!verticalOhlcv.hasOwnProperty(featureKey)) throw new Error(`Feature "${featureKey}" not found in verticalOhlcv for ${prefix}.`)
64
+ }
65
+
66
+ if(instances.regressor[prefix].X.length < trainingSize)
67
+ {
68
+ const requiredDatapoints = instances.regressor[prefix].X.length - trainingSize
69
+ throw new Error(`The current "trainingSize" at "${prefix}" requires at least ${requiredDatapoints} more datapoints. Try adding more input ohlcv rows or reducing the "trainingSize" by ${requiredDatapoints}.`)
70
+ }
71
+ }
72
+
73
+ const dataSetInstance = instances.regressor[prefix]
74
+
75
+ if(dataSetInstance.flatFeaturesColLen === 0)
76
+ {
77
+ dataSetInstance.featureCols = getFeaturedKeys({trainingCols, findGroups, verticalOhlcv, scaledGroups})
78
+
79
+ if(areKeyValuesValid(main, index, dataSetInstance.featureCols))
80
+ {
81
+ dataSetInstance.flatFeaturesColLen = computeFlatFeaturesLen(dataSetInstance.featureCols, verticalOhlcv, index)
82
+
83
+ logMlTraining({
84
+ featureCols: dataSetInstance.featureCols,
85
+ flatFeaturesColLen: dataSetInstance.flatFeaturesColLen,
86
+ type,
87
+ trainingSize
88
+ })
89
+
90
+ } else {
91
+ return
92
+ }
93
+ }
94
+
95
+ if(dataSetInstance.flatFeaturesColLen === 0) return
96
+
97
+ if(!verticalOhlcv.hasOwnProperty(target))
98
+ {
99
+ throw new Error(`Target property "${target}" not found in verticalOhlcv for regressor.`)
100
+ }
101
+
102
+ const {
103
+ expectedLoops,
104
+ X: xRows,
105
+ Y,
106
+ flatFeaturesColLen,
107
+ featureCols,
108
+ shouldRetrain
109
+ } = dataSetInstance
110
+
111
+ if(!featureCols.includes(target))
112
+ {
113
+ throw new Error(`Target property "${target}" not found in ${type} features: ${JSON.stringify(featureCols)}`)
114
+ }
115
+
116
+ const trainX = (flatX) ? verticalOhlcv[target][index] : buildTrainX({featureCols, flatFeaturesColLen, type, index, lookbackAbs, main})
117
+
118
+ if (flatX) {
119
+ if (!Number.isFinite(trainX)) return; // or `trainX == null`
120
+ } else {
121
+ if (trainX == null) return;
122
+ }
123
+
124
+ let trainY = []
125
+
126
+ for (let i = 0; i < predictions; i++) {
127
+ const v = verticalOhlcv[target][index + i + 1]
128
+
129
+ if (!Number.isFinite(v)) {
130
+ break
131
+ }
132
+
133
+ trainY.push(v)
134
+ }
135
+
136
+ const {train, unsupervised} = modelConfig
137
+
138
+ //if univariable Y (flatY) a model is created for each prediction
139
+ for(let loopIdx = 0; loopIdx < expectedLoops; loopIdx++)
140
+ {
141
+ const predictionKey = `${prefix}_${(loopIdx+1)}`
142
+ const yRows = Y[loopIdx]
143
+ const currTrainY = (flatY) ? (typeof trainY[loopIdx] === 'undefined') ? null : trainY[loopIdx] : trainY
144
+ const isTrained = dataSetInstance.isTrained[loopIdx]
145
+
146
+ const shouldPredict = allModels.hasOwnProperty(predictionKey) && isTrained && allModels[predictionKey].length === expectedLoops
147
+
148
+ //predicts using previously saved models even if current currTrainY is not available
149
+ if(shouldPredict)
150
+ {
151
+ const currModel = allModels[predictionKey][0]
152
+ const futureRow = (unsupervised) ? currModel.predict(predictions)[0] : currModel.predict([trainX])[0]
153
+
154
+ if(flatY)
155
+ {
156
+ if(Number.isNaN(futureRow)) throw new Error(`Prediction of ${type} at index ${index} was expecting a number.`)
157
+
158
+ main.pushToMain({index, key: predictionKey, value: futureRow})
159
+ }
160
+ else {
161
+
162
+ if (!Array.isArray(futureRow) || futureRow.length !== predictions) {
163
+ throw new Error(
164
+ `Prediction output of "${type}" at index ${index} was expecting an array of values.`
165
+ )
166
+ }
167
+
168
+ for(let preIdx = 0; preIdx < predictions; preIdx++) {
169
+ main.pushToMain({index, key: `${prefix}_${(preIdx+1)}`, value: futureRow[preIdx]})
170
+ }
171
+ }
172
+ }
173
+
174
+ if((index + predictions + 1) > len) continue
175
+ if(trainY.length < predictions) continue
176
+ if(currTrainY === null) continue
177
+
178
+ if(flatY)
179
+ {
180
+ if (typeof currTrainY !== 'number' || Number.isNaN(currTrainY))
181
+ {
182
+ throw new Error(`currTrainY must return number, got ${typeof currTrainY} at index ${index}`)
183
+ }
184
+ } else {
185
+ if (!Array.isArray(currTrainY)) {
186
+ throw new Error(`currTrainY must return an array, got ${typeof currTrainY} at index ${index}`)
187
+ }
188
+
189
+ if ((currTrainY.length !== predictions)) {
190
+
191
+ throw new Error(`currTrainY length (${currTrainY.length}) ≠ "options.predictions" (${predictions}) for regressor "${type}"`)
192
+ }
193
+ }
194
+
195
+ // enqueue
196
+ yRows.push(currTrainY)
197
+ if (yRows.length > trainingSize) yRows.shift()
198
+
199
+ if(loopIdx === 0)
200
+ {
201
+ xRows.push(trainX)
202
+ if (xRows.length > trainingSize) xRows.shift()
203
+ }
204
+
205
+ const shouldTrainModel = shouldRetrain || !isTrained
206
+
207
+ if (shouldTrainModel && xRows.length === trainingSize && yRows.length === trainingSize) {
208
+
209
+ const trainedModel = train({modelClass, xRows, yRows, modelArgs, uniqueLabels: 0})
210
+
211
+ if(unsupervised)
212
+ {
213
+ allModels[predictionKey] = [trainedModel]
214
+ } else {
215
+ allModels[predictionKey].push(trainedModel)
216
+
217
+ if(allModels[predictionKey].length > predictions)
218
+ {
219
+ allModels[predictionKey].shift()
220
+ }
221
+ }
222
+
223
+ dataSetInstance.isTrained[loopIdx] = true
224
+ }
225
+
226
+ }
227
+ }