market-data-tradingview-ws 0.0.14 → 0.1.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/lib/client.js +118 -97
- package/lib/tradingView.js +86 -29
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -108,6 +108,8 @@ module.exports = class Client {
|
|
|
108
108
|
};
|
|
109
109
|
|
|
110
110
|
#token = null;
|
|
111
|
+
#sessionid = null;
|
|
112
|
+
#sessionidsign = null;
|
|
111
113
|
|
|
112
114
|
constructor() {
|
|
113
115
|
// this.#resolved = new Map();
|
|
@@ -130,16 +132,8 @@ module.exports = class Client {
|
|
|
130
132
|
return { charts: this.#charts.values };
|
|
131
133
|
}
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
// }
|
|
136
|
-
|
|
137
|
-
// getSessionKeys() {
|
|
138
|
-
// return this.#sessions.values.keys();
|
|
139
|
-
// }
|
|
140
|
-
|
|
141
|
-
async getToken({ login, pass }) {
|
|
142
|
-
return tv.getToken({ login, pass });
|
|
135
|
+
async getToken({ sessionid, sessionidsign, tvecuid, imageurl }) {
|
|
136
|
+
return tv.getToken({ sessionid, sessionidsign, tvecuid, imageurl });
|
|
143
137
|
}
|
|
144
138
|
|
|
145
139
|
auth(token = 'unauthorized_user_token') {
|
|
@@ -151,38 +145,54 @@ module.exports = class Client {
|
|
|
151
145
|
});
|
|
152
146
|
}
|
|
153
147
|
|
|
154
|
-
async connect({ url, options,
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
148
|
+
async connect({ url, options, result, sessionid, sessionidsign, tvecuid, imageurl }) {
|
|
149
|
+
try {
|
|
150
|
+
this.#token = await this.getToken({ sessionid, sessionidsign, tvecuid, imageurl });
|
|
151
|
+
this.#sessionid = sessionid;
|
|
152
|
+
this.#sessionidsign = sessionidsign;
|
|
153
|
+
|
|
154
|
+
if (this.#token.length > 10) {
|
|
155
|
+
return new Promise((resolv, reject) => {
|
|
156
|
+
try {
|
|
157
|
+
url += '?from=chart%2F' + imageurl + '%2F';
|
|
158
|
+
const date = new Date();
|
|
159
|
+
url += '&date=' + date.getFullYear() + '_' + String(date.getMonth() + 1).padStart(2, '0') + '_' + String(date.getDate()).padStart(2, '0') + '-11_25';
|
|
160
|
+
url += '&type=chart';
|
|
161
|
+
ws = new WebSocket(url, options);
|
|
162
|
+
|
|
163
|
+
const originalSend = ws.send;
|
|
164
|
+
ws.send = function(data) {
|
|
165
|
+
// console.log('Message sent:', data);
|
|
166
|
+
originalSend.apply(ws, arguments);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
ws.onclose = () => console.log('disconnected');
|
|
170
|
+
|
|
171
|
+
ws.onerror = (error) => reject(console.error('error: ', error));
|
|
172
|
+
ws.onopen = () => {
|
|
173
|
+
ws.send(this.auth(this.#token));
|
|
174
|
+
ws.send(tv.setLocaleRu());
|
|
175
|
+
|
|
176
|
+
const quoteKey = this.createQuoteSmallSession();
|
|
177
|
+
|
|
178
|
+
this.messageListner({ result });
|
|
179
|
+
resolv('open');
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
reject(error);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
} catch (error) {
|
|
188
|
+
console.error('Error in connect:', error.message);
|
|
189
|
+
throw error;
|
|
184
190
|
}
|
|
185
|
-
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async disconnect() {
|
|
194
|
+
tv.disconnect({ sessionid: this.#sessionid, sessionidsign: this.#sessionidsign });
|
|
195
|
+
return 'close';
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
createQuoteSmallSession() {
|
|
@@ -235,6 +245,10 @@ module.exports = class Client {
|
|
|
235
245
|
}
|
|
236
246
|
|
|
237
247
|
addChartSymbol({ symbol, period, limit }) {
|
|
248
|
+
if (!symbol || typeof symbol !== 'string') throw new Error('Invalid symbol parameter');
|
|
249
|
+
if (!period || typeof period !== 'number') throw new Error('Invalid period parameter');
|
|
250
|
+
if (!limit || typeof limit !== 'number') throw new Error('Invalid limit parameter');
|
|
251
|
+
|
|
238
252
|
let key = this.#charts.getBySymbolPeriod({ symbol, period });
|
|
239
253
|
if (key) {
|
|
240
254
|
console.warn('addChartSymbol exist:', key, symbol, period);
|
|
@@ -260,7 +274,7 @@ module.exports = class Client {
|
|
|
260
274
|
frame: data.frame,
|
|
261
275
|
increment: data.i,
|
|
262
276
|
symbolKey: data.symbolKey,
|
|
263
|
-
|
|
277
|
+
period: periods[period],
|
|
264
278
|
limit,
|
|
265
279
|
}),
|
|
266
280
|
);
|
|
@@ -272,15 +286,6 @@ module.exports = class Client {
|
|
|
272
286
|
updateChartSymbol({ symbol, period, limit, last }) {
|
|
273
287
|
const key = this.#charts.getBySymbolPeriod({ symbol: last.symbol, period: last.period });
|
|
274
288
|
const data = this.#charts.get({ key });
|
|
275
|
-
|
|
276
|
-
// console.warn('modules updateChartSymbol 1: ', symbol, period, limit, last, data);
|
|
277
|
-
// const alreadyKey = this.#charts.getBySymbolPeriod({ symbol, period })
|
|
278
|
-
// if (alreadyKey) {
|
|
279
|
-
// // console.warn('updateChartSymbol exist: ', alreadyKey);
|
|
280
|
-
// this.#charts.delete({ key });
|
|
281
|
-
// // console.warn('updateChartSymbol exist');
|
|
282
|
-
// return 'exist';
|
|
283
|
-
// }
|
|
284
289
|
|
|
285
290
|
data.symbolKey = this.#symbols.getSymbol({ key, symbol }).key;
|
|
286
291
|
data.symbol = symbol;
|
|
@@ -293,7 +298,7 @@ module.exports = class Client {
|
|
|
293
298
|
frame: data.frame,
|
|
294
299
|
increment: data.i,
|
|
295
300
|
symbolKey: data.symbolKey,
|
|
296
|
-
|
|
301
|
+
period: periods[period],
|
|
297
302
|
}),
|
|
298
303
|
);
|
|
299
304
|
this.#charts.set({ key, data });
|
|
@@ -318,52 +323,68 @@ module.exports = class Client {
|
|
|
318
323
|
// console.log('raw: ', JSON.stringify(raw.toString()));
|
|
319
324
|
let session = '';
|
|
320
325
|
tv.parsePacket({ str: raw }).forEach((packet) => {
|
|
321
|
-
|
|
322
|
-
let send = null;
|
|
323
|
-
if (packet === undefined) {
|
|
324
|
-
console.warn('undefined: ', raw.toString());
|
|
325
|
-
} else {
|
|
326
|
-
if (packet.type === 'ping') {
|
|
327
|
-
// console.info('pong', packet);
|
|
328
|
-
ws.send(tv.createPong({ value: packet.data }));
|
|
329
|
-
} else if (packet.type === 'qsd') {
|
|
330
|
-
send = packet.data[1].v;
|
|
331
|
-
name = send.bid === undefined && send.ask === undefined ? 'data' : 'levelI';
|
|
332
|
-
// console.log(packet.data[1].n);
|
|
333
|
-
send.symbol =(extractJSON(packet.data[1].n) || {symbol: packet.data[1].n}).symbol;
|
|
334
|
-
result(name, send);
|
|
335
|
-
} else if (['du', 'timescale_update'].includes(packet.type)) {
|
|
336
|
-
session = this.#charts.get({ key: packet.data[0] });
|
|
337
|
-
if (packet.data[1][session.frame].s.length > 0) {
|
|
338
|
-
send = {
|
|
339
|
-
symbol: session.symbol,
|
|
340
|
-
period: parseInt(session.period),
|
|
341
|
-
chart: [],
|
|
342
|
-
end: packet.data[1][session.frame].lbs.bar_close_time,
|
|
343
|
-
};
|
|
344
|
-
for (let each of packet.data[1][session.frame].s) {
|
|
345
|
-
send.chart.push({
|
|
346
|
-
open: each.v[1],
|
|
347
|
-
high: each.v[2],
|
|
348
|
-
low: each.v[3],
|
|
349
|
-
close: each.v[4],
|
|
350
|
-
timestamp: each.v[0] * 1000,
|
|
351
|
-
// turnover: bar.TradeCount,
|
|
352
|
-
volume: each.v[5],
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
result(packet.type === 'du' ? 'chart_update' : 'chart_history', send);
|
|
356
|
-
}
|
|
357
|
-
} else if (['series', 'symbol', 'quote', 'session'].includes(packet.type)) {
|
|
358
|
-
// console.info(packet);
|
|
359
|
-
} else {
|
|
360
|
-
// console.error('error', raw.toString());
|
|
361
|
-
result('error', raw.toString())
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
// name = null;
|
|
365
|
-
// send = null;
|
|
326
|
+
this.handlePacket({ packet, result, raw });
|
|
366
327
|
});
|
|
367
328
|
});
|
|
368
329
|
}
|
|
330
|
+
|
|
331
|
+
handlePacket({ packet, result, raw }) {
|
|
332
|
+
if (!packet) {
|
|
333
|
+
console.warn('Undefined packet:', raw.toString());
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
switch (packet.type) {
|
|
338
|
+
case 'ping':
|
|
339
|
+
ws.send(tv.createPong({ value: packet.data }));
|
|
340
|
+
break;
|
|
341
|
+
case 'qsd':
|
|
342
|
+
this.handleQsdPacket({ packet, result });
|
|
343
|
+
break;
|
|
344
|
+
case 'du':
|
|
345
|
+
case 'timescale_update':
|
|
346
|
+
this.handleChartPacket({ packet, result });
|
|
347
|
+
break;
|
|
348
|
+
case 'session':
|
|
349
|
+
console.info(packet);
|
|
350
|
+
break;
|
|
351
|
+
case 'symbol':
|
|
352
|
+
case 'series':
|
|
353
|
+
case 'quote':
|
|
354
|
+
break;
|
|
355
|
+
default:
|
|
356
|
+
console.error('Unhandled packet type:', packet.type);
|
|
357
|
+
result('error', raw.toString());
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
handleQsdPacket({ packet, result }) {
|
|
362
|
+
const send = packet.data[1].v;
|
|
363
|
+
const name = send.bid === undefined && send.ask === undefined ? 'data' : 'levelI';
|
|
364
|
+
send.symbol = (extractJSON(packet.data[1].n) || { symbol: packet.data[1].n }).symbol;
|
|
365
|
+
result(name, send);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
handleChartPacket({ packet, result }) {
|
|
369
|
+
const session = this.#charts.get({ key: packet.data[0] });
|
|
370
|
+
if (packet.data[1][session.frame].s.length > 0) {
|
|
371
|
+
const send = {
|
|
372
|
+
symbol: session.symbol,
|
|
373
|
+
period: parseInt(session.period),
|
|
374
|
+
chart: [],
|
|
375
|
+
end: packet.data[1][session.frame].lbs.bar_close_time,
|
|
376
|
+
};
|
|
377
|
+
packet.data[1][session.frame].s.forEach((each) => {
|
|
378
|
+
send.chart.push({
|
|
379
|
+
open: each.v[1],
|
|
380
|
+
high: each.v[2],
|
|
381
|
+
low: each.v[3],
|
|
382
|
+
close: each.v[4],
|
|
383
|
+
timestamp: each.v[0] * 1000,
|
|
384
|
+
volume: each.v[5],
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
result(packet.type === 'du' ? 'chart_update' : 'chart_history', send);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
369
390
|
};
|
package/lib/tradingView.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
/* eslint-disable no-unused-vars */
|
|
2
1
|
'use strict';
|
|
3
2
|
|
|
4
|
-
// const Utils = require('./utils.js');
|
|
5
|
-
// const utils = new Utils();
|
|
6
|
-
|
|
7
3
|
const letters = 'abcdefghijklmnopqrstuvwxyz';
|
|
8
4
|
const cleanerRgx = /~h~/g;
|
|
9
5
|
const splitterRgx = /~m~[0-9]{1,}~m~/g;
|
|
10
6
|
const urlSymbolSearch = 'https://symbol-search.tradingview.com/symbol_search';
|
|
7
|
+
const urlwwwTV = 'https://www.tradingview.com';
|
|
8
|
+
const urlRuTV = 'https://ru.tradingview.com';
|
|
11
9
|
|
|
12
10
|
class TradingView {
|
|
13
|
-
async
|
|
14
|
-
const url = '
|
|
11
|
+
async authentication({ login, pass }) {
|
|
12
|
+
const url = urlwwwTV + '/accounts/signin/';
|
|
15
13
|
const headers = {
|
|
16
|
-
Referer:
|
|
14
|
+
Referer: urlwwwTV,
|
|
17
15
|
};
|
|
18
16
|
|
|
19
17
|
const form = new FormData();
|
|
@@ -21,35 +19,91 @@ class TradingView {
|
|
|
21
19
|
form.append('password', pass);
|
|
22
20
|
form.append('remember', 'on');
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
22
|
+
try {
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers,
|
|
26
|
+
body: form,
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!res.ok) throw new Error('Failed to get auth_token, status: ' + res.status);
|
|
30
|
+
|
|
31
|
+
const rawSetCookie = response.headers.raw()['set-cookie'];
|
|
32
|
+
console.log('Raw Set-Cookie headers:', rawSetCookie);
|
|
33
|
+
let sessionid, sessionid_sign;
|
|
34
|
+
for (const cookie of rawSetCookie) {
|
|
35
|
+
if (cookie.startsWith('sessionid=')) sessionid = cookie.split(';')[0].split('=')[1];
|
|
36
|
+
if (cookie.startsWith('sessionid_sign=')) sessionid_sign = cookie.split(';')[0].split('=')[1];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const data = await res.json();
|
|
40
|
+
if (!data?.user?.auth_token) throw new Error('Error get auth_token, no auth_token in response');
|
|
41
|
+
return { sessionid, sessionid_sign, token: data.user.auth_token };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
throw new Error('Authentication failed: ' + error.message);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async getToken({ sessionid, sessionidsign, tvecuid, imageurl }) {
|
|
48
|
+
const url = urlRuTV + '/chart/' + imageurl;
|
|
49
|
+
const headers = {
|
|
50
|
+
'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
51
|
+
'accept-language': 'ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7',
|
|
52
|
+
'cache-control': 'max-age=0',
|
|
53
|
+
'upgrade-insecure-requests': '1',
|
|
54
|
+
'cookie': 'tv_ecuid=' + tvecuid + '; sessionid=' + sessionid + '; sessionid_sign=' + sessionidsign + ';cookiesSettings={\"analytics\":true,\"advertising\":true};backend=test_backend',
|
|
55
|
+
'Referer': urlRuTV + '/u/PartnerFinance/',
|
|
56
|
+
'Referrer-Policy': 'origin-when-cross-origin',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const res = await fetch(url, { method: 'GET', headers });
|
|
61
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
62
|
+
const html = await res.text();
|
|
63
|
+
const match = html.match(/"auth_token"\s*:\s*"([^"]+)"/);
|
|
64
|
+
return match[1];
|
|
65
|
+
} catch (err) {
|
|
66
|
+
console.error('❌ Error fetching token:', err.message);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async disconnect({ sessionid, sessionidsign }) {
|
|
72
|
+
const url = urlRuTV + '/accounts/logout/';
|
|
73
|
+
const headers = {
|
|
74
|
+
Origin: urlRuTV,
|
|
75
|
+
Referer: urlRuTV + '/u/PartnerFinance/',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// const res = await fetch(url, {
|
|
80
|
+
// method: 'POST',
|
|
81
|
+
// headers,
|
|
82
|
+
// });
|
|
83
|
+
|
|
84
|
+
// if (!res.ok) throw new Error('Failed to disconnect, status: ' + res.status);
|
|
85
|
+
return true;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
throw new Error('Disconnection failed: ' + error.message);
|
|
88
|
+
}
|
|
39
89
|
}
|
|
40
90
|
|
|
41
91
|
async getSymbol({ symbol, type }) {
|
|
42
92
|
// type = 'stock' | 'futures' | 'forex' | 'cfd' | 'crypto' | 'index' | 'economic'
|
|
43
93
|
// query = what you want to search!
|
|
44
94
|
// it returns first matching item
|
|
95
|
+
try {
|
|
45
96
|
const res = await fetch(urlSymbolSearch + '?text=' + symbol + '&type=' + type);
|
|
46
97
|
if (res.status === 200) {
|
|
47
98
|
const data = await res.json();
|
|
48
99
|
// console.log(data);
|
|
49
|
-
|
|
100
|
+
return data[0];
|
|
101
|
+
}
|
|
102
|
+
throw new Error('Failed to fetch symbol data');
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error('Error in getSymbol:', error.message);
|
|
105
|
+
return 'Network Error!';
|
|
50
106
|
}
|
|
51
|
-
|
|
52
|
-
return 'Network Error!';
|
|
53
107
|
}
|
|
54
108
|
|
|
55
109
|
getSymbolId({ data }) {
|
|
@@ -58,6 +112,9 @@ class TradingView {
|
|
|
58
112
|
}
|
|
59
113
|
|
|
60
114
|
createSession({ startsWith }) {
|
|
115
|
+
if (typeof startsWith !== 'string' || startsWith.length === 0) {
|
|
116
|
+
throw new Error('Invalid startsWith parameter');
|
|
117
|
+
}
|
|
61
118
|
let session = startsWith;
|
|
62
119
|
while (session.length < 12) session += letters[Math.floor(Math.random() * letters.length)];
|
|
63
120
|
return session;
|
|
@@ -268,7 +325,7 @@ class TradingView {
|
|
|
268
325
|
// frame - рамка или окно для данной серии
|
|
269
326
|
// increment - инкрементируемый номер изменений запрашиваемого графика в данном frame (таймефрейм)
|
|
270
327
|
createSeries({ chartSession, frame = 'sds_1', increment = 's1', symbolKey, period = '60', limit = 1000 }) {
|
|
271
|
-
// console.warn('createSeries', chartSession, frame, increment, symbolKey,
|
|
328
|
+
// console.warn('createSeries', chartSession, frame, increment, symbolKey, period, limit);
|
|
272
329
|
return this.formatWSPacket({
|
|
273
330
|
packet: { m: 'create_series', p: [chartSession, frame, increment, symbolKey, period, limit] },
|
|
274
331
|
});
|
|
@@ -285,10 +342,10 @@ class TradingView {
|
|
|
285
342
|
return this.formatWSPacket({ packet: { m: 'remove_study', p: [ session, st ] } });
|
|
286
343
|
}
|
|
287
344
|
|
|
288
|
-
modifySeries({ chartSession, frame = 'sds_1', increment = 's1', symbolKey,
|
|
289
|
-
// console.warn('modify_series', chartSession, frame, increment, symbolKey,
|
|
345
|
+
modifySeries({ chartSession, frame = 'sds_1', increment = 's1', symbolKey, period = '60' }) {
|
|
346
|
+
// console.warn('modify_series', chartSession, frame, increment, symbolKey, pariod, limit);
|
|
290
347
|
return this.formatWSPacket({
|
|
291
|
-
packet: { m: 'modify_series', p: [chartSession, frame, increment, symbolKey,
|
|
348
|
+
packet: { m: 'modify_series', p: [chartSession, frame, increment, symbolKey, period] },
|
|
292
349
|
});
|
|
293
350
|
}
|
|
294
351
|
|