market-data-tradingview-ws 0.0.15 → 0.1.1
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 -96
- package/lib/tradingView.js +88 -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,55 @@ 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
|
+
// console.info('Token:', this.#token);
|
|
152
|
+
this.#sessionid = sessionid;
|
|
153
|
+
this.#sessionidsign = sessionidsign;
|
|
154
|
+
|
|
155
|
+
if (this.#token.length > 10) {
|
|
156
|
+
return new Promise((resolv, reject) => {
|
|
157
|
+
try {
|
|
158
|
+
url += '?from=chart%2F' + imageurl + '%2F';
|
|
159
|
+
const date = new Date();
|
|
160
|
+
url += '&date=' + date.getFullYear() + '_' + String(date.getMonth() + 1).padStart(2, '0') + '_' + String(date.getDate()).padStart(2, '0') + '-11_25';
|
|
161
|
+
url += '&type=chart';
|
|
162
|
+
ws = new WebSocket(url, options);
|
|
163
|
+
|
|
164
|
+
const originalSend = ws.send;
|
|
165
|
+
ws.send = function(data) {
|
|
166
|
+
// console.log('Message sent:', data);
|
|
167
|
+
originalSend.apply(ws, arguments);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
ws.onclose = () => console.log('disconnected');
|
|
171
|
+
|
|
172
|
+
ws.onerror = (error) => reject(console.error('error: ', error));
|
|
173
|
+
ws.onopen = () => {
|
|
174
|
+
ws.send(this.auth(this.#token));
|
|
175
|
+
ws.send(tv.setLocaleRu());
|
|
176
|
+
|
|
177
|
+
const quoteKey = this.createQuoteSmallSession();
|
|
178
|
+
|
|
179
|
+
this.messageListner({ result });
|
|
180
|
+
resolv('open');
|
|
181
|
+
};
|
|
182
|
+
} catch (error) {
|
|
183
|
+
reject(error);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return null;
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error('Error in connect:', error.message);
|
|
190
|
+
throw error;
|
|
184
191
|
}
|
|
185
|
-
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async disconnect() {
|
|
195
|
+
tv.disconnect({ sessionid: this.#sessionid, sessionidsign: this.#sessionidsign });
|
|
196
|
+
return 'close';
|
|
186
197
|
}
|
|
187
198
|
|
|
188
199
|
createQuoteSmallSession() {
|
|
@@ -234,7 +245,11 @@ module.exports = class Client {
|
|
|
234
245
|
return 'delete';
|
|
235
246
|
}
|
|
236
247
|
|
|
237
|
-
addChartSymbol({ symbol, period, limit }) {
|
|
248
|
+
addChartSymbol({ symbol, period, limit = 1500 }) {
|
|
249
|
+
if (!symbol || typeof symbol !== 'string') console.debug('Invalid symbol parameter', symbol); // throw new Error('Invalid symbol parameter', symbol);
|
|
250
|
+
if (!period || typeof period !== 'number') console.debug('Invalid period parameter', period); // throw new Error('Invalid period parameter', period);
|
|
251
|
+
if (!limit || typeof limit !== 'number') console.debug('Invalid limit parameter', limit); // throw new Error('Invalid limit parameter', limit);
|
|
252
|
+
|
|
238
253
|
let key = this.#charts.getBySymbolPeriod({ symbol, period });
|
|
239
254
|
if (key) {
|
|
240
255
|
console.warn('addChartSymbol exist:', key, symbol, period);
|
|
@@ -272,15 +287,6 @@ module.exports = class Client {
|
|
|
272
287
|
updateChartSymbol({ symbol, period, limit, last }) {
|
|
273
288
|
const key = this.#charts.getBySymbolPeriod({ symbol: last.symbol, period: last.period });
|
|
274
289
|
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
290
|
|
|
285
291
|
data.symbolKey = this.#symbols.getSymbol({ key, symbol }).key;
|
|
286
292
|
data.symbol = symbol;
|
|
@@ -318,52 +324,68 @@ module.exports = class Client {
|
|
|
318
324
|
// console.log('raw: ', JSON.stringify(raw.toString()));
|
|
319
325
|
let session = '';
|
|
320
326
|
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;
|
|
327
|
+
this.handlePacket({ packet, result, raw });
|
|
366
328
|
});
|
|
367
329
|
});
|
|
368
330
|
}
|
|
331
|
+
|
|
332
|
+
handlePacket({ packet, result, raw }) {
|
|
333
|
+
if (!packet) {
|
|
334
|
+
console.warn('Undefined packet:', raw.toString());
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
switch (packet.type) {
|
|
339
|
+
case 'ping':
|
|
340
|
+
ws.send(tv.createPong({ value: packet.data }));
|
|
341
|
+
break;
|
|
342
|
+
case 'qsd':
|
|
343
|
+
this.handleQsdPacket({ packet, result });
|
|
344
|
+
break;
|
|
345
|
+
case 'du':
|
|
346
|
+
case 'timescale_update':
|
|
347
|
+
this.handleChartPacket({ packet, result });
|
|
348
|
+
break;
|
|
349
|
+
case 'session':
|
|
350
|
+
console.info(packet);
|
|
351
|
+
break;
|
|
352
|
+
case 'symbol':
|
|
353
|
+
case 'series':
|
|
354
|
+
case 'quote':
|
|
355
|
+
break;
|
|
356
|
+
default:
|
|
357
|
+
console.error('Unhandled packet type:', packet.type);
|
|
358
|
+
result('error', raw.toString());
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
handleQsdPacket({ packet, result }) {
|
|
363
|
+
const send = packet.data[1].v;
|
|
364
|
+
const name = send.bid === undefined && send.ask === undefined ? 'data' : 'levelI';
|
|
365
|
+
send.symbol = (extractJSON(packet.data[1].n) || { symbol: packet.data[1].n }).symbol;
|
|
366
|
+
result(name, send);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
handleChartPacket({ packet, result }) {
|
|
370
|
+
const session = this.#charts.get({ key: packet.data[0] });
|
|
371
|
+
if (packet.data[1][session.frame].s.length > 0) {
|
|
372
|
+
const send = {
|
|
373
|
+
symbol: session.symbol,
|
|
374
|
+
period: parseInt(session.period),
|
|
375
|
+
chart: [],
|
|
376
|
+
end: packet.data[1][session.frame].lbs.bar_close_time,
|
|
377
|
+
};
|
|
378
|
+
packet.data[1][session.frame].s.forEach((each) => {
|
|
379
|
+
send.chart.push({
|
|
380
|
+
open: each.v[1],
|
|
381
|
+
high: each.v[2],
|
|
382
|
+
low: each.v[3],
|
|
383
|
+
close: each.v[4],
|
|
384
|
+
timestamp: each.v[0] * 1000,
|
|
385
|
+
volume: each.v[5],
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
result(packet.type === 'du' ? 'chart_update' : 'chart_history', send);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
369
391
|
};
|
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,43 +19,104 @@ 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 cookie = 'tv_ecuid=' + tvecuid + '; sessionid=' + sessionid + '; sessionid_sign=' + sessionidsign;
|
|
50
|
+
const headers = {
|
|
51
|
+
'accept': 'text/html',
|
|
52
|
+
'cookie': cookie,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch(url, { method: 'GET', headers });
|
|
57
|
+
console.warn('Response status:', res.status);
|
|
58
|
+
const html = await res.text();
|
|
59
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
60
|
+
const match = html.match(/"auth_token"\s*:\s*"([^"]+)"/);
|
|
61
|
+
const token = match ? match[1] : null;
|
|
62
|
+
console.warn('✅ Auth token found');
|
|
63
|
+
if (!token) {
|
|
64
|
+
throw new Error('❌ Auth token not found in HTML');
|
|
65
|
+
}
|
|
66
|
+
return token;
|
|
67
|
+
} catch (err) {
|
|
68
|
+
console.error('❌ Error fetching token:', err.message);
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async disconnect({ sessionid, sessionidsign }) {
|
|
74
|
+
const url = urlRuTV + '/accounts/logout/';
|
|
75
|
+
const headers = {
|
|
76
|
+
Origin: urlRuTV,
|
|
77
|
+
Referer: urlRuTV + '/u/PartnerFinance/',
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
// const res = await fetch(url, {
|
|
82
|
+
// method: 'POST',
|
|
83
|
+
// headers,
|
|
84
|
+
// });
|
|
85
|
+
|
|
86
|
+
// if (!res.ok) throw new Error('Failed to disconnect, status: ' + res.status);
|
|
87
|
+
return true;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
throw new Error('Disconnection failed: ' + error.message);
|
|
90
|
+
}
|
|
39
91
|
}
|
|
40
92
|
|
|
41
93
|
async getSymbol({ symbol, type }) {
|
|
42
94
|
// type = 'stock' | 'futures' | 'forex' | 'cfd' | 'crypto' | 'index' | 'economic'
|
|
43
95
|
// query = what you want to search!
|
|
44
96
|
// it returns first matching item
|
|
97
|
+
try {
|
|
45
98
|
const res = await fetch(urlSymbolSearch + '?text=' + symbol + '&type=' + type);
|
|
46
99
|
if (res.status === 200) {
|
|
47
100
|
const data = await res.json();
|
|
48
101
|
// console.log(data);
|
|
49
|
-
|
|
102
|
+
return data[0];
|
|
103
|
+
}
|
|
104
|
+
throw new Error('Failed to fetch symbol data');
|
|
105
|
+
} catch (error) {
|
|
106
|
+
console.error('Error in getSymbol:', error.message);
|
|
107
|
+
return 'Network Error!';
|
|
50
108
|
}
|
|
51
|
-
|
|
52
|
-
return 'Network Error!';
|
|
53
109
|
}
|
|
54
110
|
|
|
55
|
-
getSymbolId({ data }) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
}
|
|
111
|
+
// getSymbolId({ data }) {
|
|
112
|
+
// const name = data.type === 'futures' ? data.contracts[0].symbol : data.symbol;
|
|
113
|
+
// return data.exchange.toUpperCase() + ':' + name.toUpperCase();
|
|
114
|
+
// }
|
|
59
115
|
|
|
60
116
|
createSession({ startsWith }) {
|
|
117
|
+
if (typeof startsWith !== 'string' || startsWith.length === 0) {
|
|
118
|
+
throw new Error('Invalid startsWith parameter');
|
|
119
|
+
}
|
|
61
120
|
let session = startsWith;
|
|
62
121
|
while (session.length < 12) session += letters[Math.floor(Math.random() * letters.length)];
|
|
63
122
|
return session;
|