@mathieuc/tradingview 2.0.7 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,403 @@
1
+ const { genSessionID } = require('../utils');
2
+ const { parseCompressed } = require('../protocol');
3
+ const graphicParser = require('./graphicParser');
4
+
5
+ const PineIndicator = require('../classes/PineIndicator');
6
+ const BuiltInIndicator = require('../classes/BuiltInIndicator');
7
+
8
+ /**
9
+ * Get pine inputs
10
+ * @param {PineIndicator | BuiltInIndicator} options
11
+ */
12
+ function getInputs(options) {
13
+ if (options instanceof PineIndicator) {
14
+ const pineInputs = { text: options.script };
15
+
16
+ if (options.pineId) pineInputs.pineId = options.pineId;
17
+ if (options.pineVersion) pineInputs.pineVersion = options.pineVersion;
18
+
19
+ Object.keys(options.inputs).forEach((inputID) => {
20
+ const input = options.inputs[inputID];
21
+
22
+ pineInputs[inputID] = {
23
+ v: input.value,
24
+ f: input.isFake,
25
+ t: input.type,
26
+ };
27
+ });
28
+
29
+ return pineInputs;
30
+ }
31
+
32
+ return options.options;
33
+ }
34
+
35
+ /**
36
+ * @typedef {Object} TradeReport Trade report
37
+
38
+ * @property {Object} entry Trade entry
39
+ * @property {string} entry.name Trade name
40
+ * @property {'long' | 'short'} entry.type Entry type (long/short)
41
+ * @property {number} entry.value Entry price value
42
+ * @property {number} entry.time Entry timestamp
43
+
44
+ * @property {Object} exit Trade exit
45
+ * @property {'' | string} exit.name Trade name ('' if false exit)
46
+ * @property {number} exit.value Exit price value
47
+ * @property {number} exit.time Exit timestamp
48
+
49
+ * @property {number} quantity Trade quantity
50
+ * @property {RelAbsValue} profit Trade profit
51
+ * @property {RelAbsValue} cumulative Trade cummulative profit
52
+ * @property {RelAbsValue} runup Trade run-up
53
+ * @property {RelAbsValue} drawdown Trade drawdown
54
+ */
55
+
56
+ /**
57
+ * @typedef {Object} PerfReport
58
+ * @property {number} avgBarsInTrade Average bars in trade
59
+ * @property {number} avgBarsInWinTrade Average bars in winning trade
60
+ * @property {number} avgBarsInLossTrade Average bars in losing trade
61
+ * @property {number} avgTrade Average trade gain
62
+ * @property {number} avgTradePercent Average trade performace
63
+ * @property {number} avgLosTrade Average losing trade gain
64
+ * @property {number} avgLosTradePercent Average losing trade performace
65
+ * @property {number} avgWinTrade Average winning trade gain
66
+ * @property {number} avgWinTradePercent Average winning trade performace
67
+ * @property {number} commissionPaid Commission paid
68
+ * @property {number} grossLoss Gross loss value
69
+ * @property {number} grossLossPercent Gross loss percent
70
+ * @property {number} grossProfit Gross profit
71
+ * @property {number} grossProfitPercent Gross profit percent
72
+ * @property {number} largestLosTrade Largest losing trade gain
73
+ * @property {number} largestLosTradePercent Largent losing trade performance (percentage)
74
+ * @property {number} largestWinTrade Largest winning trade gain
75
+ * @property {number} largestWinTradePercent Largest winning trade performance (percentage)
76
+ * @property {number} marginCalls Margin calls
77
+ * @property {number} maxContractsHeld Max Contracts Held
78
+ * @property {number} netProfit Net profit
79
+ * @property {number} netProfitPercent Net performance (percentage)
80
+ * @property {number} numberOfLosingTrades Number of losing trades
81
+ * @property {number} numberOfWiningTrades Number of winning trades
82
+ * @property {number} percentProfitable Strategy winrate
83
+ * @property {number} profitFactor Profit factor
84
+ * @property {number} ratioAvgWinAvgLoss Ratio Average Win / Average Loss
85
+ * @property {number} totalOpenTrades Total open trades
86
+ * @property {number} totalTrades Total trades
87
+ */
88
+
89
+ /**
90
+ * @typedef {Object} StrategyReport
91
+ * @property {'EUR' | 'USD' | 'JPY' | '' | 'CHF'} [currency] Selected currency
92
+ * @property {TradeReport[]} trades Trade list starting by the last
93
+ * @property {Object} history History Chart value
94
+ * @property {number[]} [history.buyHold] Buy hold values
95
+ * @property {number[]} [history.buyHoldPercent] Buy hold percent values
96
+ * @property {number[]} [history.drawDown] Drawdown values
97
+ * @property {number[]} [history.drawDownPercent] Drawdown percent values
98
+ * @property {number[]} [history.equity] Equity values
99
+ * @property {number[]} [history.equityPercent] Equity percent values
100
+ * @property {Object} performance Strategy performance
101
+ * @property {PerfReport} [performance.all] Strategy long/short performances
102
+ * @property {PerfReport} [performance.long] Strategy long performances
103
+ * @property {PerfReport} [performance.short] Strategy short performances
104
+ * @property {number} [performance.buyHoldReturn] Strategy Buy & Hold Return
105
+ * @property {number} [performance.buyHoldReturnPercent] Strategy Buy & Hold Return percent
106
+ * @property {number} [performance.maxDrawDown] Strategy max drawdown
107
+ * @property {number} [performance.maxDrawDownPercent] Strategy max drawdown percent
108
+ * @property {number} [performance.openPL] Strategy Open P&L (Profit And Loss)
109
+ * @property {number} [performance.openPLPercent] Strategy Open P&L (Profit And Loss) percent
110
+ * @property {number} [performance.sharpeRatio] Strategy Sharpe Ratio
111
+ * @property {number} [performance.sortinoRatio] Strategy Sortino Ratio
112
+ */
113
+
114
+ /**
115
+ * @param {import('./session').ChartSessionBridge} chartSession
116
+ */
117
+ module.exports = (chartSession) => class ChartStudy {
118
+ #studID = genSessionID('st');
119
+
120
+ #studyListeners = chartSession.studyListeners;
121
+
122
+ /**
123
+ * Table of periods values indexed by timestamp
124
+ * @type {Object<number, {}[]>}
125
+ */
126
+ #periods = {};
127
+
128
+ /** @return {{}[]} List of periods values */
129
+ get periods() {
130
+ return Object.values(this.#periods).sort((a, b) => b.$time - a.$time);
131
+ }
132
+
133
+ /**
134
+ * List of graphic xPos indexes
135
+ * @type {number[]}
136
+ */
137
+ #indexes = [];
138
+
139
+ /**
140
+ * Table of graphic drawings indexed by type and ID
141
+ * @type {Object<string, Object<number, {}>>}
142
+ */
143
+ #graphic = {};
144
+
145
+ /**
146
+ * Table of graphic drawings indexed by type
147
+ * @return {import('./graphicParser').GraphicData}
148
+ */
149
+ get graphic() {
150
+ const translator = {};
151
+
152
+ Object.keys(chartSession.indexes)
153
+ .sort((a, b) => chartSession.indexes[b] - chartSession.indexes[a])
154
+ .forEach((r, n) => { translator[r] = n; });
155
+
156
+ const indexes = this.#indexes.map((i) => translator[i]);
157
+ return graphicParser(this.#graphic, indexes);
158
+ }
159
+
160
+ /** @type {StrategyReport} */
161
+ #strategyReport = {
162
+ trades: [],
163
+ history: {},
164
+ performance: {},
165
+ };
166
+
167
+ /** @return {StrategyReport} Get the strategy report if available */
168
+ get strategyReport() {
169
+ return this.#strategyReport;
170
+ }
171
+
172
+ #callbacks = {
173
+ studyCompleted: [],
174
+ update: [],
175
+
176
+ event: [],
177
+ error: [],
178
+ };
179
+
180
+ /**
181
+ * @param {ChartEvent} ev Client event
182
+ * @param {...{}} data Packet data
183
+ */
184
+ #handleEvent(ev, ...data) {
185
+ this.#callbacks[ev].forEach((e) => e(...data));
186
+ this.#callbacks.event.forEach((e) => e(ev, ...data));
187
+ }
188
+
189
+ #handleError(...msgs) {
190
+ if (this.#callbacks.error.length === 0) console.error(...msgs);
191
+ else this.#handleEvent('error', ...msgs);
192
+ }
193
+
194
+ /**
195
+ * @param {PineIndicator | BuiltInIndicator} indicator Indicator object instance
196
+ */
197
+ constructor(indicator) {
198
+ if (!(indicator instanceof PineIndicator) && !(indicator instanceof BuiltInIndicator)) {
199
+ throw new Error(`Indicator argument must be an instance of PineIndicator or BuiltInIndicator.
200
+ Please use 'TradingView.getIndicator(...)' function.`);
201
+ }
202
+
203
+ /** @type {PineIndicator | BuiltInIndicator} Indicator instance */
204
+ this.instance = indicator;
205
+
206
+ this.#studyListeners[this.#studID] = async (packet) => {
207
+ if (global.TW_DEBUG) console.log('§90§30§105 STUDY §0 DATA', packet);
208
+
209
+ if (packet.type === 'study_completed') {
210
+ this.#handleEvent('studyCompleted');
211
+ return;
212
+ }
213
+
214
+ if (['timescale_update', 'du'].includes(packet.type)) {
215
+ const changes = [];
216
+ const data = packet.data[1][this.#studID];
217
+
218
+ if (data && data.st && data.st[0]) {
219
+ data.st.forEach((p) => {
220
+ const period = {};
221
+
222
+ p.v.forEach((plot, i) => {
223
+ if (!this.instance.plots) {
224
+ period[i === 0 ? '$time' : `plot_${i - 1}`] = plot;
225
+ return;
226
+ }
227
+ const plotName = (i === 0 ? '$time' : this.instance.plots[`plot_${i - 1}`]);
228
+ if (plotName && !period[plotName]) period[plotName] = plot;
229
+ else period[`plot_${i - 1}`] = plot;
230
+ });
231
+
232
+ this.#periods[p.v[0]] = period;
233
+ });
234
+
235
+ changes.push('plots');
236
+ }
237
+
238
+ if (data.ns && data.ns.d) {
239
+ const parsed = JSON.parse(data.ns.d);
240
+
241
+ if (parsed.graphicsCmds) {
242
+ if (parsed.graphicsCmds.erase) {
243
+ parsed.graphicsCmds.erase.forEach((instruction) => {
244
+ // console.log('Erase', instruction);
245
+ if (instruction.action === 'all') {
246
+ if (!instruction.type) {
247
+ Object.keys(this.#graphic).forEach((drawType) => {
248
+ this.#graphic[drawType] = {};
249
+ });
250
+ } else delete this.#graphic[instruction.type];
251
+ return;
252
+ }
253
+
254
+ if (instruction.action === 'one') {
255
+ delete this.#graphic[instruction.type][instruction.id];
256
+ }
257
+ // Can an 'instruction' contains other things ?
258
+ });
259
+ }
260
+
261
+ if (parsed.graphicsCmds.create) {
262
+ Object.keys(parsed.graphicsCmds.create).forEach((drawType) => {
263
+ if (!this.#graphic[drawType]) this.#graphic[drawType] = {};
264
+ parsed.graphicsCmds.create[drawType].forEach((group) => {
265
+ group.data.forEach((item) => {
266
+ this.#graphic[drawType][item.id] = item;
267
+ });
268
+ });
269
+ });
270
+ }
271
+
272
+ // console.log('graphicsCmds', Object.keys(parsed.graphicsCmds));
273
+ // Can 'graphicsCmds' contains other things ?
274
+
275
+ changes.push('graphic');
276
+ }
277
+
278
+ if (parsed.data && parsed.data.report && parsed.data.report.performance) {
279
+ this.#strategyReport.performance = parsed.data.report.performance;
280
+ changes.push('perfReport');
281
+ }
282
+
283
+ if (parsed.dataCompressed) {
284
+ const parsedC = await parseCompressed(parsed.dataCompressed);
285
+
286
+ this.#strategyReport = {
287
+ currency: parsedC.report.currency,
288
+
289
+ trades: parsedC.report.trades.reverse().map((t) => ({
290
+ entry: {
291
+ name: t.e.c,
292
+ type: (t.e.tp[0] === 's' ? 'short' : 'long'),
293
+ value: t.e.p,
294
+ time: t.e.tm,
295
+ },
296
+ exit: {
297
+ name: t.x.c,
298
+ value: t.x.p,
299
+ time: t.x.tm,
300
+ },
301
+ quantity: t.q,
302
+ profit: t.tp,
303
+ cumulative: t.cp,
304
+ runup: t.rn,
305
+ drawdown: t.dd,
306
+ })),
307
+
308
+ history: {
309
+ buyHold: parsedC.report.buyHold,
310
+ buyHoldPercent: parsedC.report.buyHoldPercent,
311
+ drawDown: parsedC.report.drawDown,
312
+ drawDownPercent: parsedC.report.drawDownPercent,
313
+ equity: parsedC.report.equity,
314
+ equityPercent: parsedC.report.equityPercent,
315
+ },
316
+
317
+ performance: parsedC.report.performance,
318
+ };
319
+
320
+ changes.push('fullReport');
321
+ }
322
+ }
323
+
324
+ if (data.ns.indexes && typeof data.ns.indexes === 'object') {
325
+ this.#indexes = data.ns.indexes;
326
+ }
327
+
328
+ this.#handleEvent('update', changes);
329
+ return;
330
+ }
331
+
332
+ if (packet.type === 'study_error') {
333
+ this.#handleError(packet.data[3], packet.data[4]);
334
+ }
335
+ };
336
+
337
+ chartSession.send('create_study', [
338
+ chartSession.sessionID,
339
+ `${this.#studID}`,
340
+ 'st1',
341
+ '$prices',
342
+ this.instance.type,
343
+ getInputs(this.instance),
344
+ ]);
345
+ }
346
+
347
+ /**
348
+ * @param {PineIndicator | BuiltInIndicator} indicator Indicator instance
349
+ */
350
+ setIndicator(indicator) {
351
+ if (!(indicator instanceof PineIndicator) && !(indicator instanceof BuiltInIndicator)) {
352
+ throw new Error(`Indicator argument must be an instance of PineIndicator or BuiltInIndicator.
353
+ Please use 'TradingView.getIndicator(...)' function.`);
354
+ }
355
+
356
+ this.instance = indicator;
357
+
358
+ chartSession.send('modify_study', [
359
+ chartSession.sessionID,
360
+ `${this.#studID}`,
361
+ 'st1',
362
+ getInputs(this.instance),
363
+ ]);
364
+ }
365
+
366
+ /**
367
+ * When the indicator is ready
368
+ * @param {() => void} cb
369
+ * @event
370
+ */
371
+ onReady(cb) {
372
+ this.#callbacks.studyCompleted.push(cb);
373
+ }
374
+
375
+ /** @typedef {'plots' | 'perfReport' | 'fullReport'} UpdateChangeType */
376
+
377
+ /**
378
+ * When an indicator update happens
379
+ * @param {(changes: UpdateChangeType[]) => void} cb
380
+ * @event
381
+ */
382
+ onUpdate(cb) {
383
+ this.#callbacks.update.push(cb);
384
+ }
385
+
386
+ /**
387
+ * When indicator error happens
388
+ * @param {(...any) => void} cb Callback
389
+ * @event
390
+ */
391
+ onError(cb) {
392
+ this.#callbacks.error.push(cb);
393
+ }
394
+
395
+ /** Remove the study */
396
+ remove() {
397
+ chartSession.send('remove_study', [
398
+ chartSession.sessionID,
399
+ this.#studID,
400
+ ]);
401
+ delete this.#studyListeners[this.#studID];
402
+ }
403
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @typedef {'VbPFixed@tv-basicstudies-139!'
3
+ * | 'VbPFixed@tv-volumebyprice-53!'
4
+ * | 'VbPSessions@tv-volumebyprice-53'
5
+ * | 'VbPSessionsRough@tv-volumebyprice-53!'
6
+ * | 'VbPSessionsDetailed@tv-volumebyprice-53!'
7
+ * | 'VbPVisible@tv-volumebyprice-53'} BuiltInIndicatorType Built-in indicator type
8
+ */
9
+
10
+ /**
11
+ * @typedef {'rowsLayout' | 'rows' | 'volume'
12
+ * | 'vaVolume' | 'subscribeRealtime'
13
+ * | 'first_bar_time' | 'first_visible_bar_time'
14
+ * | 'last_bar_time' | 'last_visible_bar_time'
15
+ * | 'extendPocRight'} BuiltInIndicatorOption Built-in indicator Option
16
+ */
17
+
18
+ const defaultValues = {
19
+ 'VbPFixed@tv-basicstudies-139!': {
20
+ rowsLayout: 'Number Of Rows',
21
+ rows: 24,
22
+ volume: 'Up/Down',
23
+ vaVolume: 70,
24
+ subscribeRealtime: false,
25
+ first_bar_time: NaN,
26
+ last_bar_time: Date.now(),
27
+ },
28
+ 'VbPFixed@tv-volumebyprice-53!': {
29
+ rowsLayout: 'Number Of Rows',
30
+ rows: 24,
31
+ volume: 'Up/Down',
32
+ vaVolume: 70,
33
+ subscribeRealtime: false,
34
+ first_bar_time: NaN,
35
+ last_bar_time: Date.now(),
36
+ },
37
+ 'VbPSessions@tv-volumebyprice-53': {
38
+ rowsLayout: 'Number Of Rows',
39
+ rows: 24,
40
+ volume: 'Up/Down',
41
+ vaVolume: 70,
42
+ extendPocRight: false,
43
+ },
44
+ 'VbPSessionsRough@tv-volumebyprice-53!': {
45
+ volume: 'Up/Down',
46
+ vaVolume: 70,
47
+ },
48
+ 'VbPSessionsDetailed@tv-volumebyprice-53!': {
49
+ volume: 'Up/Down',
50
+ vaVolume: 70,
51
+ subscribeRealtime: false,
52
+ first_visible_bar_time: NaN,
53
+ last_visible_bar_time: Date.now(),
54
+ },
55
+ 'VbPVisible@tv-volumebyprice-53': {
56
+ rowsLayout: 'Number Of Rows',
57
+ rows: 24,
58
+ volume: 'Up/Down',
59
+ vaVolume: 70,
60
+ subscribeRealtime: false,
61
+ first_visible_bar_time: NaN,
62
+ last_visible_bar_time: Date.now(),
63
+ },
64
+ };
65
+
66
+ module.exports = class BuiltInIndicator {
67
+ /** @type {BuiltInIndicatorType} */
68
+ #type;
69
+
70
+ /** @return {BuiltInIndicatorType} Indicator script */
71
+ get type() {
72
+ return this.#type;
73
+ }
74
+
75
+ /** @type {Object<string, any>} */
76
+ #options = {};
77
+
78
+ /** @return {Object<string, any>} Indicator script */
79
+ get options() {
80
+ return this.#options;
81
+ }
82
+
83
+ /**
84
+ * @param {BuiltInIndicatorType} type Buit-in indocator raw type
85
+ */
86
+ constructor(type = '') {
87
+ if (!type) throw new Error(`Wrong buit-in indicator type "${type}".`);
88
+
89
+ this.#type = type;
90
+ if (defaultValues[type]) this.#options = { ...defaultValues[type] };
91
+ }
92
+
93
+ /**
94
+ * Set an option
95
+ * @param {BuiltInIndicatorOption} key The option you want to change
96
+ * @param {*} value The new value of the property
97
+ * @param {boolean} FORCE Ignore type and key verifications
98
+ */
99
+ setOption(key, value, FORCE = false) {
100
+ if (FORCE) {
101
+ this.#options[key] = value;
102
+ return;
103
+ }
104
+
105
+ if (defaultValues[this.#type] && defaultValues[this.#type][key] !== undefined) {
106
+ const requiredType = typeof defaultValues[this.#type][key];
107
+ const valType = typeof value;
108
+ if (requiredType !== valType) {
109
+ throw new Error(`Wrong '${key}' value type '${valType}' (must be '${requiredType}')`);
110
+ }
111
+ }
112
+
113
+ if (defaultValues[this.#type] && defaultValues[this.#type][key] === undefined) {
114
+ throw new Error(`Option '${key}' is denied with '${this.#type}' indicator`);
115
+ }
116
+
117
+ this.#options[key] = value;
118
+ }
119
+ };
@@ -0,0 +1,132 @@
1
+ /**
2
+ * @typedef {Object} IndicatorInput
3
+ * @property {string} name Input name
4
+ * @property {string} inline Input inline name
5
+ * @property {string} [internalID] Input internal ID
6
+ * @property {string} [tooltip] Input tooltip
7
+ * @property {'text' | 'source' | 'integer' | 'float' | 'resolution' | 'bool'} type Input type
8
+ * @property {string | number | boolean} value Input default value
9
+ * @property {boolean} isHidden If the input is hidden
10
+ * @property {boolean} isFake If the input is fake
11
+ * @property {string[]} [options] Input options if the input is a select
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} Indicator
16
+ * @property {string} pineId Indicator ID
17
+ * @property {string} pineVersion Indicator version
18
+ * @property {string} description Indicator description
19
+ * @property {string} shortDescription Indicator short description
20
+ * @property {Object<string, IndicatorInput>} inputs Indicator inputs
21
+ * @property {Object<string, string>} plots Indicator plots
22
+ * @property {string} script Indicator script
23
+ */
24
+
25
+ /**
26
+ * @typedef {'Script@tv-scripting-101!'
27
+ * | 'StrategyScript@tv-scripting-101!'} IndicatorType Indicator type
28
+ */
29
+ module.exports = class PineIndicator {
30
+ #options;
31
+
32
+ /** @type {IndicatorType} */
33
+ #type = 'Script@tv-scripting-101!';
34
+
35
+ /** @param {Indicator} options Indicator */
36
+ constructor(options) {
37
+ this.#options = options;
38
+ }
39
+
40
+ /** @return {string} Indicator ID */
41
+ get pineId() {
42
+ return this.#options.pineId;
43
+ }
44
+
45
+ /** @return {string} Indicator version */
46
+ get pineVersion() {
47
+ return this.#options.pineVersion;
48
+ }
49
+
50
+ /** @return {string} Indicator description */
51
+ get description() {
52
+ return this.#options.description;
53
+ }
54
+
55
+ /** @return {string} Indicator short description */
56
+ get shortDescription() {
57
+ return this.#options.shortDescription;
58
+ }
59
+
60
+ /** @return {Object<string, IndicatorInput>} Indicator inputs */
61
+ get inputs() {
62
+ return this.#options.inputs;
63
+ }
64
+
65
+ /** @return {Object<string, string>} Indicator plots */
66
+ get plots() {
67
+ return this.#options.plots;
68
+ }
69
+
70
+ /** @return {IndicatorType} Indicator script */
71
+ get type() {
72
+ return this.#type;
73
+ }
74
+
75
+ /**
76
+ * Set the indicator type
77
+ * @param {IndicatorType} type Indicator type
78
+ */
79
+ setType(type = 'Script@tv-scripting-101!') {
80
+ this.#type = type;
81
+ }
82
+
83
+ /** @return {string} Indicator script */
84
+ get script() {
85
+ return this.#options.script;
86
+ }
87
+
88
+ /**
89
+ * Set an option
90
+ * @param {number | string} key The key can be ID of the property (`in_{ID}`),
91
+ * the inline name or the internalID.
92
+ * @param {*} value The new value of the property
93
+ */
94
+ setOption(key, value) {
95
+ let propI = '';
96
+
97
+ if (this.#options.inputs[`in_${key}`]) propI = `in_${key}`;
98
+ else if (this.#options.inputs[key]) propI = key;
99
+ else {
100
+ propI = Object.keys(this.#options.inputs).find((I) => (
101
+ this.#options.inputs[I].inline === key
102
+ || this.#options.inputs[I].internalID === key
103
+ ));
104
+ }
105
+
106
+ if (propI && this.#options.inputs[propI]) {
107
+ const input = this.#options.inputs[propI];
108
+
109
+ const types = {
110
+ bool: 'Boolean',
111
+ integer: 'Number',
112
+ float: 'Number',
113
+ text: 'String',
114
+ };
115
+
116
+ if (
117
+ (input.type === 'bool' && typeof value !== 'boolean')
118
+ || (input.type === 'integer' && typeof value !== 'number')
119
+ || (input.type === 'float' && typeof value !== 'number')
120
+ || (input.type === 'text' && typeof value !== 'string')
121
+ ) {
122
+ throw new Error(`Input '${input.name}' (${propI}) must be a ${types[input.type]} !`);
123
+ }
124
+
125
+ if (input.options && !input.options.includes(value)) {
126
+ throw new Error(`Input '${input.name}' (${propI}) must be one of these values:`, input.options);
127
+ }
128
+
129
+ input.value = value;
130
+ } else throw new Error(`Input '${key}' not found (${propI}).`);
131
+ }
132
+ };