xy-scale 1.4.2 → 1.4.31
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/README.md +81 -147
- package/dist/xy-scale.min.js +1 -1
- package/package.json +2 -2
- package/src/datasets.js +25 -34
- package/test/test.js +41 -144
- package/src/scale.js +0 -286
package/README.md
CHANGED
|
@@ -1,196 +1,130 @@
|
|
|
1
|
-
#
|
|
2
|
-
**Machine Learning Data Preparation Toolkit**
|
|
3
|
-
*XY Splitting, Feature Weighting, Standardization, and MinMax Scaling in JavaScript*
|
|
1
|
+
# xy-scale.js
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
Machine learning data preparation helpers for JavaScript.
|
|
6
4
|
|
|
7
5
|
## Overview
|
|
8
6
|
|
|
9
|
-
`xy-scale.js`
|
|
7
|
+
`xy-scale.js` now focuses on turning already-prepared row objects into flat `X` and `Y` arrays for training or production use.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
- **Scaling:** Handling both numerical and categorical data.
|
|
13
|
-
- **Preprocessing:** Preparing time-series data for modeling.
|
|
14
|
-
|
|
15
|
-
### Key Features
|
|
16
|
-
|
|
17
|
-
- **Modular Functions:** `parseTrainingXY` and `parseProductionX` allow for flexible handling of features and labels.
|
|
18
|
-
- **Custom Scaling:** Enable custom scaling, feature weighting, and data transformation.
|
|
19
|
-
- **Feature Grouping:** Organize features into meaningful categories for streamlined processing.
|
|
20
|
-
|
|
21
|
-
---
|
|
9
|
+
The library no longer scales values internally. Your `arrObj` input, or the objects returned by your callbacks, should already contain the numeric or boolean values you want to feed into a model.
|
|
22
10
|
|
|
23
11
|
## Installation
|
|
24
12
|
|
|
25
|
-
Install the toolkit via npm:
|
|
26
|
-
|
|
27
13
|
```bash
|
|
28
14
|
npm install xy-scale
|
|
29
15
|
```
|
|
30
16
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
## Main Functions
|
|
34
|
-
|
|
35
|
-
### 1. `parseTrainingXY`
|
|
36
|
-
|
|
37
|
-
Processes a dataset for supervised learning by scaling and splitting it into training and testing subsets. It supports configurable options for feature grouping, weighting, and scaling.
|
|
38
|
-
|
|
39
|
-
**Parameters:**
|
|
17
|
+
## Exports
|
|
40
18
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- `xCallbackFunc` (Function): Extracts X values from each row. Returns `null` or `undefined` to exclude a row.
|
|
45
|
-
- `groups` (Object, optional): Groups continuous values features into categories after `yCallbackFunc` and `xCallbackFunc` callbacks are applied (e.g., `{ ohlc: ['open', 'high', 'low', 'close'] }`). Each feature belonging to a group will be MinMax scaled or standardized using the group's properties (`min`, `max`, `std`).
|
|
46
|
-
- `shuffle` (Boolean, optional): Randomizes data order after `yCallbackFunc` and `xCallbackFunc` callbacks are applied (default: `false`).
|
|
47
|
-
- `repeat` (Object, optional): Determines feature repetition after `yCallbackFunc` and `xCallbackFunc` callbacks are applied.
|
|
48
|
-
- `balancing` (String, optional): Handles inbalanced datasets applying `oversample` or `undersample` to `X` and `Y` (defaults to `null`);
|
|
49
|
-
|
|
50
|
-
**Returns:**
|
|
51
|
-
|
|
52
|
-
- `trainX`, `trainY`: Scaled training features and labels.
|
|
53
|
-
- `testX`, `testY`: Scaled testing features and labels.
|
|
54
|
-
- `configX`, `configY`: Scaling configuration objects for features and labels.
|
|
55
|
-
- `keyNamesX`, `keyNamesY`: Key names reflecting feature repetition and grouping.
|
|
56
|
-
|
|
57
|
-
---
|
|
19
|
+
```javascript
|
|
20
|
+
import { parseTrainingXY, parseProductionX, arrayToTimesteps } from 'xy-scale';
|
|
21
|
+
```
|
|
58
22
|
|
|
59
|
-
|
|
23
|
+
## Main functions
|
|
60
24
|
|
|
61
|
-
|
|
25
|
+
### parseTrainingXY
|
|
62
26
|
|
|
63
|
-
|
|
27
|
+
Builds supervised-learning datasets and splits them into training and testing arrays.
|
|
64
28
|
|
|
65
|
-
|
|
66
|
-
- `repeat` (Object, optional): Determines feature repetition after `yCallbackFunc` and `xCallbackFunc` callbacks are applied.
|
|
67
|
-
- `xCallbackFunc` (Function): Extracts X values from each row. Returns `null` or `undefined` to exclude a row.
|
|
68
|
-
- `groups` (Object, optional): Groups continuous values features into categories after `yCallbackFunc` and `xCallbackFunc` callbacks are applied (e.g., `{ ohlc: ['open', 'high', 'low', 'close'] }`). Each feature belonging to a group will be MinMax scaled or standardized using the group's properties (`min`, `max`, `std`).
|
|
69
|
-
- `shuffle` (Boolean, optional): Randomizes data order after `yCallbackFunc` and `xCallbackFunc` callbacks are applied (default: `false`).
|
|
29
|
+
#### Parameters
|
|
70
30
|
|
|
71
|
-
|
|
31
|
+
- `arrObj` (Array<Object>): Source dataset.
|
|
32
|
+
- `trainingSplit` (Number, optional): Fraction of rows used for training. Default: `0.8`.
|
|
33
|
+
- `yCallbackFunc` (Function, optional): Builds the output object for each row. Returning `null` or `undefined` skips the row.
|
|
34
|
+
- `xCallbackFunc` (Function, optional): Builds the feature object for each row. Returning `null` or `undefined` skips the row.
|
|
35
|
+
- `validateRows` (Function, optional): Extra row filter executed before the callbacks.
|
|
36
|
+
- `shuffle` (Boolean, optional): Shuffles `X` and `Y` together before splitting. Default: `false`.
|
|
37
|
+
- `balancing` (String, optional): Accepts `oversample` or `undersample`.
|
|
38
|
+
- `state` (Object, optional): Shared mutable state passed into callbacks.
|
|
72
39
|
|
|
73
|
-
|
|
74
|
-
- `configX`: Scaling configuration for features.
|
|
75
|
-
- `keyNamesX`: Key names reflecting feature repetition and grouping.
|
|
40
|
+
#### Returns
|
|
76
41
|
|
|
77
|
-
|
|
42
|
+
- `trainX`, `trainY`
|
|
43
|
+
- `testX`, `testY`
|
|
44
|
+
- `configX`: `{ keyNames: [...] }`
|
|
45
|
+
- `configY`: `{ keyNames: [...] }`
|
|
78
46
|
|
|
79
|
-
|
|
47
|
+
`configX.keyNames` and `configY.keyNames` preserve the object-key order used when flattening each callback result into an array.
|
|
80
48
|
|
|
81
|
-
|
|
49
|
+
### parseProductionX
|
|
82
50
|
|
|
83
|
-
|
|
51
|
+
Builds production-ready feature arrays from already-prepared rows.
|
|
84
52
|
|
|
85
|
-
|
|
86
|
-
- `config` (Object): Configuration object returned from `parseTrainingXY` or `parseProductionX`.
|
|
87
|
-
- `keyNames` (Array): Key names reflecting feature repetition and grouping.
|
|
53
|
+
#### Parameters
|
|
88
54
|
|
|
89
|
-
|
|
55
|
+
- `arrObj` (Array<Object>): Source dataset.
|
|
56
|
+
- `xCallbackFunc` (Function, optional): Builds the feature object for each row. Returning `null`, `undefined`, or `false` skips the row.
|
|
57
|
+
- `validateRows` (Function, optional): Extra row filter executed before the callback.
|
|
58
|
+
- `shuffle` (Boolean, optional): Shuffles the final `X` rows. Default: `false`.
|
|
59
|
+
- `state` (Object, optional): Shared mutable state passed into the callback.
|
|
90
60
|
|
|
91
|
-
|
|
61
|
+
#### Returns
|
|
92
62
|
|
|
93
|
-
|
|
63
|
+
- `X`
|
|
64
|
+
- `configX`: `{ keyNames: [...] }`
|
|
94
65
|
|
|
95
|
-
###
|
|
66
|
+
### arrayToTimesteps
|
|
96
67
|
|
|
97
|
-
Converts a flat array into sequences for time-series
|
|
68
|
+
Converts a flat array into overlapping sequences for time-series models.
|
|
98
69
|
|
|
99
|
-
|
|
70
|
+
#### Parameters
|
|
100
71
|
|
|
101
72
|
- `arr` (Array): Input array.
|
|
102
73
|
- `timeSteps` (Number): Length of each sequence.
|
|
103
74
|
- If `timeSteps === 0`, returns the original array.
|
|
104
75
|
- If `timeSteps < 0`, throws an error.
|
|
105
76
|
|
|
106
|
-
|
|
77
|
+
#### Returns
|
|
107
78
|
|
|
108
79
|
- An array of overlapping sub-arrays, each containing `timeSteps` elements.
|
|
109
80
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
## Usage Example
|
|
81
|
+
## Usage example
|
|
113
82
|
|
|
114
83
|
```javascript
|
|
115
|
-
import { parseTrainingXY,
|
|
116
|
-
import { loadFile } from './fs.js';
|
|
84
|
+
import { parseTrainingXY, arrayToTimesteps } from 'xy-scale';
|
|
117
85
|
import * as tf from '@tensorflow/tfjs-node';
|
|
118
86
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
// Parse and scale training data
|
|
154
|
-
const {
|
|
155
|
-
trainX,
|
|
156
|
-
trainY,
|
|
157
|
-
testX,
|
|
158
|
-
testY,
|
|
159
|
-
configX,
|
|
160
|
-
keyNamesX,
|
|
161
|
-
} = parseTrainingXY({
|
|
162
|
-
arrObj: myArray,
|
|
163
|
-
trainingSplit: 0.9,
|
|
164
|
-
yCallbackFunc,
|
|
165
|
-
xCallbackFunc,
|
|
166
|
-
groups: { ohlc: ['open', 'high', 'low', 'close'] },
|
|
167
|
-
shuffle: true,
|
|
168
|
-
repeat: { close: 20 },
|
|
169
|
-
balancing: null, //oversample or undersample
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
// Time-stepping and TensorFlow integration
|
|
173
|
-
const timeSteps = 10;
|
|
174
|
-
const timeSteppedTrainX = arrayToTimesteps(trainX, timeSteps);
|
|
175
|
-
const trimmedTrainY = trainY.slice(timeSteps - 1);
|
|
176
|
-
|
|
177
|
-
const inputX = tf.tensor3d(timeSteppedTrainX, [timeSteppedTrainX.length, timeSteps, trainX[0].length]);
|
|
178
|
-
const targetY = tf.tensor2d(trimmedTrainY, [trimmedTrainY.length, trainY[0].length]);
|
|
179
|
-
|
|
180
|
-
console.log('configX', configX);
|
|
181
|
-
console.log('inputX', inputX);
|
|
182
|
-
console.log('targetY', targetY);
|
|
183
|
-
})();
|
|
87
|
+
const candles = [
|
|
88
|
+
{ closeScaled: 0.41, volumeScaled: 0.22, targetUp: 1 },
|
|
89
|
+
{ closeScaled: 0.45, volumeScaled: 0.25, targetUp: 0 },
|
|
90
|
+
{ closeScaled: 0.48, volumeScaled: 0.28, targetUp: 1 },
|
|
91
|
+
{ closeScaled: 0.51, volumeScaled: 0.31, targetUp: 1 },
|
|
92
|
+
{ closeScaled: 0.49, volumeScaled: 0.27, targetUp: 0 },
|
|
93
|
+
{ closeScaled: 0.54, volumeScaled: 0.35, targetUp: 1 },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
const { trainX, trainY, testX, testY, configX, configY } = parseTrainingXY({
|
|
97
|
+
arrObj: candles,
|
|
98
|
+
trainingSplit: 0.8,
|
|
99
|
+
shuffle: true,
|
|
100
|
+
xCallbackFunc: ({ objRow, index }) => ({
|
|
101
|
+
close: objRow[index].closeScaled,
|
|
102
|
+
volume: objRow[index].volumeScaled,
|
|
103
|
+
}),
|
|
104
|
+
yCallbackFunc: ({ objRow, index }) => ({
|
|
105
|
+
target: objRow[index].targetUp,
|
|
106
|
+
}),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const timeSteps = 3;
|
|
110
|
+
const timeSteppedTrainX = arrayToTimesteps(trainX, timeSteps);
|
|
111
|
+
const trimmedTrainY = trainY.slice(timeSteps - 1);
|
|
112
|
+
|
|
113
|
+
const inputX = tf.tensor3d(timeSteppedTrainX, [timeSteppedTrainX.length, timeSteps, trainX[0].length]);
|
|
114
|
+
const targetY = tf.tensor2d(trimmedTrainY, [trimmedTrainY.length, trainY[0].length]);
|
|
115
|
+
|
|
116
|
+
console.log(configX.keyNames);
|
|
117
|
+
console.log(configY.keyNames);
|
|
118
|
+
console.log(testX, testY);
|
|
119
|
+
console.log(inputX, targetY);
|
|
184
120
|
```
|
|
185
121
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
## Additional Information
|
|
189
|
-
|
|
190
|
-
For more detailed documentation, examples, and support, please visit the [GitHub repository](https://github.com/jaimelias/xy-scale).
|
|
122
|
+
## Notes
|
|
191
123
|
|
|
192
|
-
|
|
124
|
+
- `parseTrainingXY` and `parseProductionX` do not scale values.
|
|
125
|
+
- If you need scaling, do it before passing data into this library.
|
|
126
|
+
- Callback return objects are flattened with `Object.values(...)`, using the same key order stored in `configX.keyNames` and `configY.keyNames`.
|
|
193
127
|
|
|
194
128
|
## License
|
|
195
129
|
|
|
196
|
-
This project is licensed under the
|
|
130
|
+
This project is licensed under the MIT License.
|
package/dist/xy-scale.min.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var XY_Scale;(()=>{"use strict";var e={d:(t,
|
|
1
|
+
var XY_Scale;(()=>{"use strict";var e={d:(t,r)=>{for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};e.r(t),e.d(t,{arrayToTimesteps:()=>l,parseProductionX:()=>o,parseTrainingXY:()=>a});const r=(e,{min:t=-1/0,max:r=1/0},n)=>{if(!Array.isArray(e))throw new Error(`Invalid property. "${n}" expected an array.`);if(e.length<t)throw new Error(`Invalid property value. Array "${n}" expected at least ${r} items.`);if(e.length>r)throw new Error(`Invalid property value. Array "${n}" expected at max ${r} items.`);return!0},n=e=>{for(const[t,r]of Object.entries(e)){if("number"==typeof r&&Number.isNaN(r))throw new Error(`Invalid value at index 0 property "${t}": value is "${r}". Expected a numeric value.`);if(null===r)throw new Error(`Invalid value at index 0 property "${t}": value is "${r}".`)}return!0},a=({arrObj:e=[],trainingSplit:t=.8,yCallbackFunc:a=e=>e,xCallbackFunc:o=e=>e,validateRows:l=()=>!0,shuffle:s=!1,balancing:i="",state:c={}})=>{let u=[],f=[];r(e,{min:5},"parseTrainingXY"),n(e[0]);for(let t=0;t<e.length;t++){if(!l({objRow:e,index:t,state:c}))continue;const r=o({objRow:e,index:t,state:c}),n=a({objRow:e,index:t,state:c});null!=r&&null!=n&&(u.push(r),f.push(n))}if(s){const{shuffledX:e,shuffledY:t}=((e,t)=>{if(e.length!==t.length)throw new Error("X and Y arrays must have the same length");const r=Array.from({length:e.length},((e,t)=>t));for(let e=r.length-1;e>0;e--){const t=Math.floor(Math.random()*(e+1));[r[e],r[t]]=[r[t],r[e]]}return{shuffledX:r.map((t=>e[t])),shuffledY:r.map((e=>t[e]))}})(u,f);u=e,f=t}const h=u.length,p=f.length,d=new Array(h),y=new Array(p),m={keyNames:h?Object.keys(u[0]):[]},b={keyNames:p?Object.keys(f[0]):[]};for(let e=0;e<h;e++)d[e]=Object.values(u[e]);for(let e=0;e<p;e++)y[e]=Object.values(f[e]);const g=Math.floor(d.length*t);let v=d.slice(0,g),w=y.slice(0,g),j=d.slice(g),O=y.slice(g);if(i){let e;if("oversample"===i)e=((e,t)=>{const r={},n={};t.forEach(((a,o)=>{r[a]||(r[a]=0,n[a]=[]),r[a]++,n[a].push([e[o],t[o]])}));const a=Math.max(...Object.values(r)),o=[],l=[];return Object.keys(n).forEach((e=>{const t=n[e],r=t.length;for(let e=0;e<a;e++){const n=t[e%r];o.push(n[0]),l.push(n[1])}})),{X:o,Y:l}})(v,w),v=e.X,w=e.Y;else{if("undersample"!==i)throw Error('balancing argument only accepts "false", "oversample" and "undersample". Defaults to "false".');e=((e,t)=>{const r={},n={};t.forEach(((a,o)=>{r[a]||(r[a]=0,n[a]=[]),r[a]++,n[a].push([e[o],t[o]])}));const a=Math.min(...Object.values(r)),o=[],l=[];return Object.keys(n).forEach((e=>{const t=n[e];for(let e=0;e<a;e++){const r=t[e];o.push(r[0]),l.push(r[1])}})),{X:o,Y:l}})(v,w),v=e.X,w=e.Y}}return{trainX:v,trainY:w,testX:j,testY:O,configX:m,configY:b}},o=({arrObj:e=[],xCallbackFunc:t=e=>e,validateRows:a=()=>!0,shuffle:o=!1,state:l={}})=>{let s=[];r(e,{min:5},"parseProductionX"),n(e[0]);for(let r=0;r<e.length;r++){if(!a(e[r]))continue;const n=t({objRow:e,index:r,state:l});null!=n&&!1!==n&&s.push(n)}o&&(s=(e=>{const t=[...e];for(let e=t.length-1;e>0;e--){const r=Math.floor(Math.random()*(e+1));[t[e],t[r]]=[t[r],t[e]]}return t})(s));const i=s.length,c=new Array(i),u={keyNames:i?Object.keys(s[0]):[]};for(let e=0;e<i;e++)c[e]=Object.values(s[e]);return{X:c,configX:u}},l=(e,t)=>{if(0===t)return e;if(t<0)throw new Error("timeSteps must be greater than 0");const r=[];for(let n=0;n<=e.length-t;n++)r.push(e.slice(n,n+t));return r};XY_Scale=t})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xy-scale",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.31",
|
|
4
4
|
"main": "./index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"@tensorflow/tfjs-node": "^4.22.0",
|
|
15
15
|
"ml-confusion-matrix": "^2.0.0",
|
|
16
16
|
"ml-knn": "^3.0.0",
|
|
17
|
-
"ohlcv-indicators": "^3.
|
|
17
|
+
"ohlcv-indicators": "^3.5.59",
|
|
18
18
|
"webpack": "^5.96.1",
|
|
19
19
|
"webpack-cli": "^5.1.4"
|
|
20
20
|
},
|
package/src/datasets.js
CHANGED
|
@@ -1,32 +1,24 @@
|
|
|
1
|
-
import { scaleArrayObj } from "./scale.js";
|
|
2
1
|
import { arrayShuffle, xyArrayShuffle } from "./utilities.js";
|
|
3
2
|
import { oversampleXY, undersampleXY } from "./balancing.js";
|
|
4
3
|
import { validateFirstRow, validateArray } from "./validators.js";
|
|
5
|
-
import { validateExcludes } from "./validators.js";
|
|
6
4
|
|
|
7
5
|
//ADD A PARAM max correlation that will measure the correlation between variables if defined
|
|
8
6
|
|
|
9
7
|
export const parseTrainingXY = ({
|
|
10
8
|
arrObj = [], //array of objects
|
|
11
9
|
trainingSplit = 0.8, //numberic float between 0.01 and 0.99
|
|
12
|
-
repeat = {}, //accepted key pair object with number as values
|
|
13
10
|
yCallbackFunc = row => row, //accepted callback functions
|
|
14
11
|
xCallbackFunc = row => row, //accepted callback functions
|
|
15
12
|
validateRows = () => true,//accepted callback functions
|
|
16
|
-
groups = {},//accepted object of arrays
|
|
17
13
|
shuffle = false,//only booleans
|
|
18
|
-
minmaxRange = [0, 1],
|
|
19
14
|
balancing = '',//accepted null, "oversample" or "undersample"
|
|
20
15
|
state = {}, //accepted object or classes
|
|
21
|
-
customMinMaxRanges = {},
|
|
22
|
-
excludes = [],//each item must be a string
|
|
23
16
|
}) => {
|
|
24
17
|
let X = [];
|
|
25
18
|
let Y = [];
|
|
26
19
|
|
|
27
20
|
validateArray(arrObj, {min: 5}, 'parseTrainingXY')
|
|
28
21
|
validateFirstRow(arrObj[0])
|
|
29
|
-
validateExcludes(arrObj[0], excludes)
|
|
30
22
|
|
|
31
23
|
//if parsedX and parsedY is undefined or null the current row will be excluded from training or production
|
|
32
24
|
for (let x = 0; x < arrObj.length; x++) {
|
|
@@ -49,18 +41,20 @@ export const parseTrainingXY = ({
|
|
|
49
41
|
Y = shuffledY
|
|
50
42
|
}
|
|
51
43
|
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
let {
|
|
55
|
-
scaledOutput: scaledX,
|
|
56
|
-
scaledConfig: configX
|
|
57
|
-
} = scaleArrayObj({arrObj: X, repeat, groups, minmaxRange, customMinMaxRanges, excludes: excludesSet})
|
|
58
|
-
|
|
59
|
-
|
|
44
|
+
const xLen = X.length
|
|
60
45
|
const yLen = Y.length
|
|
46
|
+
const flatX = new Array(xLen)
|
|
61
47
|
const flatY = new Array(yLen)
|
|
48
|
+
const configX = {
|
|
49
|
+
keyNames: xLen ? Object.keys(X[0]) : []
|
|
50
|
+
}
|
|
62
51
|
const configY = {
|
|
63
|
-
keyNames: Object.keys(Y[0])
|
|
52
|
+
keyNames: yLen ? Object.keys(Y[0]) : []
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for(let idx = 0; idx < xLen; idx++)
|
|
56
|
+
{
|
|
57
|
+
flatX[idx] = Object.values(X[idx])
|
|
64
58
|
}
|
|
65
59
|
|
|
66
60
|
for(let idx = 0; idx < yLen; idx++)
|
|
@@ -68,11 +62,11 @@ export const parseTrainingXY = ({
|
|
|
68
62
|
flatY[idx] = Object.values(Y[idx])
|
|
69
63
|
}
|
|
70
64
|
|
|
71
|
-
const splitIndex = Math.floor(
|
|
65
|
+
const splitIndex = Math.floor(flatX.length * trainingSplit)
|
|
72
66
|
|
|
73
|
-
let trainX =
|
|
67
|
+
let trainX = flatX.slice(0, splitIndex)
|
|
74
68
|
let trainY = flatY.slice(0, splitIndex)
|
|
75
|
-
let testX =
|
|
69
|
+
let testX = flatX.slice(splitIndex)
|
|
76
70
|
let testY = flatY.slice(splitIndex)
|
|
77
71
|
|
|
78
72
|
|
|
@@ -113,21 +107,15 @@ export const parseTrainingXY = ({
|
|
|
113
107
|
|
|
114
108
|
export const parseProductionX = ({
|
|
115
109
|
arrObj = [],
|
|
116
|
-
repeat = {},
|
|
117
110
|
xCallbackFunc = row => row,
|
|
118
111
|
validateRows = () => true,
|
|
119
|
-
groups = {},
|
|
120
112
|
shuffle = false,
|
|
121
|
-
minmaxRange = [0, 1],
|
|
122
113
|
state = {},
|
|
123
|
-
customMinMaxRanges,
|
|
124
|
-
excludes = []
|
|
125
114
|
}) => {
|
|
126
115
|
let X = [];
|
|
127
116
|
|
|
128
117
|
validateArray(arrObj, {min: 5}, 'parseProductionX')
|
|
129
118
|
validateFirstRow(arrObj[0])
|
|
130
|
-
validateExcludes(arrObj[0], excludes)
|
|
131
119
|
|
|
132
120
|
for (let x = 0; x < arrObj.length; x++) {
|
|
133
121
|
|
|
@@ -145,17 +133,20 @@ export const parseProductionX = ({
|
|
|
145
133
|
X = arrayShuffle(X)
|
|
146
134
|
}
|
|
147
135
|
|
|
136
|
+
const xLen = X.length
|
|
137
|
+
const flatX = new Array(xLen)
|
|
138
|
+
const configX = {
|
|
139
|
+
keyNames: xLen ? Object.keys(X[0]) : []
|
|
140
|
+
}
|
|
148
141
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
} = scaleArrayObj({arrObj: X, repeat, groups, minmaxRange, customMinMaxRanges, excludes: new Set(excludes)})
|
|
154
|
-
|
|
142
|
+
for(let idx = 0; idx < xLen; idx++)
|
|
143
|
+
{
|
|
144
|
+
flatX[idx] = Object.values(X[idx])
|
|
145
|
+
}
|
|
155
146
|
|
|
156
147
|
// Split into training and testing sets
|
|
157
148
|
return {
|
|
158
|
-
X:
|
|
149
|
+
X: flatX,
|
|
159
150
|
configX,
|
|
160
151
|
}
|
|
161
|
-
};
|
|
152
|
+
};
|
package/test/test.js
CHANGED
|
@@ -1,39 +1,34 @@
|
|
|
1
1
|
import OHLCV_INDICATORS from 'ohlcv-indicators'
|
|
2
|
-
import KNN from 'ml-knn'
|
|
3
|
-
import { ConfusionMatrix } from 'ml-confusion-matrix';
|
|
4
|
-
|
|
5
2
|
import { parseTrainingXY } from "../src/datasets.js"
|
|
6
|
-
import {arrayToTimesteps} from '../src/timeSteps.js'
|
|
7
3
|
import { loadFile } from "./fs.js"
|
|
8
|
-
import * as tf from '@tensorflow/tfjs-node'
|
|
9
4
|
|
|
10
5
|
const test = async () => {
|
|
11
6
|
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
const indicators = new OHLCV_INDICATORS({input: ohlcv, ticker: 'BTC', precision: false})
|
|
7
|
+
const input = (await loadFile({fileName: 'btc-1d.json', pathName: 'datasets'}))
|
|
15
8
|
|
|
16
|
-
indicators
|
|
17
|
-
.rsi(40)
|
|
18
|
-
.bollingerBands(20, 2)
|
|
19
|
-
.ema(50)
|
|
20
|
-
.sma(200)
|
|
21
|
-
.sma(300)
|
|
22
|
-
.scaler(200, ['open', 'high', 'low', 'close'], {group: true})
|
|
23
|
-
.crossPairs([
|
|
24
|
-
{fast: 'rsi_40', slow: 30},
|
|
25
|
-
{fast: 'price', slow: 'sma_200'},
|
|
26
|
-
{fast: 'price', slow: 'sma_300'},
|
|
27
|
-
{fast: 'price', slow: 'bollinger_bands_upper'}
|
|
28
|
-
])
|
|
9
|
+
const indicators = new OHLCV_INDICATORS({input, precision: false})
|
|
29
10
|
|
|
30
|
-
const
|
|
11
|
+
const weights5 = {
|
|
12
|
+
ret_gap: [2, 2, 1.75, 1.5],
|
|
13
|
+
ret_change: [2, 2, 1.75, 1.5]
|
|
14
|
+
}
|
|
31
15
|
|
|
32
|
-
|
|
16
|
+
const weights3 = {
|
|
17
|
+
ret_ema_5: [1.5, 1.5, 1.2],
|
|
18
|
+
ret_sma_200: [1.5, 1.5, 1.2],
|
|
19
|
+
ret_atr_14_upper: [1.5, 1.5, 1.2],
|
|
20
|
+
}
|
|
33
21
|
|
|
34
|
-
|
|
22
|
+
indicators
|
|
23
|
+
.volumeDelta()
|
|
24
|
+
.atr(14, {upper: 1})
|
|
25
|
+
.ema(5)
|
|
26
|
+
.sma(200)
|
|
27
|
+
.priceFeatures({colKeys: ['ema_5', 'sma_200', 'atr_14_upper']})
|
|
28
|
+
.scaler('zscore', 5, {group: true, lag: true, colKeys: ['ret_change', 'ret_upper_wick', 'ret_lower_wick', 'ret_gap', 'ret_body', 'ret_range'], weights: weights5})
|
|
29
|
+
.scaler('zscore', 3, {group: true, lag: true, colKeys: ['ret_change', 'ret_ema_5', 'ret_sma_200', 'ret_atr_14_upper'], weights: weights3})
|
|
35
30
|
|
|
36
|
-
|
|
31
|
+
const arrObj = indicators.getData()
|
|
37
32
|
|
|
38
33
|
const {
|
|
39
34
|
trainX,
|
|
@@ -41,156 +36,58 @@ const test = async () => {
|
|
|
41
36
|
testX,
|
|
42
37
|
testY,
|
|
43
38
|
configX,
|
|
44
|
-
keyNamesX,
|
|
45
39
|
} = parseTrainingXY({
|
|
46
|
-
arrObj
|
|
40
|
+
arrObj,
|
|
47
41
|
trainingSplit: 0.50,
|
|
48
42
|
yCallbackFunc,
|
|
49
43
|
xCallbackFunc,
|
|
50
44
|
validateRows: ({objRow, index}) => {
|
|
45
|
+
|
|
51
46
|
const curr = objRow[index]
|
|
52
47
|
const prev = objRow[index - 1]
|
|
53
48
|
|
|
54
|
-
if(typeof prev === 'undefined') return false
|
|
49
|
+
if(typeof prev === 'undefined') return false //return false or null or undefined to continue to skip this row
|
|
50
|
+
|
|
51
|
+
if(!Number.isNaN(curr.sma_200) && !Number.isNaN(prev.sma_200) && curr.sma_200 > prev.sma_200) return true //return true to include this row in dataset
|
|
55
52
|
|
|
56
|
-
return
|
|
53
|
+
return false
|
|
57
54
|
},
|
|
58
55
|
shuffle: true,
|
|
59
|
-
minmaxRange: [0, 1],
|
|
60
56
|
balancing: null,
|
|
61
|
-
groups: scaledGroups,
|
|
62
|
-
excludes: ['high2'],
|
|
63
|
-
correlation: {corrExcludes: ['price_x_sma_300', 'price_x_sma_200']}
|
|
64
57
|
});
|
|
65
58
|
|
|
66
|
-
console.log(configX.
|
|
67
|
-
console.log(
|
|
68
|
-
//console.log(configX)
|
|
69
|
-
|
|
70
|
-
//console.log('trainX', trainX[0])
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/*
|
|
75
|
-
tensorflowExample({
|
|
76
|
-
trainX,
|
|
77
|
-
trainY,
|
|
78
|
-
testX,
|
|
79
|
-
testY,
|
|
80
|
-
configX,
|
|
81
|
-
keyNamesX,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
classifiersExample({
|
|
85
|
-
trainX,
|
|
86
|
-
trainY,
|
|
87
|
-
testX,
|
|
88
|
-
testY,
|
|
89
|
-
configX,
|
|
90
|
-
keyNamesX,
|
|
91
|
-
})
|
|
92
|
-
*/
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const classifiersExample = ({
|
|
97
|
-
trainX,
|
|
98
|
-
trainY,
|
|
99
|
-
testX,
|
|
100
|
-
testY,
|
|
101
|
-
configX,
|
|
102
|
-
keyNamesX,
|
|
103
|
-
}) => {
|
|
104
|
-
const model = new KNN(trainX, trainY)
|
|
105
|
-
|
|
106
|
-
const predictions = model.predict(testX)
|
|
107
|
-
const compare = ConfusionMatrix.fromLabels(testY.flat(), predictions.flat())
|
|
108
|
-
|
|
109
|
-
//console.log(testY.flat(), predictions.flat())
|
|
110
|
-
|
|
111
|
-
console.log(compare.getAccuracy())
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const tensorflowExample = ({
|
|
115
|
-
trainX,
|
|
116
|
-
trainY,
|
|
117
|
-
testX,
|
|
118
|
-
testY,
|
|
119
|
-
configX,
|
|
120
|
-
keyNamesX,
|
|
121
|
-
}) => {
|
|
122
|
-
|
|
123
|
-
const timeSteps = 10
|
|
124
|
-
const colsX = trainX[0].length
|
|
125
|
-
const colsY = trainY[0].length
|
|
126
|
-
const timeSteppedTrainX = arrayToTimesteps(trainX, timeSteps)
|
|
127
|
-
const trimedTrainY = trainY.slice(timeSteps-1)
|
|
128
|
-
|
|
129
|
-
const inputX = tf.tensor3d(timeSteppedTrainX, [timeSteppedTrainX.length, timeSteps, colsX])
|
|
130
|
-
const targetY = tf.tensor2d(trimedTrainY, [trimedTrainY.length, colsY])
|
|
131
|
-
|
|
132
|
-
//console.log('trainX', trainX)
|
|
133
|
-
//console.log('configX', configX)
|
|
134
|
-
//console.log('inputX', inputX)
|
|
135
|
-
//console.log('inputX', targetY)
|
|
59
|
+
console.log(configX.keyNames)
|
|
60
|
+
console.log('row_1', {features: trainX[0], labels: trainY[0]})
|
|
136
61
|
|
|
62
|
+
console.log(trainY.length, trainX.length)
|
|
137
63
|
}
|
|
138
64
|
|
|
139
|
-
//callback function used to prepare X before
|
|
65
|
+
//callback function used to prepare X before flattening
|
|
140
66
|
const xCallbackFunc = ({ objRow, index }) => {
|
|
141
67
|
const curr = objRow[index]
|
|
142
|
-
const prev = objRow[index - 1]
|
|
143
68
|
|
|
144
|
-
|
|
145
|
-
if(typeof prev === 'undefined') return null
|
|
146
|
-
|
|
147
|
-
//console.log(((curr.sma_300 - curr.low) / curr.low) * 100)
|
|
148
|
-
|
|
149
|
-
const output = {
|
|
150
|
-
high: curr.high,
|
|
151
|
-
ema50IsUp: curr.ema_50 > prev.ema_50,
|
|
152
|
-
ema50GtSma200: curr.ema_50 > curr.sma_200,
|
|
153
|
-
ema50GtSma300: curr.ema_50 > curr.sma_300,
|
|
154
|
-
sma200IsUp: curr.sma_200 > prev.sma_200,
|
|
155
|
-
sma200GtSma300: curr.sma_200 > prev.sma_300,
|
|
156
|
-
sma_300IsUp: curr.sma_300 > prev.sma_300
|
|
157
|
-
}
|
|
69
|
+
const output = {}
|
|
158
70
|
|
|
159
|
-
for(const [
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
{
|
|
163
|
-
output[key] = value
|
|
71
|
+
for(const [k, v] of Object.entries(curr)) {
|
|
72
|
+
if(k.startsWith('zscore_')) {
|
|
73
|
+
output[k] = v
|
|
164
74
|
}
|
|
165
75
|
}
|
|
166
76
|
|
|
167
|
-
return output
|
|
77
|
+
return output //returning null or undefined will exclude current row X and Y from training
|
|
168
78
|
}
|
|
169
79
|
|
|
170
|
-
//callback function used to prepare Y before
|
|
80
|
+
//callback function used to prepare Y before flattening
|
|
171
81
|
const yCallbackFunc = ({ objRow, index }) => {
|
|
172
|
-
const curr = objRow[index]
|
|
173
|
-
const next = new Array(60).fill(0).map((_, i) => objRow[index + 1 + i])
|
|
174
|
-
|
|
175
|
-
//returning null or undefined will exclude current row X and Y from training
|
|
176
|
-
if (next.some(o => typeof o === 'undefined')) return null;
|
|
177
82
|
|
|
178
|
-
const
|
|
179
|
-
const entryPrice = curr.sma_300 * 0.96
|
|
180
|
-
const tp = next.some(o => o.close > entryPrice && (o.high > priceTp))
|
|
181
|
-
const sl = next.some(o => (o.low - entryPrice)/entryPrice < -0.10 && o.low < entryPrice)
|
|
83
|
+
const next = objRow[index + 1]
|
|
182
84
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if(lowestLow > entryPrice) return null
|
|
188
|
-
|
|
189
|
-
//console.log([curr.date, (lowestLow - entryPrice)/entryPrice])
|
|
85
|
+
//returning null or undefined will exclude current row X and Y from training
|
|
86
|
+
if (typeof next === 'undefined') return null
|
|
190
87
|
|
|
191
88
|
return {
|
|
192
|
-
result: Number(
|
|
89
|
+
result: Number(next.close > next.open)
|
|
193
90
|
}
|
|
194
91
|
}
|
|
195
92
|
|
|
196
|
-
test()
|
|
93
|
+
test()
|
package/src/scale.js
DELETED
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
export const scaleArrayObj = ({ arrObj, repeat = {}, minmaxRange, groups = {}, customMinMaxRanges = null, excludes = new Set() }) => {
|
|
2
|
-
|
|
3
|
-
const arrObjClone = [...arrObj]
|
|
4
|
-
const arrObjLen = arrObjClone.length;
|
|
5
|
-
const firstRow = arrObjClone[0]
|
|
6
|
-
const validCustomMinMaxRanges = typeof customMinMaxRanges === 'object' && customMinMaxRanges !== null
|
|
7
|
-
|
|
8
|
-
if (arrObjLen === 0) {
|
|
9
|
-
return {
|
|
10
|
-
scaledOutput: [],
|
|
11
|
-
scaledConfig: {}
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const inputKeyNames = Object.keys(firstRow);
|
|
16
|
-
|
|
17
|
-
const repeatedKeyNames = inputKeyNames.map(key => {
|
|
18
|
-
return repeat.hasOwnProperty(key) ? Math.max(repeat[key], 1) : 1;
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
const countRepeatedKeyNames = repeatedKeyNames.reduce((sum, rep) => sum + rep, 0);
|
|
22
|
-
|
|
23
|
-
const config = {
|
|
24
|
-
arrObjLen,
|
|
25
|
-
rangeMin: minmaxRange[0],
|
|
26
|
-
rangeMax: minmaxRange[1],
|
|
27
|
-
inputTypes: {},
|
|
28
|
-
min: {},
|
|
29
|
-
max: {},
|
|
30
|
-
groupMinMax: {},
|
|
31
|
-
repeat,
|
|
32
|
-
groups,
|
|
33
|
-
inputKeyNames,
|
|
34
|
-
outputKeyNames: new Array(countRepeatedKeyNames),
|
|
35
|
-
repeatedKeyNames
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
let keyNamesIdx = 0;
|
|
39
|
-
|
|
40
|
-
for (let i = 0; i < config.inputKeyNames.length; i++) {
|
|
41
|
-
for (let w = 0; w < config.repeatedKeyNames[i]; w++) {
|
|
42
|
-
config.outputKeyNames[keyNamesIdx++] = config.inputKeyNames[i];
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
validateUniqueProperties(config.groups);
|
|
47
|
-
|
|
48
|
-
const validInputTypes = ['number', 'boolean']
|
|
49
|
-
|
|
50
|
-
for (const key of config.inputKeyNames) {
|
|
51
|
-
|
|
52
|
-
if(excludes.has(key))
|
|
53
|
-
{
|
|
54
|
-
config.inputTypes[key] = 'excluded'
|
|
55
|
-
continue
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const firstType = typeof firstRow[key]
|
|
59
|
-
const thisGroup = findGroup(key, config.groups);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if(!validInputTypes.includes(firstType))
|
|
63
|
-
{
|
|
64
|
-
throw new Error(`Invalid input type "${firstType}" provided for key "${key}". Only accepting `)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
config.inputTypes[key] = firstType;
|
|
68
|
-
|
|
69
|
-
if(validCustomMinMaxRanges && customMinMaxRanges.hasOwnProperty(key))
|
|
70
|
-
{
|
|
71
|
-
if (thisGroup)
|
|
72
|
-
{
|
|
73
|
-
config.groupMinMax[thisGroup] = customMinMaxRanges[key]
|
|
74
|
-
}
|
|
75
|
-
else
|
|
76
|
-
{
|
|
77
|
-
config.min[key] = customMinMaxRanges[key].min;
|
|
78
|
-
config.max[key] = customMinMaxRanges[key].max;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
else {
|
|
82
|
-
if (thisGroup) {
|
|
83
|
-
config.groupMinMax[thisGroup] = { min: Infinity, max: -Infinity };
|
|
84
|
-
}
|
|
85
|
-
else {
|
|
86
|
-
config.min[key] = Infinity;
|
|
87
|
-
config.max[key] = -Infinity;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
for (const obj of arrObjClone) {
|
|
93
|
-
for (const key of config.inputKeyNames) {
|
|
94
|
-
|
|
95
|
-
if (config.inputTypes[key] === 'excluded')
|
|
96
|
-
{
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
let value = obj[key];
|
|
101
|
-
|
|
102
|
-
if (config.inputTypes[key] === 'boolean') {
|
|
103
|
-
obj[key] = Number(value);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const thisGroup = findGroup(key, config.groups);
|
|
107
|
-
|
|
108
|
-
if(validCustomMinMaxRanges === false || (validCustomMinMaxRanges && !customMinMaxRanges.hasOwnProperty(key)))
|
|
109
|
-
{
|
|
110
|
-
if (thisGroup) {
|
|
111
|
-
config.groupMinMax[thisGroup].min = Math.min(config.groupMinMax[thisGroup].min, value);
|
|
112
|
-
config.groupMinMax[thisGroup].max = Math.max(config.groupMinMax[thisGroup].max, value);
|
|
113
|
-
} else {
|
|
114
|
-
config.min[key] = Math.min(config.min[key], value);
|
|
115
|
-
config.max[key] = Math.max(config.max[key], value);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const scaledOutput = new Array(arrObjLen);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
for (let i = 0; i < arrObjLen; i++) {
|
|
126
|
-
const obj = arrObjClone[i];
|
|
127
|
-
const scaledRow = new Array(config.outputKeyNames.length);
|
|
128
|
-
let idx = 0;
|
|
129
|
-
|
|
130
|
-
for (let j = 0; j < config.inputKeyNames.length; j++) {
|
|
131
|
-
const key = config.inputKeyNames[j]
|
|
132
|
-
const value = obj[key]
|
|
133
|
-
|
|
134
|
-
if (config.inputTypes[key] === 'excluded')
|
|
135
|
-
{
|
|
136
|
-
scaledRow[idx++] = value
|
|
137
|
-
continue
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const thisGroup = findGroup(key, config.groups);
|
|
141
|
-
let minValue, maxValue
|
|
142
|
-
|
|
143
|
-
if (thisGroup) {
|
|
144
|
-
minValue = config.groupMinMax[thisGroup].min
|
|
145
|
-
maxValue = config.groupMinMax[thisGroup].max
|
|
146
|
-
} else {
|
|
147
|
-
minValue = config.min[key]
|
|
148
|
-
maxValue = config.max[key]
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const scaledValue =
|
|
152
|
-
maxValue !== minValue
|
|
153
|
-
? config.rangeMin + ((value - minValue) / (maxValue - minValue)) * (config.rangeMax - config.rangeMin)
|
|
154
|
-
: config.rangeMin;
|
|
155
|
-
|
|
156
|
-
const rep = config.repeatedKeyNames[j]
|
|
157
|
-
|
|
158
|
-
for (let w = 0; w < rep; w++) {
|
|
159
|
-
scaledRow[idx++] = scaledValue
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
scaledOutput[i] = scaledRow
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
return {
|
|
169
|
-
scaledOutput,
|
|
170
|
-
scaledConfig: config
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const validateUniqueProperties = obj => {
|
|
176
|
-
const uniqueValues = new Set();
|
|
177
|
-
const allValues = [];
|
|
178
|
-
|
|
179
|
-
for (const [key, arr] of Object.entries(obj)) {
|
|
180
|
-
uniqueValues.add(key);
|
|
181
|
-
allValues.push(key);
|
|
182
|
-
|
|
183
|
-
arr.forEach(v => {
|
|
184
|
-
uniqueValues.add(v);
|
|
185
|
-
allValues.push(v);
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
if (uniqueValues.size !== allValues.length) {
|
|
190
|
-
throw new Error('Duplicate value found between properties in validateUniqueProperties function.');
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const findGroup = (key, groups) => {
|
|
195
|
-
for (const [groupK, groupV] of Object.entries(groups)) {
|
|
196
|
-
if (groupV.includes(key)) {
|
|
197
|
-
return groupK;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
return null;
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
const validateConfig = config => {
|
|
205
|
-
|
|
206
|
-
if(!config) return false
|
|
207
|
-
|
|
208
|
-
const requiredKeys = [
|
|
209
|
-
"rangeMin",
|
|
210
|
-
"rangeMax",
|
|
211
|
-
"inputTypes",
|
|
212
|
-
"min",
|
|
213
|
-
"max",
|
|
214
|
-
"groupMinMax",
|
|
215
|
-
"repeat",
|
|
216
|
-
"groups",
|
|
217
|
-
"inputKeyNames",
|
|
218
|
-
"outputKeyNames",
|
|
219
|
-
"repeatedKeyNames"
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
// Check for missing keys
|
|
223
|
-
for (const key of requiredKeys) {
|
|
224
|
-
if (!config.hasOwnProperty(key)) {
|
|
225
|
-
throw new Error(`Missing key "${key}" in config.`);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const {
|
|
230
|
-
rangeMin,
|
|
231
|
-
rangeMax,
|
|
232
|
-
inputTypes,
|
|
233
|
-
min,
|
|
234
|
-
max,
|
|
235
|
-
groupMinMax,
|
|
236
|
-
repeat,
|
|
237
|
-
groups,
|
|
238
|
-
inputKeyNames,
|
|
239
|
-
outputKeyNames,
|
|
240
|
-
repeatedKeyNames
|
|
241
|
-
} = config;
|
|
242
|
-
|
|
243
|
-
// Validate rangeMin and rangeMax are numbers and in proper order
|
|
244
|
-
if (typeof rangeMin !== 'number' || typeof rangeMax !== 'number') {
|
|
245
|
-
throw new Error("rangeMin and rangeMax must be numbers.");
|
|
246
|
-
}
|
|
247
|
-
if (rangeMin >= rangeMax) {
|
|
248
|
-
throw new Error("rangeMin must be less than rangeMax.");
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// Helper to check if a value is a plain object (and not null or an array)
|
|
252
|
-
const isPlainObject = (obj) => typeof obj === 'object' && obj !== null && !Array.isArray(obj);
|
|
253
|
-
|
|
254
|
-
if (!isPlainObject(inputTypes)) {
|
|
255
|
-
throw new Error("inputTypes must be an object.");
|
|
256
|
-
}
|
|
257
|
-
if (!isPlainObject(min)) {
|
|
258
|
-
throw new Error("min must be an object.");
|
|
259
|
-
}
|
|
260
|
-
if (!isPlainObject(max)) {
|
|
261
|
-
throw new Error("max must be an object.");
|
|
262
|
-
}
|
|
263
|
-
if (!isPlainObject(groupMinMax)) {
|
|
264
|
-
throw new Error("groupMinMax must be an object.");
|
|
265
|
-
}
|
|
266
|
-
if (!isPlainObject(repeat)) {
|
|
267
|
-
throw new Error("repeat must be an object.");
|
|
268
|
-
}
|
|
269
|
-
if (!isPlainObject(groups)) {
|
|
270
|
-
throw new Error("groups must be an object.");
|
|
271
|
-
}
|
|
272
|
-
if(!Array.isArray(inputKeyNames))
|
|
273
|
-
{
|
|
274
|
-
throw new Error("inputKeyNames must be an array.");
|
|
275
|
-
}
|
|
276
|
-
if(!Array.isArray(outputKeyNames))
|
|
277
|
-
{
|
|
278
|
-
throw new Error("outputKeyNames must be an array.");
|
|
279
|
-
}
|
|
280
|
-
if(!Array.isArray(repeatedKeyNames))
|
|
281
|
-
{
|
|
282
|
-
throw new Error("repeatedKeyNames must be an array.");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
return true;
|
|
286
|
-
}
|