@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
@@ -1,40 +1,11 @@
1
1
  const https = require('https');
2
+ const request = require('./request');
2
3
 
3
- const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA'];
4
- const indicList = [];
5
-
6
- /**
7
- * @param {https.RequestOptions} options HTTPS Request options
8
- * @param {boolean} [raw] Get raw or JSON data
9
- * @param {string} [content] Request body content
10
- * @returns {Promise<string | object | array>} Result
11
- */
12
- function request(options = {}, raw = false, content = '') {
13
- return new Promise((cb, err) => {
14
- const req = https.request(options, (res) => {
15
- let data = '';
16
- res.on('data', (c) => { data += c; });
17
- res.on('end', () => {
18
- if (raw) {
19
- cb(data);
20
- return;
21
- }
22
-
23
- try {
24
- data = JSON.parse(data);
25
- } catch (error) {
26
- err(new Error('Can\'t parse server response'));
27
- return;
28
- }
29
-
30
- cb(data);
31
- });
32
- });
4
+ const PinePermManager = require('./classes/PinePermManager');
5
+ const PineIndicator = require('./classes/PineIndicator');
33
6
 
34
- req.on('error', err);
35
- req.end(content);
36
- });
37
- }
7
+ const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA'];
8
+ const builtInIndicList = [];
38
9
 
