ohlcv-indicators 3.5.56 → 3.5.60

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.
@@ -4,12 +4,6 @@ export const oneHotEncode = (idx, size) => {
4
4
  return vec
5
5
  }
6
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
7
  export const normalizeMinMax = (value, min, max, [validMin, validMax]) => {
14
8
  const clamped = Math.min(Math.max(value, min), max)
15
9
  return ((clamped - min) / (max - min)) * (validMax - validMin) + validMin
@@ -19,112 +13,10 @@ export const normalizeZScore = (value, mean, std) => {
19
13
  return std === 0 ? 0 : (value - mean) / std
20
14
  }
21
15
 
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
- }
16
+ export const scaleByFeatureRange = (x, min, max) => {
17
+ const center = (min + max) / 2
18
+ const halfRange = (max - min) / 2
19
+ const scaled = ((x - center) / halfRange) * 2
67
20
 
68
- return flatFeaturesColLen
69
- }
70
-
71
-
72
- export const getFeaturedKeys = ({trainingCols, findGroups, verticalOhlcv, scaledGroups}) => {
73
-
74
- const featureCols = [...trainingCols, ...(findGroupsFunc(findGroups, scaledGroups))]
75
-
76
-
77
- if (featureCols.length === 0) {
78
- return []
79
- }
80
-
81
- let keyNotFound = false
82
-
83
- // sanity‐check that all features exist
84
- for(let x = 0; x < featureCols.length; x++)
85
- {
86
- const featureKey = featureCols[x]
87
-
88
- if(!verticalOhlcv.hasOwnProperty(featureKey)) {
89
- keyNotFound = true
90
- break
91
- }
92
- }
93
-
94
- if(keyNotFound) return []
95
-
96
- return featureCols
97
- }
98
-
99
- export const updateClassifierMetrics = ({ metrics, trueLabel, predictedLabel }) => {
100
- // Increment overall counters
101
- metrics.total = (metrics.total || 0) + 1
102
- metrics.correct = (metrics.correct || 0)
103
-
104
- // Initialize per‐label stats container if needed
105
- metrics.labels = metrics.labels || {}
106
-
107
- const labelKey = predictedLabel.toString()
108
- // Ensure we have a slot for this label
109
- if (!metrics.labels[labelKey]) {
110
- metrics.labels[labelKey] = { total: 0, correct: 0, accuracy: 0 }
111
- }
112
-
113
- // Update this label’s total predictions
114
- metrics.labels[labelKey].total++
115
-
116
- // If correct, bump both overall and per-label correct counters
117
- if (predictedLabel === trueLabel) {
118
- metrics.correct++
119
- metrics.labels[labelKey].correct++
120
- }
121
-
122
- // Recompute overall accuracy
123
- metrics.accuracy = metrics.correct / metrics.total
124
-
125
- // Recompute accuracy for each label
126
- for (const key in metrics.labels) {
127
- const stats = metrics.labels[key]
128
- stats.accuracy = stats.correct / stats.total
129
- }
130
- }
21
+ return Math.max(-2, Math.min(2, scaled))
22
+ }
@@ -1,12 +1,15 @@
1
- import { normalizeMinMax, normalizeZScore } from "./ml-utilities.js";
2
-
3
- export const scaler = (main, index, size, colKeys, {type, group, range, lag, precomputed, secondaryLoop}) => {
1
+ import { normalizeMinMax, normalizeZScore, scaleByFeatureRange } from "./ml-utilities.js";
4
2
 
5
-
3
+ const isBadNumber = (v) => v == null || !Number.isFinite(v)
4
+
5
+ //rowWiseScaler
6
6
 
7
- const {groupKey, groupKeyLen} = precomputed
8
- const { verticalOhlcv, instances, invalidValueIndex } = main;
9
- const prefix = `${type}_${size}`;
7
+ export const scaler = (main, index, size, colKeys, {type, minMaxRange, lookback, weights, euclideanWeights, byFeatureRange}) => {
8
+
9
+ //this function performs scaling row by row "row wise scaling"
10
+
11
+ const { verticalOhlcv, instances } = main
12
+ const prefix = `${type}_${size}`
10
13
 
11
14
  if(index === 0) {
12
15
  const { len, isAlreadyComputed } = main
@@ -15,8 +18,6 @@ export const scaler = (main, index, size, colKeys, {type, group, range, lag, pre
15
18
  {
16
19
  throw new Error(`In scaler method can not repeat params "size" and "options.type" together between indicators.`)
17
20
  }
18
-
19
- isAlreadyComputed.add(prefix)
20
21
 
21
22
  if(!instances.hasOwnProperty('scaler')) {
22
23
  instances.scaler = {}
@@ -25,7 +26,7 @@ export const scaler = (main, index, size, colKeys, {type, group, range, lag, pre
25
26
  if(!instances.scaler.hasOwnProperty(prefix))
26
27
  {
27
28
  instances.scaler[prefix] = {
28
- windows: {}
29
+ extectedFeatures: (lookback === 0) ? colKeys.length : colKeys.length + (lookback * colKeys.length)
29
30
  }
30
31
  }
31
32
 
@@ -39,103 +40,120 @@ export const scaler = (main, index, size, colKeys, {type, group, range, lag, pre
39
40
  const key = `${prefix}_${target}`;
40
41
  verticalOhlcv[key] = new Float64Array(len).fill(NaN)
41
42
 
42
- const winKey = group ? groupKey : target;
43
- instances.scaler[prefix].windows[winKey] = [];
44
-
45
- if (!main.scaledGroups[groupKey]) main.scaledGroups[groupKey] = [];
46
- main.scaledGroups[groupKey].push(key);
43
+ if (!main.scaledGroups[prefix]) main.scaledGroups[prefix] = [];
44
+
45
+ main.scaledGroups[prefix].push(key);
47
46
 
48
- if (lag > 0) {
49
- const lags = Array.from({ length: lag }).map((_, i) => `${key}_lag_${i + 1}`)
50
- main.scaledGroups[groupKey].push(...lags)
47
+ if (lookback > 0) {
48
+ const laggedKeys = Array.from({ length: lookback }).map((_, i) => `${key}_lag_${i + 1}`)
49
+
50
+ for(const lKey of laggedKeys) {
51
+ verticalOhlcv[lKey] = new Float64Array(len).fill(NaN)
52
+ }
53
+
54
+ main.scaledGroups[prefix].push(...laggedKeys)
51
55
  features.push(key)
52
56
  }
53
57
  }
54
58
 
55
- if(lag > 0)
56
- {
57
- main.lag(features, lag)
59
+ for(const weKey of Object.keys(weights)) {
60
+ if (!verticalOhlcv.hasOwnProperty(weKey)) {
61
+ throw new Error(`Target property "${weKey}" not found in verticalOhlcv`);
62
+ }
58
63
  }
59
- }
60
64
 
61
- if(secondaryLoop === true && index <= invalidValueIndex) true
65
+ }
62
66
 
63
- const { windows } = instances.scaler[prefix]
67
+ const { extectedFeatures } = instances.scaler[prefix]
64
68
 
65
69
  let hasInvalidVal = false
70
+ const row = {}
71
+ const rowValues = []
72
+ let countFeatures = 0
66
73
 
67
- // update windows with current values
68
74
  for (let x = 0; x < colKeys.length; x++) {
69
75
  const target = colKeys[x]
70
76
  const col = verticalOhlcv[target]
77
+ const currWeight = weights?.[target]?.[0] ?? 1
78
+ const currVal = col[index]
71
79
 
72
- if(typeof col === 'undefined')
80
+ if(isBadNumber(currVal))
73
81
  {
74
82
  hasInvalidVal = true
75
- break
83
+ break
76
84
  }
77
85
 
78
- const val = col[index]
86
+ row[target] = {
87
+ val: currVal,
88
+ weight: (euclideanWeights) ? Math.sqrt(currWeight) : currWeight
89
+ }
79
90
 
80
- if(Number.isNaN(val) || val === null)
81
- {
82
- hasInvalidVal = true
83
- break
91
+ rowValues.push(currVal)
92
+ countFeatures++
93
+
94
+ if(lookback > 0) {
95
+ for (let step = 1; step <= lookback; step++) {
96
+
97
+ const laggedWeight = weights?.[target]?.[step] ?? 1
98
+ const laggedVal = col[index - step]
99
+
100
+ if(isBadNumber(laggedVal))
101
+ {
102
+ hasInvalidVal = true
103
+ break
104
+ }
105
+
106
+ row[`${target}_lag_${step}`] = {
107
+ val: laggedVal,
108
+ weight: (euclideanWeights) ? Math.sqrt(laggedWeight) : laggedWeight
109
+ }
110
+
111
+ rowValues.push(laggedVal)
112
+ countFeatures++
113
+ }
114
+
115
+ if(hasInvalidVal) break
84
116
  }
117
+ }
85
118
 
86
- const winKey = group ? groupKey : target
87
- const win = windows[winKey]
88
-
89
- win.push(val)
90
-
91
- if (win.length > (group ? size * groupKeyLen : size)) {
92
- win.shift()
119
+ if(hasInvalidVal || countFeatures !== extectedFeatures) return
120
+
121
+ if (type === "minmax") {
122
+ const mn = Math.min(...rowValues);
123
+ const mx = Math.max(...rowValues);
124
+
125
+ for (const [key, { val, weight }] of Object.entries(row)) {
126
+ const scaled = normalizeMinMax(val, mn, mx, minMaxRange)
127
+ const weigthed = scaled === 0 ? 0 : scaled * weight
128
+ main.pushToMain({ index, key: `${prefix}_${key}`, value: weigthed });
93
129
  }
130
+ }
94
131
 
132
+ if (type === "zscore") {
133
+ const n = rowValues.length;
134
+ const mean = rowValues.reduce((s, x) => s + x, 0) / n;
135
+ const variance = rowValues.reduce((s, x) => s + (x - mean) ** 2, 0) / n;
136
+ const std = Math.sqrt(variance);
137
+
138
+ for (const [key, { val, weight }] of Object.entries(row)) {
139
+ const scaled = normalizeZScore(val, mean, std)
140
+ const weigthed = scaled === 0 ? 0 : scaled * weight
141
+ main.pushToMain({ index, key: `${prefix}_${key}`, value: weigthed });
142
+ }
95
143
  }
96
144
 
97
- if(hasInvalidVal) return
98
-
99
- const ready = index + 1 >= size
145
+ if(type === "byfeature") {
100
146
 
101
- if(!ready) return
102
-
103
- // scale values once enough data
104
- for (let x = 0; x < colKeys.length; x++) {
105
- const target = colKeys[x]
106
- const col = verticalOhlcv[target]
147
+ const [mn, mx] = byFeatureRange
107
148
 
108
- if(typeof col === 'undefined')
109
- {
110
- break
111
- }
149
+ for (const [key, { val, weight }] of Object.entries(row)) {
112
150
 
113
- const val = col[index]
151
+ const scaled = normalizeMinMax(val, mn, mx, minMaxRange)
152
+ const weigthed = scaled === 0 ? 0 : scaled * weight
114
153
 
115
- if(Number.isNaN(val) || val === null)
116
- {
117
- break
154
+ main.pushToMain({ index, key: `${prefix}_${key}`, value: weigthed });
118
155
  }
119
156
 
120
- const key = `${prefix}_${target}`
121
- let scaled = null
122
- const winKey = group ? groupKey : target
123
- const win = windows[winKey]
124
-
125
- if (type === 'minmax') {
126
- const mn = Math.min(...win)
127
- const mx = Math.max(...win)
128
- scaled = normalizeMinMax(val, mn, mx, range)
129
- } else if (type === 'zscore') {
130
- const mean = win.reduce((sum, x) => sum + x, 0) / win.length;
131
- const variance = win.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / win.length
132
- const std = Math.sqrt(variance);
133
- scaled = normalizeZScore(val, mean, std)
134
- } else {
135
- throw new Error(`Unknown scaler type "${type}"`)
136
- }
137
-
138
- main.pushToMain({ index, key, value: scaled})
139
157
  }
140
158
  }
141
159
 
@@ -12,17 +12,12 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
12
12
  const {len, dateType} = main
13
13
  if(!dateType) throw Error('dateTime method found and invalid "date" in input ohlcv')
14
14
 
15
- const startYear = verticalOhlcv.date[0].getUTCFullYear()
16
- const currentYear = new Date().getUTCFullYear()
17
-
18
15
  Object.assign(instances, {
19
16
  dateTime: {
20
17
  colKeys: [...precompute.colKeys, `${prefix}year`],
21
18
  colKeySizes: {
22
- ...precompute.colKeySizes,
23
- [`${prefix}year`]: (currentYear - startYear) + 1
19
+ ...precompute.colKeySizes
24
20
  }
25
- ,startYear
26
21
  }
27
22
  })
28
23
 
@@ -45,11 +40,11 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
45
40
  }
46
41
  }
47
42
 
48
- const {colKeySizes, startYear} = instances.dateTime
43
+ const {colKeySizes} = instances.dateTime
49
44
 
50
45
  const currDate = verticalOhlcv.date[index]
51
46
 
52
- const dateInfo = getDateInfo(currDate, oneHot, colKeySizes, prefix, startYear)
47
+ const dateInfo = getDateInfo(currDate, oneHot, colKeySizes)
53
48
 
54
49
  for(const [key, value] of Object.entries(dateInfo))
55
50
  {
@@ -59,25 +54,34 @@ export const dateTime = (main, index, {lag, oneHot, precompute}) => {
59
54
 
60
55
 
61
56
 
62
- const getDateInfo = (date, oneHot, colKeySizes, prefix, startYear) => {
57
+ const getDateInfo = (date, oneHot, colKeySizes) => {
63
58
 
64
- const encode = (value, size) => (oneHot) ? oneHotEncode(value, size) : value
65
59
 
66
- const output = {
67
- [`${prefix}year`]: date.getUTCFullYear(),
68
- [`${prefix}month`]: date.getUTCMonth(),
69
- [`${prefix}hour`]: date.getUTCHours(),
70
- [`${prefix}minute`]: date.getUTCMinutes(),
71
- [`${prefix}day_of_the_week`]: date.getUTCDay(),
72
- [`${prefix}day_of_the_month`]: date.getUTCDate() - 1,
73
- }
60
+ const year = date.getFullYear();
74
61
 
75
- for(const [key, size] of Object.entries(colKeySizes))
76
- {
77
- const value = (key === `${prefix}year`) ? output[key] - startYear: output[key]
78
- output[key] = encode(value, size)
79
- }
62
+ //start with 0 values
63
+ const month = date.getMonth();
64
+ const hour = date.getHours();
65
+ const minute = date.getMinutes();
66
+ const dayOfWeek = date.getDay();
67
+ const dayOfMonth = date.getDate() - 1; // keep same offset
80
68
 
81
- return output
82
- }
69
+ if (!oneHot) {
70
+ return {
71
+ year: year,
72
+ month: month + 1, //iso month
73
+ hour: hour,
74
+ minute: minute,
75
+ day_of_the_week: ((dayOfWeek + 6) % 7) + 1, //iso date of the week
76
+ day_of_the_month: dayOfMonth + 1, //iso date of the month
77
+ };
78
+ }
83
79
 
80
+ return {
81
+ one_hot_month: oneHotEncode(month, colKeySizes.one_hot_month),
82
+ one_hot_hour: oneHotEncode(hour, colKeySizes.one_hot_hour),
83
+ one_hot_minute: oneHotEncode(minute, colKeySizes.one_hot_minute),
84
+ one_hot_day_of_the_week: oneHotEncode(dayOfWeek, colKeySizes.one_hot_day_of_the_week),
85
+ one_hot_day_of_the_month: oneHotEncode(dayOfMonth, colKeySizes.one_hot_day_of_the_month),
86
+ };
87
+ };
@@ -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
@@ -123,8 +123,8 @@ export const validateArrayOfRanges = (range, paramName, callerName) => {
123
123
 
124
124
  const [min, max] = range
125
125
 
126
- validateNumber(min, {min: -100, max, allowDecimals: false}, 'min', callerName)
127
- validateNumber(max, {min: min, max: 100, allowDecimals: false}, 'max', callerName)
126
+ validateNumber(min, {min: -100, max, allowDecimals: false}, `${paramName}[0] (min)`, callerName)
127
+ validateNumber(max, {min: min, max: 100, allowDecimals: false}, `${paramName}[1] (max)`, callerName)
128
128
 
129
129
  if(min === max)
130
130
  {
@@ -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
- };