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 CHANGED
@@ -1,196 +1,130 @@
1
- # **xy-scale.js**
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` is a robust toolkit designed for scaling and preparing datasets in JavaScript, specifically tailored for machine learning applications. It offers essential utilities for:
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
- - **Dataset Splitting:** Dividing data into training and testing subsets.
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
- - `arrObj` (Array of Objects): Input data containing features (X) and labels (Y) e.g, `[{high: 10, low: 8, close: 9, dayOfTheWeek: 'monday'}, {high: 11, low: 8, close: 10, dayOfTheWeek: 'tuesday'}]`. Each Item an be a `number`, `string` or `boolean`.
42
- - `trainingSplit` (Number, optional): Fraction of data for training (default: `0.8`).
43
- - `yCallbackFunc` (Function): Extracts Y values from each row. Returns `null` or `undefined` to exclude a row.
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
- ### 2. `parseProductionX`
23
+ ## Main functions
60
24
 
61
- Parses and scales unseen production data for feature preparation.
25
+ ### parseTrainingXY
62
26
 
63
- **Parameters:**
27
+ Builds supervised-learning datasets and splits them into training and testing arrays.
64
28
 
65
- - `arrObj` (Array of Objects): Input data containing features (X) and labels (Y) e.g, `[{high: 10, low: 8, close: 9, dayOfTheWeek: 'monday'}, {high: 11, low: 8, close: 10, dayOfTheWeek: 'tuesday'}]`. Each Item an be a `number`, `string` or `boolean`.
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
- **Returns:**
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
- - `X`: Scaled feature array.
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
- ### 3. `descaleArrayObj`
47
+ `configX.keyNames` and `configY.keyNames` preserve the object-key order used when flattening each callback result into an array.
80
48
 
81
- Reconstructs original data values from scaled arrays.
49
+ ### parseProductionX
82
50
 
83
- **Parameters:**
51
+ Builds production-ready feature arrays from already-prepared rows.
84
52
 
85
- - `scaled` (Array): Scaled data array (e.g., `trainX`, `trainY`, etc.).
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
- **Returns:**
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
- - An array of objects resembling the original input data structure.
61
+ #### Returns
92
62
 
93
- ---
63
+ - `X`
64
+ - `configX`: `{ keyNames: [...] }`
94
65
 
95
- ### 4. `arrayToTimesteps`
66
+ ### arrayToTimesteps
96
67
 
97
- Converts a flat array into sequences for time-series modeling.
68
+ Converts a flat array into overlapping sequences for time-series models.
98
69
 
99
- **Parameters:**
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
- **Returns:**
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, parseProductionX, descaleArrayObj, arrayToTimesteps } from 'xy-scale';
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
- (async () => {
120
- const myArray = await loadFile({ fileName: '1d-spy.json', pathName: 'datasets' });
121
-
122
- // Callback function for parsing `X` features
123
- const xCallbackFunc = ({ objRow, index }) => {
124
- const curr = objRow[index];
125
- const prev = objRow[index - 1];
126
- if (typeof prev === 'undefined') return null;
127
-
128
- const { open, high, low, close, volume } = curr;
129
- return {
130
- open,
131
- high,
132
- low,
133
- close,
134
- change: open - prev.close,
135
- top: high - Math.max(open, close),
136
- bottom: low - Math.min(open, close),
137
- body: open - close,
138
- };
139
- };
140
-
141
- // Callback function for parsing `Y` labels
142
- const yCallbackFunc = ({ objRow, index }) => {
143
- const curr = objRow[index];
144
- const next = objRow[index + 1];
145
- if (typeof next === 'undefined') return null;
146
-
147
- return {
148
- label_1: next.open > curr.close,
149
- label_2: next.close > curr.close,
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 [MIT License](LICENSE).
130
+ This project is licensed under the MIT License.
@@ -1 +1 @@
1
- var XY_Scale;(()=>{"use strict";var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},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:()=>c,parseProductionX:()=>l,parseTrainingXY:()=>u});const n=({arrObj:e,repeat:t={},minmaxRange:n,groups:o={},customMinMaxRanges:s=null,excludes:i=new Set})=>{const u=[...e],l=u.length,c=u[0],p="object"==typeof s&&null!==s;if(0===l)return{scaledOutput:[],scaledConfig:{}};const f=Object.keys(c),m=f.map((e=>t.hasOwnProperty(e)?Math.max(t[e],1):1)),d=m.reduce(((e,t)=>e+t),0),g={arrObjLen:l,rangeMin:n[0],rangeMax:n[1],inputTypes:{},min:{},max:{},groupMinMax:{},repeat:t,groups:o,inputKeyNames:f,outputKeyNames:new Array(d),repeatedKeyNames:m};let h=0;for(let e=0;e<g.inputKeyNames.length;e++)for(let t=0;t<g.repeatedKeyNames[e];t++)g.outputKeyNames[h++]=g.inputKeyNames[e];r(g.groups);const y=["number","boolean"];for(const e of g.inputKeyNames){if(i.has(e)){g.inputTypes[e]="excluded";continue}const t=typeof c[e],n=a(e,g.groups);if(!y.includes(t))throw new Error(`Invalid input type "${t}" provided for key "${e}". Only accepting `);g.inputTypes[e]=t,p&&s.hasOwnProperty(e)?n?g.groupMinMax[n]=s[e]:(g.min[e]=s[e].min,g.max[e]=s[e].max):n?g.groupMinMax[n]={min:1/0,max:-1/0}:(g.min[e]=1/0,g.max[e]=-1/0)}for(const e of u)for(const t of g.inputKeyNames){if("excluded"===g.inputTypes[t])continue;let n=e[t];"boolean"===g.inputTypes[t]&&(e[t]=Number(n));const r=a(t,g.groups);(!1===p||p&&!s.hasOwnProperty(t))&&(r?(g.groupMinMax[r].min=Math.min(g.groupMinMax[r].min,n),g.groupMinMax[r].max=Math.max(g.groupMinMax[r].max,n)):(g.min[t]=Math.min(g.min[t],n),g.max[t]=Math.max(g.max[t],n)))}const x=new Array(l);for(let e=0;e<l;e++){const t=u[e],n=new Array(g.outputKeyNames.length);let r=0;for(let e=0;e<g.inputKeyNames.length;e++){const o=g.inputKeyNames[e],s=t[o];if("excluded"===g.inputTypes[o]){n[r++]=s;continue}const i=a(o,g.groups);let u,l;i?(u=g.groupMinMax[i].min,l=g.groupMinMax[i].max):(u=g.min[o],l=g.max[o]);const c=l!==u?g.rangeMin+(s-u)/(l-u)*(g.rangeMax-g.rangeMin):g.rangeMin,p=g.repeatedKeyNames[e];for(let e=0;e<p;e++)n[r++]=c}x[e]=n}return{scaledOutput:x,scaledConfig:g}},r=e=>{const t=new Set,n=[];for(const[r,a]of Object.entries(e))t.add(r),n.push(r),a.forEach((e=>{t.add(e),n.push(e)}));if(t.size!==n.length)throw new Error("Duplicate value found between properties in validateUniqueProperties function.")},a=(e,t)=>{for(const[n,r]of Object.entries(t))if(r.includes(e))return n;return null},o=(e,t)=>{const n=Object.keys(e);if(!Array.isArray(t))throw new Error('Property "excludes" must be an array.');for(const e of t)if(!n.includes(e))throw new Error(`An item in "excludes" property was not found in "arrObj".\n\nexcludes: ${JSON.stringify(t)}\n\narrObj: ${JSON.stringify(n)}`)},s=(e,{min:t=-1/0,max:n=1/0},r)=>{if(!Array.isArray(e))throw new Error(`Invalid property. "${r}" expected an array.`);if(e.length<t)throw new Error(`Invalid property value. Array "${r}" expected at least ${n} items.`);if(e.length>n)throw new Error(`Invalid property value. Array "${r}" expected at max ${n} items.`);return!0},i=e=>{for(const[t,n]of Object.entries(e)){if("number"==typeof n&&Number.isNaN(n))throw new Error(`Invalid value at index 0 property "${t}": value is "${n}". Expected a numeric value.`);if(null===n)throw new Error(`Invalid value at index 0 property "${t}": value is "${n}".`)}return!0},u=({arrObj:e=[],trainingSplit:t=.8,repeat:r={},yCallbackFunc:a=e=>e,xCallbackFunc:u=e=>e,validateRows:l=()=>!0,groups:c={},shuffle:p=!1,minmaxRange:f=[0,1],balancing:m="",state:d={},customMinMaxRanges:g={},excludes:h=[]})=>{let y=[],x=[];s(e,{min:5},"parseTrainingXY"),i(e[0]),o(e[0],h);for(let t=0;t<e.length;t++){if(!l({objRow:e,index:t,state:d}))continue;const n=u({objRow:e,index:t,state:d}),r=a({objRow:e,index:t,state:d});null!=n&&null!=r&&(y.push(n),x.push(r))}if(p){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 n=Array.from({length:e.length},((e,t)=>t));for(let e=n.length-1;e>0;e--){const t=Math.floor(Math.random()*(e+1));[n[e],n[t]]=[n[t],n[e]]}return{shuffledX:n.map((t=>e[t])),shuffledY:n.map((e=>t[e]))}})(y,x);y=e,x=t}const M=new Set(h);let{scaledOutput:b,scaledConfig:w}=n({arrObj:y,repeat:r,groups:c,minmaxRange:f,customMinMaxRanges:g,excludes:M});const O=x.length,v=new Array(O),j={keyNames:Object.keys(x[0])};for(let e=0;e<O;e++)v[e]=Object.values(x[e]);const N=Math.floor(b.length*t);let E=b.slice(0,N),X=v.slice(0,N),R=b.slice(N),S=v.slice(N);if(m){let e;if("oversample"===m)e=((e,t)=>{const n={},r={};t.forEach(((a,o)=>{n[a]||(n[a]=0,r[a]=[]),n[a]++,r[a].push([e[o],t[o]])}));const a=Math.max(...Object.values(n)),o=[],s=[];return Object.keys(r).forEach((e=>{const t=r[e],n=t.length;for(let e=0;e<a;e++){const r=t[e%n];o.push(r[0]),s.push(r[1])}})),{X:o,Y:s}})(E,X),E=e.X,X=e.Y;else{if("undersample"!==m)throw Error('balancing argument only accepts "false", "oversample" and "undersample". Defaults to "false".');e=((e,t)=>{const n={},r={};t.forEach(((a,o)=>{n[a]||(n[a]=0,r[a]=[]),n[a]++,r[a].push([e[o],t[o]])}));const a=Math.min(...Object.values(n)),o=[],s=[];return Object.keys(r).forEach((e=>{const t=r[e];for(let e=0;e<a;e++){const n=t[e];o.push(n[0]),s.push(n[1])}})),{X:o,Y:s}})(E,X),E=e.X,X=e.Y}}return{trainX:E,trainY:X,testX:R,testY:S,configX:w,configY:j}},l=({arrObj:e=[],repeat:t={},xCallbackFunc:r=e=>e,validateRows:a=()=>!0,groups:u={},shuffle:l=!1,minmaxRange:c=[0,1],state:p={},customMinMaxRanges:f,excludes:m=[]})=>{let d=[];s(e,{min:5},"parseProductionX"),i(e[0]),o(e[0],m);for(let t=0;t<e.length;t++){if(!a(e[t]))continue;const n=r({objRow:e,index:t,state:p});null!=n&&!1!==n&&d.push(n)}l&&(d=(e=>{const t=[...e];for(let e=t.length-1;e>0;e--){const n=Math.floor(Math.random()*(e+1));[t[e],t[n]]=[t[n],t[e]]}return t})(d));const{scaledOutput:g,scaledConfig:h}=n({arrObj:d,repeat:t,groups:u,minmaxRange:c,customMinMaxRanges:f,excludes:new Set(m)});return{X:g,configX:h}},c=(e,t)=>{if(0===t)return e;if(t<0)throw new Error("timeSteps must be greater than 0");const n=[];for(let r=0;r<=e.length-t;r++)n.push(e.slice(r,r+t));return n};XY_Scale=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.2",
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.4.7",
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 excludesSet = new Set(excludes)
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(scaledX.length * trainingSplit)
65
+ const splitIndex = Math.floor(flatX.length * trainingSplit)
72
66
 
73
- let trainX = scaledX.slice(0, splitIndex)
67
+ let trainX = flatX.slice(0, splitIndex)
74
68
  let trainY = flatY.slice(0, splitIndex)
75
- let testX = scaledX.slice(splitIndex)
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
- // Scale X
150
- const {
151
- scaledOutput: scaledX,
152
- scaledConfig: configX
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: scaledX,
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 ohlcv = (await loadFile({fileName: 'btc-1d.json', pathName: 'datasets'}))
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 parsedOhlcv = indicators.getData({dateFormat: 'milliseconds'})
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
- //console.log(parsedOhlcv.slice(-1))
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
- const {scaledGroups} = indicators
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
- //console.log(scaledGroups)
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: parsedOhlcv,
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 curr.ema_50 > curr.sma_300 && curr.sma_200 > curr.sma_300 && (curr.price_x_sma_300 === -1 || curr.price_x_sma_300 === 1)
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.outputKeyNames)
67
- console.log(configX.inputTypes)
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 scaling
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
- //returning null or undefined will exclude current row X and Y from training
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 [key, value] of Object.entries(curr))
160
- {
161
- if(key.includes('minmax'))
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 scaling
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 priceTp = curr.sma_300 * 1.3
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
- const highestHigh = Math.max(...next.map(o => o.high))
184
- const lowestLow = Math.min(...next.slice(0, 5).map(o => o.low))
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(tp === true && sl === false)
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
- }