39
10
  async function fetchScanData(tickers = [], type = '', columns = []) {
40
11
  let data = await request({
@@ -132,7 +103,7 @@ module.exports = {
132
103
  },
133
104
 
134
105
  /**
135
- * @typedef {Object} SearchResult
106
+ * @typedef {Object} SearchMarketResult
136
107
  * @property {string} id
137
108
  * @property {string} exchange
138
109
  * @property {string} fullExchange
@@ -147,9 +118,9 @@ module.exports = {
147
118
  * Find a symbol
148
119
  * @param {string} search Keywords
149
120
  * @param {'stock' | 'futures' | 'forex' | 'cfd' | 'crypto' | 'index' | 'economic'} [filter]
150
- * @returns {Promise<SearchResult[]>} Search results
121
+ * @returns {Promise<SearchMarketResult[]>} Search results
151
122
  */
152
- async search(search, filter = '') {
123
+ async searchMarket(search, filter = '') {
153
124
  const data = await request({
154
125
  host: 'symbol-search.tradingview.com',
155
126
  path: `/symbol_search/?text=${search.replace(/ /g, '%20')}&type=${filter}`,
@@ -179,7 +150,7 @@ module.exports = {
179
150
  },
180
151
 
181
152
  /**
182
- * @typedef {Object} IndicatorResult
153
+ * @typedef {Object} SearchIndicatorResult
183
154
  * @property {string} id Script ID
184
155
  * @property {string} version Script version
185
156
  * @property {string} name Script complete name
@@ -187,18 +158,20 @@ module.exports = {
187
158
  * @property {string} image Image ID https://tradingview.com/i/${image}
188
159
  * @property {string | ''} source Script source (if available)
189
160
  * @property {'study' | 'strategy'} type Script type (study / strategy)
190
- * @property {'open_source' | 'closed_source' | 'invite_only' | 'other'} access Script access type
161
+ * @property {'open_source' | 'closed_source' | 'invite_only'
162
+ * | 'private' | 'other'} access Script access type
163
+ * @property {() => Promise<PineIndicator>} get Get the full indicator informations
191
164
  */
192
165
 
193
166
  /**
194
167
  * Find an indicator
195
168
  * @param {string} search Keywords
196
- * @returns {Promise<IndicatorResult[]>} Search results
169
+ * @returns {Promise<SearchIndicatorResult[]>} Search results
197
170
  */
198
171
  async searchIndicator(search = '') {
199
- if (!indicList.length) {
172
+ if (!builtInIndicList.length) {
200
173
  await Promise.all(['standard', 'candlestick', 'fundamental'].map(async (type) => {
201
- indicList.push(...await request({
174
+ builtInIndicList.push(...await request({
202
175
  host: 'pine-facade.tradingview.com',
203
176
  path: `/pine-facade/list/?filter=${type}`,
204
177
  }));
@@ -215,7 +188,7 @@ module.exports = {
215
188
  }
216
189
 
217
190
  return [
218
- ...indicList.filter((i) => (
191
+ ...builtInIndicList.filter((i) => (
219
192
  norm(i.scriptName).includes(norm(search))
220
193
  || norm(i.extra.shortDescription).includes(norm(search))
221
194
  )).map((ind) => ({
@@ -230,6 +203,9 @@ module.exports = {
230
203
  access: 'closed_source',
231
204
  source: '',
232
205
  type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study',
206
+ get() {
207
+ return module.exports.getIndicator(ind.scriptIdPart, ind.version);
208
+ },
233
209
  })),
234
210
 
235
211
  ...data.results.map((ind) => ({
@@ -244,42 +220,20 @@ module.exports = {
244
220
  access: ['open_source', 'closed_source', 'invite_only'][ind.access - 1] || 'other',
245
221
  source: ind.scriptSource,
246
222
  type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study',
223
+ get() {
224
+ return module.exports.getIndicator(ind.scriptIdPart, ind.version);
225
+ },
247
226
  })),
248
227
  ];
249
228
  },
250
229
 
251
- /**
252
- * @typedef {Object} indicatorInput
253
- * @property {string} name
254
- * @property {string} inline Inline name
255
- * @property { 'text' | 'source' | 'integer' | 'float' | 'resolution' | 'bool' } type
256
- * @property {string | number | boolean} value
257
- * @property {string | number | boolean} defVal
258
- * @property {boolean} hidden
259
- * @property {boolean} isFake
260
- * @property {string[]} [options]
261
- */
262
-
263
- /**
264
- * @typedef {Object} Indicator
265
- * @property {string} pineId Indicator ID
266
- * @property {string} pineVersion Indicator version
267
- * @property {string} description Indicator description
268
- * @property {string} shortDescription Indicator short description
269
- * @property {string} typeID Indicator script type ID
270
- * @property {Object<string, indicatorInput>} inputs Indicator inputs
271
- * @property {Object<string, string>} plots Indicator plots
272
- * @property {string} script Indicator script
273
- */
274
-
275
230
  /**
276
231
  * Get an indicator
277
232
  * @param {string} id Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX)
278
- * @param {string} version Wanted version of the indicator
279
- * @param {'study' | 'strategy'} [type] Script type
280
- * @returns {Promise<Indicator>} Indicator
233
+ * @param {'last' | string} [version] Wanted version of the indicator
234
+ * @returns {Promise<PineIndicator>} Indicator
281
235
  */
282
- async getIndicator(id, version, settings = [], type = 'study') {
236
+ async getIndicator(id, version = 'last') {
283
237
  const indicID = id.replace(/ |%/g, '%25');
284
238
 
285
239
  let data = await request({
@@ -294,26 +248,24 @@ module.exports = {
294
248
  }
295
249
 
296
250
  if (!data.success || !data.result.metaInfo || !data.result.metaInfo.inputs) {
297
- console.error(data);
298
- throw new Error('Inexistent or unsupported indicator');
251
+ throw new Error(`Inexistent or unsupported indicator: "${data.reason}"`);
299
252
  }
300
253
 
301
254
  const inputs = {};
302
255
 
303
256
  data.result.metaInfo.inputs.forEach((input) => {
304
257
  if (['text', 'pineId', 'pineVersion'].includes(input.id)) return;
305
- const inline = input.inline || input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '');
306
-
307
- const i = parseInt(input.id.replace(/[^0-9]/g, ''), 10);
308
258
 
309
259
  inputs[input.id] = {
310
260
  name: input.name,
261
+ inline: input.inline || input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, ''),
262
+ internalID: input.internalID,
263
+ tooltip: input.tooltip,
264
+
311
265
  type: input.type,
312
- value: settings[input.internalID] ?? settings[i] ?? input.defval,
313
- defVal: input.defval,
314
- hidden: !!input.isHidden,
266
+ value: input.defval,
267
+ isHidden: !!input.isHidden,
315
268
  isFake: !!input.isFake,
316
- inline,
317
269
  };
318
270
 
319
271
  if (input.options) inputs[input.id].options = input.options;
@@ -322,22 +274,37 @@ module.exports = {
322
274
  const plots = {};
323
275
 
324
276
  Object.keys(data.result.metaInfo.styles).forEach((plotId) => {
325
- plots[plotId] = data.result.metaInfo.styles[plotId].title.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, '');
277
+ const plotTitle = data
278
+ .result
279
+ .metaInfo
280
+ .styles[plotId]
281
+ .title
282
+ .replace(/ /g, '_')
283
+ .replace(/[^a-zA-Z0-9_]/g, '');
284
+
285
+ const titles = Object.values(plots);
286
+
287
+ if (titles.includes(plotTitle)) {
288
+ let i = 2;
289
+ while (titles.includes(`${plotTitle}_${i}`)) i += 1;
290
+ plots[plotId] = `${plotTitle}_${i}`;
291
+ } else plots[plotId] = plotTitle;
326
292
  });
327
293
 
328
- return {
329
- pineId: indicID,
330
- pineVersion: version,
294
+ data.result.metaInfo.plots.forEach((plot) => {
295
+ if (!plot.target) return;
296
+ plots[plot.id] = `${plots[plot.target] ?? plot.target}_${plot.type}`;
297
+ });
298
+
299
+ return new PineIndicator({
300
+ pineId: data.result.metaInfo.scriptIdPart || indicID,
301
+ pineVersion: data.result.metaInfo.pine.version || version,
331
302
  description: data.result.metaInfo.description,
332
303
  shortDescription: data.result.metaInfo.shortDescription,
333
- typeID: {
334
- study: 'Script@tv-scripting-101!',
335
- strategy: 'StrategyScript@tv-scripting-101!',
336
- }[type] || type,
337
304
  inputs,
338
305
  plots,
339
306
  script: data.result.ilTemplate,
340
- };
307
+ });
341
308
  },
342
309
 
343
310
  /**
@@ -404,6 +371,52 @@ module.exports = {
404
371
  });
405
372
  },
406
373
 
374
+ /**
375
+ * Get user's private indicators from a 'sessionid' cookie
376
+ * @param {string} session User 'sessionid' cookie
377
+ * @returns {Promise<SearchIndicatorResult[]>} Search results
378
+ */
379
+ async getPrivateIndicators(session) {
380
+ return new Promise((cb, err) => {
381
+ https.get('https://pine-facade.tradingview.com/pine-facade/list?filter=saved', {
382
+ headers: { cookie: `sessionid=${session}` },
383
+ }, (res) => {
384
+ let rs = '';
385
+ res.on('data', (d) => { rs += d; });
386
+ res.on('end', async () => {
387
+ try {
388
+ rs = JSON.parse(rs);
389
+ } catch (error) {
390
+ err(new Error('Can\'t parse private indicator list'));
391
+ return;
392
+ }
393
+
394
+ cb(rs.map((ind) => ({
395
+ id: ind.scriptIdPart,
396
+ version: ind.version,
397
+ name: ind.scriptName,
398
+ author: {
399
+ id: -1,
400
+ username: '@ME@',
401
+ },
402
+ image: ind.imageUrl,
403
+ access: 'private',
404
+ source: ind.scriptSource,
405
+ type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study',
406
+ get() {
407
+ return module.exports.getIndicator(ind.scriptIdPart, ind.version);
408
+ },
409
+ getManager() {
410
+ return new PinePermManager(ind.scriptIdPart);
411
+ },
412
+ })));
413
+ });
414
+
415
+ res.on('error', err);
416
+ }).end();
417
+ });
418
+ },
419
+
407
420
  /**
408
421
  * User credentials
409
422
  * @typedef {Object} UserCredentials
@@ -0,0 +1,54 @@
1
+ const JSZip = require('jszip');
2
+
3
+ /**
4
+ * @typedef {Object} TWPacket
5
+ * @prop {string} [m] Packet type
6
+ * @prop {[session: string, {}]} [p] Packet data
7
+ */
8
+
9
+ module.exports = {
10
+ /**
11
+ * Parse websocket packet
12
+ * @param {string} str Websocket raw data
13
+ * @returns {TWPacket[]} TradingView packets
14
+ */
15
+ parseWSPacket(str) {
16
+ return str.replace(/~h~/g, '').split(/~m~[0-9]{1,}~m~/g)
17
+ .map((p) => {
18
+ if (!p) return false;
19
+ try {
20
+ return JSON.parse(p);
21
+ } catch (error) {
22
+ console.warn('Cant parse', p);
23
+ return false;
24
+ }
25
+ })
26
+ .filter((p) => p);
27
+ },
28
+
29
+ /**
30
+ * Format websocket packet
31
+ * @param {TWPacket} packet TradingView packet
32
+ * @returns {string} Websocket raw data
33
+ */
34
+ formatWSPacket(packet) {
35
+ const msg = typeof packet === 'object'
36
+ ? JSON.stringify(packet)
37
+ : packet;
38
+ return `~m~${msg.length}~m~${msg}`;
39
+ },
40
+
41
+ /**
42
+ * Parse compressed data
43
+ * @param {string} data Compressed data
44
+ * @returns {Promise<{}>} Parsed data
45
+ */
46
+ async parseCompressed(data) {
47
+ const zip = new JSZip();
48
+ return JSON.parse(
49
+ await (
50
+ await zip.loadAsync(data, { base64: true })
51
+ ).file('').async('text'),
52
+ );
53
+ },
54
+ };
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @typedef {'loaded' | 'data' | 'error'} MarketEvent
3
+ */
4
+
5
+ /**
6
+ * @param {import('./session').QuoteSessionBridge} quoteSession
7
+ */
8
+ module.exports = (quoteSession) => class QuoteMarket {
9
+ #symbolListeners = quoteSession.symbolListeners;
10
+
11
+ #symbol;
12
+
13
+ #symbolListenerID = 0;
14
+
15
+ #lastData = {};
16
+
17
+ #callbacks = {
18
+ loaded: [],
19
+ data: [],
20
+
21
+ event: [],
22
+ error: [],
23
+ };
24
+
25
+ /**
26
+ * @param {MarketEvent} ev Client event
27
+ * @param {...{}} data Packet data
28
+ */
29
+ #handleEvent(ev, ...data) {
30
+ this.#callbacks[ev].forEach((e) => e(...data));
31
+ this.#callbacks.event.forEach((e) => e(ev, ...data));
32
+ }
33
+
34
+ #handleError(...msgs) {
35
+ if (this.#callbacks.error.length === 0) console.error(...msgs);
36
+ else this.#handleEvent('error', ...msgs);
37
+ }
38
+
39
+ /**
40
+ * @param {string} symbol Market symbol (like: 'BTCEUR' or 'KRAKEN:BTCEUR')
41
+ */
42
+ constructor(symbol) {
43
+ this.#symbol = symbol;
44
+
45
+ if (!this.#symbolListeners[symbol]) {
46
+ this.#symbolListeners[symbol] = [];
47
+ quoteSession.send('quote_add_symbols', [
48
+ quoteSession.sessionID,
49
+ symbol,
50
+ ]);
51
+ }
52
+ this.#symbolListenerID = this.#symbolListeners[symbol].length;
53
+
54
+ this.#symbolListeners[symbol][this.#symbolListenerID] = (packet) => {
55
+ if (global.TW_DEBUG) console.log('§90§30§105 MARKET §0 DATA', packet);
56
+
57
+ if (packet.type === 'qsd' && packet.data[1].s === 'ok') {
58
+ this.#lastData = {
59
+ ...this.#lastData,
60
+ ...packet.data[1].v,
61
+ };
62
+ this.#handleEvent('data', this.#lastData);
63
+ return;
64
+ }
65
+
66
+ if (packet.type === 'quote_completed') {
67
+ this.#handleEvent('loaded');
68
+ return;
69
+ }
70
+
71
+ if (packet.type === 'qsd' && packet.data[1].s === 'error') {
72
+ this.#handleError('Market error', packet.data);
73
+ }
74
+ };
75
+ }
76
+
77
+ /**
78
+ * When quote market is loaded
79
+ * @param {() => void} cb Callback
80
+ * @event
81
+ */
82
+ onLoaded(cb) {
83
+ this.#callbacks.loaded.push(cb);
84
+ }
85
+
86
+ /**
87
+ * When quote data is received
88
+ * @param {(data: {}) => void} cb Callback
89
+ * @event
90
+ */
91
+ onData(cb) {
92
+ this.#callbacks.data.push(cb);
93
+ }
94
+
95
+ /**
96
+ * When quote event happens
97
+ * @param {(...any) => void} cb Callback
98
+ * @event
99
+ */
100
+ onEvent(cb) {
101
+ this.#callbacks.event.push(cb);
102
+ }
103
+
104
+ /**
105
+ * When quote error happens
106
+ * @param {(...any) => void} cb Callback
107
+ * @event
108
+ */
109
+ onError(cb) {
110
+ this.#callbacks.error.push(cb);
111
+ }
112
+
113
+ /** Close this listener */
114
+ close() {
115
+ if (this.#symbolListeners[this.#symbol].length <= 1) {
116
+ quoteSession.send('quote_remove_symbols', [
117
+ quoteSession.sessionID,
118
+ this.#symbol,
119
+ ]);
120
+ }
121
+ delete this.#symbolListeners[this.#symbol][this.#symbolListenerID];
122
+ }
123
+ };
@@ -0,0 +1,125 @@
1
+ const { genSessionID } = require('../utils');
2
+
3
+ const quoteMarketConstructor = require('./market');
4
+
5
+ /** @typedef {Object<string, Function[]>} SymbolListeners */
6
+
7
+ /**
8
+ * @typedef {Object} QuoteSessionBridge
9
+ * @prop {string} sessionID
10
+ * @prop {SymbolListeners} symbolListeners
11
+ * @prop {import('../client').SendPacket} send
12
+ */
13
+
14
+ /**
15
+ * @typedef {'base-currency-logoid'
16
+ * | 'ch' | 'chp' | 'currency-logoid' | 'provider_id'
17
+ * | 'currency_code' | 'current_session' | 'description'
18
+ * | 'exchange' | 'format' | 'fractional' | 'is_tradable'
19
+ * | 'language' | 'local_description' | 'logoid' | 'lp'
20
+ * | 'lp_time' | 'minmov' | 'minmove2' | 'original_name'
21
+ * | 'pricescale' | 'pro_name' | 'short_name' | 'type'
22
+ * | 'update_mode' | 'volume' | 'ask' | 'bid' | 'fundamentals'
23
+ * | 'high_price' | 'low_price' | 'open_price' | 'prev_close_price'
24
+ * | 'rch' | 'rchp' | 'rtc' | 'rtc_time' | 'status' | 'industry'
25
+ * | 'basic_eps_net_income' | 'beta_1_year' | 'market_cap_basic'
26
+ * | 'earnings_per_share_basic_ttm' | 'price_earnings_ttm'
27
+ * | 'sector' | 'dividends_yield' | 'timezone' | 'country_code'
28
+ * } quoteField Quote data field
29
+ */
30
+
31
+ /** @param {'all' | 'price'} fieldsType */
32
+ function getQuoteFields(fieldsType) {
33
+ if (fieldsType === 'price') {
34
+ return ['lp'];
35
+ }
36
+
37
+ return [
38
+ 'base-currency-logoid', 'ch', 'chp', 'currency-logoid',
39
+ 'currency_code', 'current_session', 'description',
40
+ 'exchange', 'format', 'fractional', 'is_tradable',
41
+ 'language', 'local_description', 'logoid', 'lp',
42
+ 'lp_time', 'minmov', 'minmove2', 'original_name',
43
+ 'pricescale', 'pro_name', 'short_name', 'type',
44
+ 'update_mode', 'volume', 'ask', 'bid', 'fundamentals',
45
+ 'high_price', 'low_price', 'open_price', 'prev_close_price',
46
+ 'rch', 'rchp', 'rtc', 'rtc_time', 'status', 'industry',
47
+ 'basic_eps_net_income', 'beta_1_year', 'market_cap_basic',
48
+ 'earnings_per_share_basic_ttm', 'price_earnings_ttm',
49
+ 'sector', 'dividends_yield', 'timezone', 'country_code',
50
+ 'provider_id',
51
+ ];
52
+ }
53
+
54
+ /**
55
+ * @param {import('../client').ClientBridge} client
56
+ */
57
+ module.exports = (client) => class QuoteSession {
58
+ #sessionID = genSessionID('qs');
59
+
60
+ /** Parent client */
61
+ #client = client;
62
+
63
+ /** @type {SymbolListeners} */
64
+ #symbolListeners = {};
65
+
66
+ /**
67
+ * @typedef {Object} quoteSessionOptions Quote Session options
68
+ * @prop {'all' | 'price'} [fields] Asked quote fields
69
+ * @prop {quoteField[]} [customFields] List of asked quote fields
70
+ */
71
+
72
+ /**
73
+ * @param {quoteSessionOptions} options Quote settings options
74
+ */
75
+ constructor(options = {}) {
76
+ this.#client.sessions[this.#sessionID] = {
77
+ type: 'quote',
78
+ onData: (packet) => {
79
+ if (global.TW_DEBUG) console.log('§90§30§102 QUOTE SESSION §0 DATA', packet);
80
+
81
+ if (packet.type === 'quote_completed') {
82
+ const symbol = packet.data[1];
83
+ if (!this.#symbolListeners[symbol]) {
84
+ this.#client.send('quote_remove_symbols', [this.#sessionID, symbol]);
85
+ return;
86
+ }
87
+ this.#symbolListeners[symbol].forEach((h) => h(packet));
88
+ }
89
+
90
+ if (packet.type === 'qsd') {
91
+ const symbol = packet.data[1].n;
92
+ if (!this.#symbolListeners[symbol]) {
93
+ this.#client.send('quote_remove_symbols', [this.#sessionID, symbol]);
94
+ return;
95
+ }
96
+ this.#symbolListeners[symbol].forEach((h) => h(packet));
97
+ }
98
+ },
99
+ };
100
+
101
+ const fields = (options.customFields && options.customFields.length > 0
102
+ ? options.customFields
103
+ : getQuoteFields(options.fields)
104
+ );
105
+
106
+ this.#client.send('quote_create_session', [this.#sessionID]);
107
+ this.#client.send('quote_set_fields', [this.#sessionID, ...fields]);
108
+ }
109
+
110
+ /** @type {QuoteSessionBridge} */
111
+ #quoteSession = {
112
+ sessionID: this.#sessionID,
113
+ symbolListeners: this.#symbolListeners,
114
+ send: (t, p) => this.#client.send(t, p),
115
+ };
116
+
117
+ /** @constructor */
118
+ Market = quoteMarketConstructor(this.#quoteSession);
119
+
120
+ /** Delete the quote session */
121
+ delete() {
122
+ this.#client.send('quote_delete_session', [this.#sessionID]);
123
+ delete this.#client.sessions[this.#sessionID];
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;
package/src/types.js ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @typedef {string} MarketSymbol Market symbol (like: 'BTCEUR' or 'KRAKEN:BTCEUR')
3
+ *
4
+ * @typedef {'Etc/UTC' | 'exchange'
5
+ * | 'Pacific/Honolulu' | 'America/Juneau' | 'America/Los_Angeles'
6
+ * | 'America/Phoenix' | 'America/Vancouver' | 'US/Mountain'
7
+ * | 'America/El_Salvador' | 'America/Bogota' | 'America/Chicago'
8
+ * | 'America/Lima' | 'America/Mexico_City' | 'America/Caracas'
9
+ * | 'America/New_York' | 'America/Toronto' | 'America/Argentina/Buenos_Aires'
10
+ * | 'America/Santiago' | 'America/Sao_Paulo' | 'Atlantic/Reykjavik'
11
+ * | 'Europe/Dublin' | 'Africa/Lagos' | 'Europe/Lisbon' | 'Europe/London'
12
+ * | 'Europe/Amsterdam' | 'Europe/Belgrade' | 'Europe/Berlin'
13
+ * | 'Europe/Brussels' | 'Europe/Copenhagen' | 'Africa/Johannesburg'
14
+ * | 'Africa/Cairo' | 'Europe/Luxembourg' | 'Europe/Madrid' | 'Europe/Malta'
15
+ * | 'Europe/Oslo' | 'Europe/Paris' | 'Europe/Rome' | 'Europe/Stockholm'
16
+ * | 'Europe/Warsaw' | 'Europe/Zurich' | 'Europe/Athens' | 'Asia/Bahrain'
17
+ * | 'Europe/Helsinki' | 'Europe/Istanbul' | 'Asia/Jerusalem' | 'Asia/Kuwait'
18
+ * | 'Europe/Moscow' | 'Asia/Qatar' | 'Europe/Riga' | 'Asia/Riyadh'
19
+ * | 'Europe/Tallinn' | 'Europe/Vilnius' | 'Asia/Tehran' | 'Asia/Dubai'
20
+ * | 'Asia/Muscat' | 'Asia/Ashkhabad' | 'Asia/Kolkata' | 'Asia/Almaty'
21
+ * | 'Asia/Bangkok' | 'Asia/Jakarta' | 'Asia/Ho_Chi_Minh' | 'Asia/Chongqing'
22
+ * | 'Asia/Hong_Kong' | 'Australia/Perth' | 'Asia/Shanghai' | 'Asia/Singapore'
23
+ * | 'Asia/Taipei' | 'Asia/Seoul' | 'Asia/Tokyo' | 'Australia/Brisbane'
24
+ * | 'Australia/Adelaide' | 'Australia/Sydney' | 'Pacific/Norfolk'
25
+ * | 'Pacific/Auckland' | 'Pacific/Fakaofo' | 'Pacific/Chatham'} Timezone (Chart) timezone
26
+ *
27
+ * @typedef {'1' | '3' | '5' | '15' | '30'
28
+ * | '45' | '60' | '120' | '180' | '240'
29
+ * | '1D' | '1W' | '1M' | 'D' | 'W' | 'M'} TimeFrame
30
+ */
31
+
32
+ module.exports = {};
package/src/utils.js ADDED
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ genSessionID(type = 'xs') {
3
+ let r = '';
4
+ const c = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
5
+ for (let i = 0; i < 12; i += 1) r += c.charAt(Math.floor(Math.random() * c.length));
6
+ return `${type}_${r}`;
7
+ },
8
+ };