@mathieuc/tradingview 2.0.91 → 3.2.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.
Files changed (56) hide show
  1. package/.eslintrc.js +18 -16
  2. package/README.md +19 -98
  3. package/examples/AllPrivateIndicators.js +35 -0
  4. package/examples/BuiltInIndicator.js +41 -0
  5. package/examples/CurrentTimeframe.js +34 -0
  6. package/examples/CustomChartType.js +122 -0
  7. package/examples/CustomTimeframe.js +29 -0
  8. package/examples/Errors.js +147 -0
  9. package/examples/FakeReplayMode.js +41 -0
  10. package/examples/FromToData.js +29 -0
  11. package/examples/GraphicIndicator.js +36 -0
  12. package/examples/MultipleSyncFetch.js +38 -0
  13. package/examples/PinePermManage.js +57 -0
  14. package/examples/ReplayMode.js +97 -0
  15. package/examples/Search.js +15 -0
  16. package/examples/SimpleChart.js +63 -0
  17. package/examples/_RenkoIndicator.js +41 -0
  18. package/main.js +11 -504
  19. package/package.json +9 -8
  20. package/src/FormData.js +35 -0
  21. package/src/chart/graphicParser.js +308 -0
  22. package/src/chart/session.js +546 -0
  23. package/src/chart/study.js +403 -0
  24. package/src/classes/BuiltInIndicator.js +124 -0
  25. package/src/classes/PineIndicator.js +132 -0
  26. package/src/classes/PinePermManager.js +142 -0
  27. package/src/client.js +280 -0
  28. package/{miscRequests.js → src/miscRequests.js} +102 -89
  29. package/src/protocol.js +54 -0
  30. package/src/quote/market.js +123 -0
  31. package/src/quote/session.js +125 -0
  32. package/src/request.js +37 -0
  33. package/src/types.js +32 -0
  34. package/src/utils.js +8 -0
  35. package/test.js +64 -0
  36. package/tests/1. Search.js +18 -0
  37. package/tests/2. QuoteSession.js +54 -0
  38. package/tests/3. SimpleChart.js +61 -0
  39. package/tests/4. Indicators.js +99 -0
  40. package/tests/5. BuiltInIndicator.js +41 -0
  41. package/tests/6. FakeReplayMode.js +59 -0
  42. package/tests/7. ReplayMode.js +51 -0
  43. package/tests/8. CustomChartTypes.js +115 -0
  44. package/tests/9. AllErrors.js +146 -0
  45. package/tests/disabled/x. Template.js +24 -0
  46. package/.github/FUNDING.yml +0 -1
  47. package/tests/alerts/discordWH.js +0 -76
  48. package/tests/alerts/drawings.js +0 -41
  49. package/tests/alerts/envLoader.js +0 -11
  50. package/tests/alerts/manualAlert.js +0 -77
  51. package/tests/alerts/sentNotifs.js +0 -16
  52. package/tests/alerts/strategyAlert.js +0 -94
  53. package/tests/analysis.js +0 -13
  54. package/tests/indicator.js +0 -42
  55. package/tests/prices.js +0 -33
  56. package/tests/strategy.js +0 -56
