@mathieuc/tradingview 3.0.1 → 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.
@@ -113,7 +113,14 @@ const ChartTypes = {
113
113
  * @param {import('../client').ClientBridge} client
114
114
  */
115
115
  module.exports = (client) => class ChartSession {
116
- #sessionID = genSessionID('cs');
116
+ #chartSessionID = genSessionID('cs');
117
+
118
+ #replaySessionID = genSessionID('rs');
119
+
120
+ #replayMode = false;
121
+
122
+ /** @type {Object<string, (): any>} */
123
+ #replayOKCB = {}
117
124
 
118
125
  /** Parent client */
119
126
  #client = client;
@@ -148,6 +155,11 @@ module.exports = (client) => class ChartSession {
148
155
  symbolLoaded: [],
149
156
  update: [],
150
157
 
158
+ replayLoaded: [],
159
+ replayPoint: [],
160
+ replayResolution: [],
161
+ replayEnd: [],
162
+
151
163
  event: [],
152
164
  error: [],
153
165
  };
@@ -167,7 +179,7 @@ module.exports = (client) => class ChartSession {
167
179
  }
168
180
 
169
181
  constructor() {
170
- this.#client.sessions[this.#sessionID] = {
182
+ this.#client.sessions[this.#chartSessionID] = {
171
183
  type: 'chart',
172
184
  onData: (packet) => {
173
185
  if (global.TW_DEBUG) console.log('§90§30§106 CHART SESSION §0 DATA', packet);
@@ -188,7 +200,10 @@ module.exports = (client) => class ChartSession {
188
200
  }
189
201
 
190
202
  if (['timescale_update', 'du'].includes(packet.type)) {
203
+ const changes = [];
204
+
191
205
  Object.keys(packet.data[1]).forEach((k) => {
206
+ changes.push(k);
192
207
  if (k === '$prices') {
193
208
  const periods = packet.data[1].$prices;
194
209
  if (!periods || !periods.s) return;
@@ -211,7 +226,7 @@ module.exports = (client) => class ChartSession {
211
226
  if (this.#studyListeners[k]) this.#studyListeners[k](packet);
212
227
  });
213
228
 
214
- this.#handleEvent('update');
229
+ this.#handleEvent('update', changes);
215
230
  return;
216
231
  }
217
232
 
@@ -232,7 +247,47 @@ module.exports = (client) => class ChartSession {
232
247
  },
233
248
  };
234
249
 
235
- this.#client.send('chart_create_session', [this.#sessionID]);
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]);
236
291
  }
237
292
 
238
293
  #seriesCreated = false;
@@ -255,7 +310,7 @@ module.exports = (client) => class ChartSession {
255
310
  this.#periods = {};
256
311
 
257
312
  this.#client.send(`${this.#seriesCreated ? 'modify' : 'create'}_series`, [
258
- this.#sessionID,
313
+ this.#chartSessionID,
259
314
  '$prices',
260
315
  's1',
261
316
  `ser_${this.#currentSeries}`,
@@ -278,31 +333,56 @@ module.exports = (client) => class ChartSession {
278
333
  * @param {'EUR' | 'USD' | string} [options.currency] Chart currency
279
334
  * @param {ChartType} [options.type] Chart custom type
280
335
  * @param {ChartInputs} [options.inputs] Chart custom inputs
336
+ * @param {number} [options.replay] Replay mode starting point (Timestamp)
281
337
  */
282
338
  setMarket(symbol, options = {}) {
283
339
  this.#periods = {};
284
340
 
341
+ if (this.#replayMode) {
342
+ this.#replayMode = false;
343
+ this.#client.send('replay_delete_session', [this.#replaySessionID]);
344
+ }
345
+
285
346
  const symbolInit = {
286
347
  symbol: symbol || 'BTCEUR',
287
348
  adjustment: options.adjustment || 'splits',
288
349
  };
289
350
 
290
351
  if (options.session) symbolInit.session = options.session;
291
-
292
352
  if (options.currency) symbolInit['currency-id'] = options.currency;
293
353
 
294
- const chartInit = (options.type && ChartTypes[options.type]) ? {} : symbolInit;
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;
295
374
 
296
- if (options.type && ChartTypes[options.type]) {
375
+ if (complex) {
376
+ if (options.replay) chartInit.replay = this.#replaySessionID;
297
377
  chartInit.symbol = symbolInit;
298
378
  chartInit.type = ChartTypes[options.type];
299
- chartInit.inputs = { ...options.inputs };
379
+ if (options.type) chartInit.inputs = { ...options.inputs };
300
380
  }
301
381
 
302
382
  this.#currentSeries += 1;
303
383
 
304
384
  this.#client.send('resolve_symbol', [
305
- this.#sessionID,
385
+ this.#chartSessionID,
306
386
  `ser_${this.#currentSeries}`,
307
387
  `=${JSON.stringify(chartInit)}`,
308
388
  ]);
@@ -316,7 +396,7 @@ module.exports = (client) => class ChartSession {
316
396
  */
317
397
  setTimezone(timezone) {
318
398
  this.#periods = {};
319
- this.#client.send('switch_timezone', [this.#sessionID, timezone]);
399
+ this.#client.send('switch_timezone', [this.#chartSessionID, timezone]);
320
400
  }
321
401
 
322
402
  /**
@@ -324,7 +404,60 @@ module.exports = (client) => class ChartSession {
324
404
  * @param {number} number Number of additional periods/candles you want to fetch
325
405
  */
326
406
  fetchMore(number = 1) {
327
- this.#client.send('request_more_data', [this.#sessionID, '$prices', number]);
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
+ });
328
461
  }
329
462
 
330
463
  /**
@@ -338,13 +471,52 @@ module.exports = (client) => class ChartSession {
338
471
 
339
472
  /**
340
473
  * When a chart update happens
341
- * @param {() => void} cb
474
+ * @param {(changes: ('$prices' | string)[]) => void} cb
342
475
  * @event
343
476
  */
344
477
  onUpdate(cb) {
345
478
  this.#callbacks.update.push(cb);
346
479
  }
347
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
+
348
520
  /**
349
521
  * When chart error happens
350
522
  * @param {(...any) => void} cb Callback
@@ -356,7 +528,7 @@ module.exports = (client) => class ChartSession {
356
528
 
357
529
  /** @type {ChartSessionBridge} */
358
530
  #chartSession = {
359
- sessionID: this.#sessionID,
531
+ sessionID: this.#chartSessionID,
360
532
  studyListeners: this.#studyListeners,
361
533
  indexes: {},
362
534
  send: (t, p) => this.#client.send(t, p),
@@ -366,7 +538,9 @@ module.exports = (client) => class ChartSession {
366
538
 
367
539
  /** Delete the chart session */
368
540
  delete() {
369
- this.#client.send('quote_delete_session', [this.#sessionID]);
370
- delete this.#client.sessions[this.#sessionID];
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;
371
545
  }
372
546
  };
@@ -0,0 +1,142 @@
1
+ const request = require('../request');
2
+ const FormData = require('../FormData');
3
+
4
+ /**
5
+ * @typedef {Object} AuthorizationUser
6
+ * @prop {id} id User id
7
+ * @prop {string} username User's username
8
+ * @prop {string} userpic User's profile picture URL
9
+ * @prop {string} expiration Authorization expiration date
10
+ * @prop {string} created Authorization creation date
11
+ */
12
+
13
+ class PinePermManager {
14
+ sessionId;
15
+
16
+ pineId;
17
+
18
+ /**
19
+ * Creates a PinePermManager instance
20
+ * @param {string} sessionId Token from sessionid cookie
21
+ * @param {string} pineId Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX)
22
+ */
23
+ constructor(sessionId, pineId) {
24
+ if (!sessionId) throw new Error('Please provide a SessionID');
25
+ if (!pineId) throw new Error('Please provide a PineID');
26
+ this.pineId = pineId;
27
+ this.sessionId = sessionId;
28
+ }
29
+
30
+ /**
31
+ * Get list of authorized users
32
+ * @param {number} limit Fetching limit
33
+ * @param {'user__username'
34
+ * | '-user__username'
35
+ * | 'created' | 'created'
36
+ * | 'expiration,user__username'
37
+ * | '-expiration,user__username'
38
+ * } order Fetching order
39
+ * @returns {AuthorizationUser[]}
40
+ */
41
+ async getUsers(limit = 10, order = '-created') {
42
+ const data = await request({
43
+ method: 'POST',
44
+ host: 'www.tradingview.com',
45
+ path: `/pine_perm/list_users/?limit=${limit}&order_by=${order}`,
46
+ headers: {
47
+ origin: 'https://www.tradingview.com',
48
+ 'Content-Type': 'application/x-www-form-urlencoded',
49
+ cookie: `sessionid=${this.sessionId}`,
50
+ },
51
+ }, false, `pine_id=${this.pineId.replace(/;/g, '%3B')}`);
52
+
53
+ if (!data.results) throw new Error('Wrong sessionId or pineId');
54
+
55
+ return data.results;
56
+ }
57
+
58
+ /**
59
+ * Adds an user to the authorized list
60
+ * @param {string} username User's username
61
+ * @param {Date} [expiration] Expiration date
62
+ * @returns {'ok' | 'exists' | null}
63
+ */
64
+ async addUser(username, expiration = null) {
65
+ const formData = new FormData();
66
+ formData.append('pine_id', this.pineId);
67
+ formData.append('username_recip', username);
68
+ if (expiration && expiration instanceof Date) {
69
+ formData.append('expiration', expiration.toString());
70
+ }
71
+
72
+ const data = await request({
73
+ method: 'POST',
74
+ host: 'www.tradingview.com',
75
+ path: '/pine_perm/add/',
76
+ headers: {
77
+ origin: 'https://www.tradingview.com',
78
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
79
+ cookie: `sessionid=${this.sessionId}`,
80
+ },
81
+ }, false, formData.toString());
82
+
83
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
84
+ return data.status;
85
+ }
86
+
87
+ /**
88
+ * Modify an authorization expiration date
89
+ * @param {string} username User's username
90
+ * @param {Date} [expiration] New expiration date
91
+ * @returns {'ok' | null}
92
+ */
93
+ async modifyExpiration(username, expiration = null) {
94
+ const formData = new FormData();
95
+ formData.append('pine_id', this.pineId);
96
+ formData.append('username_recip', username);
97
+ if (expiration && expiration instanceof Date) {
98
+ formData.append('expiration', expiration.toISOString());
99
+ }
100
+
101
+ const data = await request({
102
+ method: 'POST',
103
+ host: 'www.tradingview.com',
104
+ path: '/pine_perm/modify_user_expiration/',
105
+ headers: {
106
+ origin: 'https://www.tradingview.com',
107
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
108
+ cookie: `sessionid=${this.sessionId}`,
109
+ },
110
+ }, false, formData.toString());
111
+
112
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
113
+ return data.status;
114
+ }
115
+
116
+ /**
117
+ * Removes an user to the authorized list
118
+ * @param {string} username User's username
119
+ * @returns {'ok' | null}
120
+ */
121
+ async removeUser(username) {
122
+ const formData = new FormData();
123
+ formData.append('pine_id', this.pineId);
124
+ formData.append('username_recip', username);
125
+
126
+ const data = await request({
127
+ method: 'POST',
128
+ host: 'www.tradingview.com',
129
+ path: '/pine_perm/remove/',
130
+ headers: {
131
+ origin: 'https://www.tradingview.com',
132
+ 'Content-Type': `multipart/form-data; boundary=${formData.boundary}`,
133
+ cookie: `sessionid=${this.sessionId}`,
134
+ },
135
+ }, false, formData.toString());
136
+
137
+ if (!data.status) throw new Error('Wrong sessionId or pineId');
138
+ return data.status;
139
+ }
140
+ }
141
+
142
+ module.exports = PinePermManager;
package/src/client.js CHANGED
@@ -273,7 +273,7 @@ module.exports = class Client {
273
273
  */
274
274
  end() {
275
275
  return new Promise((cb) => {
276
- this.#ws.close();
276
+ if (this.#ws.readyState) this.#ws.close();
277
277
  cb();
278
278
  });
279
279
  }
@@ -1,43 +1,12 @@
1
1
  const https = require('https');
2
+ const request = require('./request');
2
3
 
4
+ const PinePermManager = require('./classes/PinePermManager');
3
5
  const PineIndicator = require('./classes/PineIndicator');
4
6
 
5
7
  const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA'];
6
8
  const builtInIndicList = [];
7
9
 
8
- /**
9
- * @param {https.RequestOptions} options HTTPS Request options
10
- * @param {boolean} [raw] Get raw or JSON data
11
- * @param {string} [content] Request body content
12
- * @returns {Promise<string | object | array>} Result
13
- */
14
- function request(options = {}, raw = false, content = '') {
15
- return new Promise((cb, err) => {
16
- const req = https.request(options, (res) => {
17
- let data = '';
18
- res.on('data', (c) => { data += c; });
19
- res.on('end', () => {
20
- if (raw) {
21
- cb(data);
22
- return;
23
- }
24
-
25
- try {
26
- data = JSON.parse(data);
27
- } catch (error) {
28
- err(new Error('Can\'t parse server response'));
29
- return;
30
- }
31
-
32
- cb(data);
33
- });
34
- });
35
-
36
- req.on('error', err);
37
- req.end(content);
38
- });
39
- }
40
-
41
10
  async function fetchScanData(tickers = [], type = '', columns = []) {
42
11
  let data = await request({
43
12
  method: 'POST',
@@ -437,6 +406,9 @@ module.exports = {
437
406
  get() {
438
407
  return module.exports.getIndicator(ind.scriptIdPart, ind.version);
439
408
  },
409
+ getManager() {
410
+ return new PinePermManager(ind.scriptIdPart);
411
+ },
440
412
  })));
441
413
  });
442
414
 
@@ -120,5 +120,6 @@ module.exports = (client) => class QuoteSession {
120
120
  /** Delete the quote session */
121
121
  delete() {
122
122
  this.#client.send('quote_delete_session', [this.#sessionID]);
123
+ delete this.#client.sessions[this.#sessionID];
123
124
  }
124
125
  };
package/src/request.js ADDED
@@ -0,0 +1,37 @@
1
+ const https = require('https');
2
+
3
+ /**
4
+ * @param {https.RequestOptions} options HTTPS Request options
5
+ * @param {boolean} [raw] Get raw or JSON data
6
+ * @param {string} [content] Request body content
7
+ * @returns {Promise<string | object | array>} Result
8
+ */
9
+ function request(options = {}, raw = false, content = '') {
10
+ return new Promise((cb, err) => {
11
+ const req = https.request(options, (res) => {
12
+ let data = '';
13
+ res.on('data', (c) => { data += c; });
14
+ res.on('end', () => {
15
+ if (raw) {
16
+ cb(data);
17
+ return;
18
+ }
19
+
20
+ try {
21
+ data = JSON.parse(data);
22
+ } catch (error) {
23
+ console.log(data);
24
+ err(new Error('Can\'t parse server response'));
25
+ return;
26
+ }
27
+
28
+ cb(data);
29
+ });
30
+ });
31
+
32
+ req.on('error', err);
33
+ req.end(content);
34
+ });
35
+ }
36
+
37
+ module.exports = request;