@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.
- package/.eslintrc.js +18 -16
- package/README.md +19 -98
- package/examples/AllPrivateIndicators.js +35 -0
- package/examples/BuiltInIndicator.js +41 -0
- package/examples/CurrentTimeframe.js +34 -0
- package/examples/CustomChartType.js +122 -0
- package/examples/CustomTimeframe.js +29 -0
- package/examples/Errors.js +147 -0
- package/examples/FakeReplayMode.js +41 -0
- package/examples/FromToData.js +29 -0
- package/examples/GraphicIndicator.js +36 -0
- package/examples/MultipleSyncFetch.js +38 -0
- package/examples/PinePermManage.js +57 -0
- package/examples/ReplayMode.js +97 -0
- package/examples/Search.js +15 -0
- package/examples/SimpleChart.js +63 -0
- package/examples/_RenkoIndicator.js +41 -0
- package/main.js +11 -504
- package/package.json +9 -8
- package/src/FormData.js +35 -0
- package/src/chart/graphicParser.js +308 -0
- package/src/chart/session.js +546 -0
- package/src/chart/study.js +403 -0
- package/src/classes/BuiltInIndicator.js +124 -0
- package/src/classes/PineIndicator.js +132 -0
- package/src/classes/PinePermManager.js +142 -0
- package/src/client.js +280 -0
- package/{miscRequests.js → src/miscRequests.js} +102 -89
- package/src/protocol.js +54 -0
- package/src/quote/market.js +123 -0
- package/src/quote/session.js +125 -0
- package/src/request.js +37 -0
- package/src/types.js +32 -0
- package/src/utils.js +8 -0
- package/test.js +64 -0
- package/tests/1. Search.js +18 -0
- package/tests/2. QuoteSession.js +54 -0
- package/tests/3. SimpleChart.js +61 -0
- package/tests/4. Indicators.js +99 -0
- package/tests/5. BuiltInIndicator.js +41 -0
- package/tests/6. FakeReplayMode.js +59 -0
- package/tests/7. ReplayMode.js +51 -0
- package/tests/8. CustomChartTypes.js +115 -0
- package/tests/9. AllErrors.js +146 -0
- package/tests/disabled/x. Template.js +24 -0
- package/.github/FUNDING.yml +0 -1
- package/tests/alerts/discordWH.js +0 -76
- package/tests/alerts/drawings.js +0 -41
- package/tests/alerts/envLoader.js +0 -11
- package/tests/alerts/manualAlert.js +0 -77
- package/tests/alerts/sentNotifs.js +0 -16
- package/tests/alerts/strategyAlert.js +0 -94
- package/tests/analysis.js +0 -13
- package/tests/indicator.js +0 -42
- package/tests/prices.js +0 -33
- 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
|
|
4
|
-
const
|
|
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
|
-
|
|
35
|
-
|
|
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}
|
|
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<
|
|
121
|
+
* @returns {Promise<SearchMarketResult[]>} Search results
|
|
151
122
|
*/
|
|
152
|
-
async
|
|
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}
|
|
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'
|
|
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<
|
|
169
|
+
* @returns {Promise<SearchIndicatorResult[]>} Search results
|
|
197
170
|
*/
|
|
198
171
|
async searchIndicator(search = '') {
|
|
199
|
-
if (!
|
|
172
|
+
if (!builtInIndicList.length) {
|
|
200
173
|
await Promise.all(['standard', 'candlestick', 'fundamental'].map(async (type) => {
|
|
201
|
-
|
|
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
|
-
...
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
|
|
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:
|
|
313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
package/src/protocol.js
ADDED
|
@@ -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