@@ -0,0 +1,546 @@
1
+ const { genSessionID } = require('../utils');
2
+
3
+ const studyConstructor = require('./study');
4
+
5
+ /**
6
+ * @typedef {'HeikinAshi' | 'Renko' | 'LineBreak' | 'Kagi' | 'PointAndFigure'
7
+ * | 'Range'} ChartType Custom chart type
8
+ */
9
+
10
+ const ChartTypes = {
11
+ HeikinAshi: 'BarSetHeikenAshi@tv-basicstudies-60!',
12
+ Renko: 'BarSetRenko@tv-prostudies-40!',
13
+ LineBreak: 'BarSetPriceBreak@tv-prostudies-34!',
14
+ Kagi: 'BarSetKagi@tv-prostudies-34!',
15
+ PointAndFigure: 'BarSetPnF@tv-prostudies-34!',
16
+ Range: 'BarSetRange@tv-basicstudies-72!',
17
+ };
18
+
19
+ /**
20
+ * @typedef {Object} ChartInputs Custom chart type
21
+ * @prop {number} [atrLength] Renko/Kagi/PointAndFigure ATR length
22
+ * @prop {'open' | 'high' | 'low' | 'close' | 'hl2'
23
+ * | 'hlc3' | 'ohlc4'} [source] Renko/LineBreak/Kagi source
24
+ * @prop {'ATR' | string} [style] Renko/Kagi/PointAndFigure style
25
+ * @prop {number} [boxSize] Renko/PointAndFigure box size
26
+ * @prop {number} [reversalAmount] Kagi/PointAndFigure reversal amount
27
+ * @prop {'Close'} [sources] Renko/PointAndFigure sources
28
+ * @prop {boolean} [wicks] Renko wicks
29
+ * @prop {number} [lb] LineBreak Line break
30
+ * @prop {boolean} [oneStepBackBuilding] PointAndFigure oneStepBackBuilding
31
+ * @prop {boolean} [phantomBars] Range phantom bars
32
+ * @prop {boolean} [range] Range range
33
+ */
34
+
35
+ /** @typedef {Object<string, Function[]>} StudyListeners */
36
+
37
+ /**
38
+ * @typedef {Object} ChartSessionBridge
39
+ * @prop {string} sessionID
40
+ * @prop {StudyListeners} studyListeners
41
+ * @prop {Object<number, number>} indexes
42
+ * @prop {import('../client').SendPacket} send
43
+ */
44
+
45
+ /**
46
+ * @typedef {'seriesLoaded' | 'symbolLoaded' | 'update' | 'error'} ChartEvent
47
+ */
48
+
49
+ /**
50
+ * @typedef {Object} PricePeriod
51
+ * @prop {number} time Period timestamp
52
+ * @prop {number} open Period open value
53
+ * @prop {number} close Period close value
54
+ * @prop {number} max Period max value
55
+ * @prop {number} min Period min value
56
+ * @prop {number} volume Period volume value
57
+ */
58
+
59
+ /**
60
+ * @typedef {Object} Subsession
61
+ * @prop {string} id Subsession ID (ex: 'regular')
62
+ * @prop {string} description Subsession description (ex: 'Regular')
63
+ * @prop {boolean} private If private
64
+ * @prop {string} session Session (ex: '24x7')
65
+ * @prop {string} session-correction Session correction
66
+ * @prop {string} session-display Session display (ex: '24x7')
67
+ *
68
+ * @typedef {Object} MarketInfos
69
+ * @prop {string} series_id Used series (ex: 'ser_1')
70
+ * @prop {string} base_currency Base currency (ex: 'BTC')
71
+ * @prop {string} base_currency_id Base currency ID (ex: 'XTVCBTC')
72
+ * @prop {string} name Market short name (ex: 'BTCEUR')
73
+ * @prop {string} full_name Market full name (ex: 'COINBASE:BTCEUR')
74
+ * @prop {string} pro_name Market pro name (ex: 'COINBASE:BTCEUR')
75
+ * @prop {string} description Market symbol description (ex: 'BTC/EUR')
76
+ * @prop {string} short_description Market symbol short description (ex: 'BTC/EUR')
77
+ * @prop {string} exchange Market exchange (ex: 'COINBASE')
78
+ * @prop {string} listed_exchange Market exchange (ex: 'COINBASE')
79
+ * @prop {string} provider_id Values provider ID (ex: 'coinbase')
80
+ * @prop {string} currency_id Used currency ID (ex: 'EUR')
81
+ * @prop {string} currency_code Used currency code (ex: 'EUR')
82
+ * @prop {string} variable_tick_size Variable tick size
83
+ * @prop {number} pricescale Price scale
84
+ * @prop {number} pointvalue Point value
85
+ * @prop {string} session Session (ex: '24x7')
86
+ * @prop {string} session_display Session display (ex: '24x7')
87
+ * @prop {string} type Market type (ex: 'crypto')
88
+ * @prop {boolean} has_intraday If intraday values are available
89
+ * @prop {boolean} fractional If market is fractional
90
+ * @prop {boolean} is_tradable If the market is curently tradable
91
+ * @prop {number} minmov Minimum move value
92
+ * @prop {number} minmove2 Minimum move value 2
93
+ * @prop {string} timezone Used timezone
94
+ * @prop {boolean} is_replayable If the replay mode is available
95
+ * @prop {boolean} has_adjustment If the adjustment mode is enabled
96
+ * @prop {boolean} has_extended_hours Has extended hours
97
+ * @prop {string} bar_source Bar source
98
+ * @prop {string} bar_transform Bar transform
99
+ * @prop {boolean} bar_fillgaps Bar fill gaps
100
+ * @prop {string} allowed_adjustment Allowed adjustment (ex: 'none')
101
+ * @prop {string} subsession_id Subsession ID (ex: 'regular')
102
+ * @prop {string} pro_perm Pro permission (ex: '')
103
+ * @prop {[]} base_name Base name (ex: ['COINBASE:BTCEUR'])
104
+ * @prop {[]} legs Legs (ex: ['COINBASE:BTCEUR'])
105
+ * @prop {Subsession[]} subsessions Sub sessions
106
+ * @prop {[]} typespecs Typespecs (ex: [])
107
+ * @prop {[]} resolutions Resolutions (ex: [])
108
+ * @prop {[]} aliases Aliases (ex: [])
109
+ * @prop {[]} alternatives Alternatives (ex: [])
110
+ */
111
+
112
+ /**
113
+ * @param {import('../client').ClientBridge} client
114
+ */
115
+ module.exports = (client) => class ChartSession {
116
+ #chartSessionID = genSessionID('cs');
117
+
118
+ #replaySessionID = genSessionID('rs');
119
+
120
+ #replayMode = false;
121
+
122
+ /** @type {Object<string, (): any>} */
123
+ #replayOKCB = {}
124
+
125
+ /** Parent client */
126
+ #client = client;
127
+
128
+ /** @type {StudyListeners} */
129
+ #studyListeners = {};
130
+
131
+ /**
132
+ * Table of periods values indexed by timestamp
133
+ * @type {Object<number, PricePeriod[]>}
134
+ */
135
+ #periods = {};
136
+
137
+ /** @return {PricePeriod[]} List of periods values */
138
+ get periods() {
139
+ return Object.values(this.#periods).sort((a, b) => b.time - a.time);
140
+ }
141
+
142
+ /**
143
+ * Current market infos
144
+ * @type {MarketInfos}
145
+ */
146
+ #infos = {};
147
+
148
+ /** @return {MarketInfos} Current market infos */
149
+ get infos() {
150
+ return this.#infos;
151
+ }
152
+
153
+ #callbacks = {
154
+ seriesLoaded: [],
155
+ symbolLoaded: [],
156
+ update: [],
157
+
158
+ replayLoaded: [],
159
+ replayPoint: [],
160
+ replayResolution: [],
161
+ replayEnd: [],
162
+
163
+ event: [],
164
+ error: [],
165
+ };
166
+
167
+ /**
168
+ * @param {ChartEvent} ev Client event
169
+ * @param {...{}} data Packet data
170
+ */
171
+ #handleEvent(ev, ...data) {
172
+ this.#callbacks[ev].forEach((e) => e(...data));
173
+ this.#callbacks.event.forEach((e) => e(ev, ...data));
174
+ }
175
+
176
+ #handleError(...msgs) {
177
+ if (this.#callbacks.error.length === 0) console.error(...msgs);
178
+ else this.#handleEvent('error', ...msgs);
179
+ }
180
+
181
+ constructor() {
182
+ this.#client.sessions[this.#chartSessionID] = {
183
+ type: 'chart',
184
+ onData: (packet) => {
185
+ if (global.TW_DEBUG) console.log('§90§30§106 CHART SESSION §0 DATA', packet);
186
+
187
+ if (typeof packet.data[1] === 'string' && this.#studyListeners[packet.data[1]]) {
188
+ this.#studyListeners[packet.data[1]](packet);
189
+ return;
190
+ }
191
+
192
+ if (packet.type === 'symbol_resolved') {
193
+ this.#infos = {
194
+ series_id: packet.data[1],
195
+ ...packet.data[2],
196
+ };
197
+
198
+ this.#handleEvent('symbolLoaded');
199
+ return;
200
+ }
201
+
202
+ if (['timescale_update', 'du'].includes(packet.type)) {
203
+ const changes = [];
204
+
205
+ Object.keys(packet.data[1]).forEach((k) => {
206
+ changes.push(k);
207
+ if (k === '$prices') {
208
+ const periods = packet.data[1].$prices;
209
+ if (!periods || !periods.s) return;
210
+
211
+ periods.s.forEach((p) => {
212
+ [this.#chartSession.indexes[p.i]] = p.v;
213
+ this.#periods[p.v[0]] = {
214
+ time: p.v[0],
215
+ open: p.v[1],
216
+ close: p.v[4],
217
+ max: p.v[2],
218
+ min: p.v[3],
219
+ volume: Math.round(p.v[5] * 100) / 100,
220
+ };
221
+ });
222
+
223
+ return;
224
+ }
225
+
226
+ if (this.#studyListeners[k]) this.#studyListeners[k](packet);
227
+ });
228
+
229
+ this.#handleEvent('update', changes);
230
+ return;
231
+ }
232
+
233
+ if (packet.type === 'symbol_error') {
234
+ this.#handleError(`(${packet.data[1]}) Symbol error:`, packet.data[2]);
235
+ return;
236
+ }
237
+
238
+ if (packet.type === 'series_error') {
239
+ this.#handleError('Series error:', packet.data[3]);
240
+ return;
241
+ }
242
+
243
+ if (packet.type === 'critical_error') {
244
+ const [, name, description] = packet.data;
245
+ this.#handleError('Critical error:', name, description);
246
+ }
247
+ },
248
+ };
249
+
250
+ this.#client.sessions[this.#replaySessionID] = {
251
+ type: 'replay',
252
+ onData: (packet) => {
253
+ if (global.TW_DEBUG) console.log('§90§30§106 REPLAY SESSION §0 DATA', packet);
254
+
255
+ if (packet.type === 'replay_ok') {
256
+ if (this.#replayOKCB[packet.data[1]]) {
257
+ this.#replayOKCB[packet.data[1]]();
258
+ delete this.#replayOKCB[packet.data[1]];
259
+ }
260
+ return;
261
+ }
262
+
263
+ if (packet.type === 'replay_instance_id') {
264
+ this.#handleEvent('replayLoaded', packet.data[1]);
265
+ return;
266
+ }
267
+
268
+ if (packet.type === 'replay_point') {
269
+ this.#handleEvent('replayPoint', packet.data[1]);
270
+ return;
271
+ }
272
+
273
+ if (packet.type === 'replay_resolutions') {
274
+ this.#handleEvent('replayResolution', packet.data[1], packet.data[2]);
275
+ return;
276
+ }
277
+
278
+ if (packet.type === 'replay_data_end') {
279
+ this.#handleEvent('replayEnd');
280
+ return;
281
+ }
282
+
283
+ if (packet.type === 'critical_error') {
284
+ const [, name, description] = packet.data;
285
+ this.#handleError('Critical error:', name, description);
286
+ }
287
+ },
288
+ };
289
+
290
+ this.#client.send('chart_create_session', [this.#chartSessionID]);
291
+ }
292
+
293
+ #seriesCreated = false;
294
+
295
+ #currentSeries = 0;
296
+
297
+ /**
298
+ * @param {import('../types').TimeFrame} timeframe Chart period timeframe
299
+ * @param {number} [range] Number of loaded periods/candles (Default: 100)
300
+ * @param {number} [reference] Reference candle timestamp (Default is now)
301
+ */
302
+ setSeries(timeframe = '240', range = 100, reference = null) {
303
+ if (!this.#currentSeries) {
304
+ this.#handleError('Please set the market before setting series');
305
+ return;
306
+ }
307
+
308
+ const calcRange = !reference ? range : ['bar_count', reference, range];
309
+
310
+ this.#periods = {};
311
+
312
+ this.#client.send(`${this.#seriesCreated ? 'modify' : 'create'}_series`, [
313
+ this.#chartSessionID,
314
+ '$prices',
315
+ 's1',
316
+ `ser_${this.#currentSeries}`,
317
+ timeframe,
318
+ this.#seriesCreated ? '' : calcRange,
319
+ ]);
320
+
321
+ this.#seriesCreated = true;
322
+ }
323
+
324
+ /**
325
+ * Set the chart market
326
+ * @param {string} symbol Market symbol
327
+ * @param {Object} [options] Chart options
328
+ * @param {import('../types').TimeFrame} [options.timeframe] Chart period timeframe
329
+ * @param {number} [options.range] Number of loaded periods/candles (Default: 100)
330
+ * @param {number} [options.to] Last candle timestamp (Default is now)
331
+ * @param {'splits' | 'dividends'} [options.adjustment] Market adjustment
332
+ * @param {'regular' | 'extended'} [options.session] Chart session
333
+ * @param {'EUR' | 'USD' | string} [options.currency] Chart currency
334
+ * @param {ChartType} [options.type] Chart custom type
335
+ * @param {ChartInputs} [options.inputs] Chart custom inputs
336
+ * @param {number} [options.replay] Replay mode starting point (Timestamp)
337
+ */
338
+ setMarket(symbol, options = {}) {
339
+ this.#periods = {};
340
+
341
+ if (this.#replayMode) {
342
+ this.#replayMode = false;
343
+ this.#client.send('replay_delete_session', [this.#replaySessionID]);
344
+ }
345
+
346
+ const symbolInit = {
347
+ symbol: symbol || 'BTCEUR',
348
+ adjustment: options.adjustment || 'splits',
349
+ };
350
+
351
+ if (options.session) symbolInit.session = options.session;
352
+ if (options.currency) symbolInit['currency-id'] = options.currency;
353
+
354
+ if (options.replay) {
355
+ this.#replayMode = true;
356
+ this.#client.send('replay_create_session', [this.#replaySessionID]);
357
+
358
+ this.#client.send('replay_add_series', [
359
+ this.#replaySessionID,
360
+ 'req_replay_addseries',
361
+ `=${JSON.stringify(symbolInit)}`,
362
+ options.timeframe,
363
+ ]);
364
+
365
+ this.#client.send('replay_reset', [
366
+ this.#replaySessionID,
367
+ 'req_replay_reset',
368
+ options.replay,
369
+ ]);
370
+ }
371
+
372
+ const complex = options.type || options.replay;
373
+ const chartInit = complex ? {} : symbolInit;
374
+
375
+ if (complex) {
376
+ if (options.replay) chartInit.replay = this.#replaySessionID;
377
+ chartInit.symbol = symbolInit;
378
+ chartInit.type = ChartTypes[options.type];
379
+ if (options.type) chartInit.inputs = { ...options.inputs };
380
+ }
381
+
382
+ this.#currentSeries += 1;
383
+
384
+ this.#client.send('resolve_symbol', [
385
+ this.#chartSessionID,
386
+ `ser_${this.#currentSeries}`,
387
+ `=${JSON.stringify(chartInit)}`,
388
+ ]);
389
+
390
+ this.setSeries(options.timeframe, options.range, options.to);
391
+ }
392
+
393
+ /**
394
+ * Set the chart timezone
395
+ * @param {import('../types').Timezone} timezone New timezone
396
+ */
397
+ setTimezone(timezone) {
398
+ this.#periods = {};
399
+ this.#client.send('switch_timezone', [this.#chartSessionID, timezone]);
400
+ }
401
+
402
+ /**
403
+ * Fetch x additional previous periods/candles values
404
+ * @param {number} number Number of additional periods/candles you want to fetch
405
+ */
406
+ fetchMore(number = 1) {
407
+ this.#client.send('request_more_data', [this.#chartSessionID, '$prices', number]);
408
+ }
409
+
410
+ /**
411
+ * Fetch x additional previous periods/candles values
412
+ * @param {number} number Number of additional periods/candles you want to fetch
413
+ * @returns {Promise} Raise when the data has been fetched
414
+ */
415
+ replayStep(number = 1) {
416
+ return new Promise((cb) => {
417
+ if (!this.#replayMode) {
418
+ this.#handleError('No replay session');
419
+ return;
420
+ }
421
+
422
+ const reqID = genSessionID('rsq_step');
423
+ this.#client.send('replay_step', [this.#replaySessionID, reqID, number]);
424
+ this.#replayOKCB[reqID] = () => { cb(); };
425
+ });
426
+ }
427
+
428
+ /**
429
+ * Start fetching a new period/candle every x ms
430
+ * @param {number} interval Number of additional periods/candles you want to fetch
431
+ * @returns {Promise} Raise when the replay mode starts
432
+ */
433
+ replayStart(interval = 1000) {
434
+ return new Promise((cb) => {
435
+ if (!this.#replayMode) {
436
+ this.#handleError('No replay session');
437
+ return;
438
+ }
439
+
440
+ const reqID = genSessionID('rsq_start');
441
+ this.#client.send('replay_start', [this.#replaySessionID, reqID, interval]);
442
+ this.#replayOKCB[reqID] = () => { cb(); };
443
+ });
444
+ }
445
+
446
+ /**
447
+ * Stop fetching a new period/candle every x ms
448
+ * @returns {Promise} Raise when the replay mode stops
449
+ */
450
+ replayStop() {
451
+ return new Promise((cb) => {
452
+ if (!this.#replayMode) {
453
+ this.#handleError('No replay session');
454
+ return;
455
+ }
456
+
457
+ const reqID = genSessionID('rsq_stop');
458
+ this.#client.send('replay_stop', [this.#replaySessionID, reqID]);
459
+ this.#replayOKCB[reqID] = () => { cb(); };
460
+ });
461
+ }
462
+
463
+ /**
464
+ * When a symbol is loaded
465
+ * @param {() => void} cb
466
+ * @event
467
+ */
468
+ onSymbolLoaded(cb) {
469
+ this.#callbacks.symbolLoaded.push(cb);
470
+ }
471
+
472
+ /**
473
+ * When a chart update happens
474
+ * @param {(changes: ('$prices' | string)[]) => void} cb
475
+ * @event
476
+ */
477
+ onUpdate(cb) {
478
+ this.#callbacks.update.push(cb);
479
+ }
480
+
481
+ /**
482
+ * When the replay session is ready
483
+ * @param {() => void} cb
484
+ * @event
485
+ */
486
+ onReplayLoaded(cb) {
487
+ this.#callbacks.replayLoaded.push(cb);
488
+ }
489
+
490
+ /**
491
+ * When the replay session has new resolution
492
+ * @param {(
493
+ * timeframe: import('../types').TimeFrame,
494
+ * index: number,
495
+ * ) => void} cb
496
+ * @event
497
+ */
498
+ onReplayResolution(cb) {
499
+ this.#callbacks.replayResolution.push(cb);
500
+ }
501
+
502
+ /**
503
+ * When the replay session ends
504
+ * @param {() => void} cb
505
+ * @event
506
+ */
507
+ onReplayEnd(cb) {
508
+ this.#callbacks.replayEnd.push(cb);
509
+ }
510
+
511
+ /**
512
+ * When the replay session cursor has moved
513
+ * @param {(index: number) => void} cb
514
+ * @event
515
+ */
516
+ onReplayPoint(cb) {
517
+ this.#callbacks.replayPoint.push(cb);
518
+ }
519
+
520
+ /**
521
+ * When chart error happens
522
+ * @param {(...any) => void} cb Callback
523
+ * @event
524
+ */
525
+ onError(cb) {
526
+ this.#callbacks.error.push(cb);
527
+ }
528
+
529
+ /** @type {ChartSessionBridge} */
530
+ #chartSession = {
531
+ sessionID: this.#chartSessionID,
532
+ studyListeners: this.#studyListeners,
533
+ indexes: {},
534
+ send: (t, p) => this.#client.send(t, p),
535
+ };
536
+
537
+ Study = studyConstructor(this.#chartSession);
538
+
539
+ /** Delete the chart session */
540
+ delete() {
541
+ if (this.#replayMode) this.#client.send('replay_delete_session', [this.#replaySessionID]);
542
+ this.#client.send('chart_delete_session', [this.#chartSessionID]);
543
+ delete this.#client.sessions[this.#chartSessionID];
544
+ this.#replayMode = false;
545
+ }
546
+ };