@mathieuc/tradingview 2.0.91 → 3.0.0-pre
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 +5 -97
- package/examples/AllPrivateIndicators.js +35 -0
- package/examples/BuiltInIndicator.js +41 -0
- package/examples/CustomChartType.js +121 -0
- package/examples/CustomTimeframe.js +29 -0
- package/examples/Errors.js +153 -0
- package/examples/GraphicIndicator.js +44 -0
- package/examples/MultipleSyncFetch.js +38 -0
- package/examples/Search.js +15 -0
- package/examples/SimpleChart.js +63 -0
- package/examples/_RenkoIndicator.js +41 -0
- package/main.js +9 -504
- package/package.json +5 -2
- package/src/chart/graphicParser.js +110 -0
- package/src/chart/session.js +367 -0
- package/src/chart/study.js +403 -0
- package/src/classes/BuiltInIndicator.js +119 -0
- package/src/classes/PineIndicator.js +132 -0
- package/src/client.js +280 -0
- package/{miscRequests.js → src/miscRequests.js} +97 -56
- package/src/protocol.js +54 -0
- package/src/quote/market.js +123 -0
- package/src/quote/session.js +124 -0
- package/src/types.js +32 -0
- package/src/utils.js +8 -0
- package/test.js +155 -0
- package/tests/alerts/discordWH.js +76 -76
- package/tests/alerts/envLoader.js +11 -11
- package/tests/alerts/manualAlert.js +77 -77
- package/tests/alerts/sentNotifs.js +16 -16
- package/tests/alerts/strategyAlert.js +94 -94
- package/tests/indicator.js +42 -42
- package/tests/strategy.js +56 -56
package/src/client.js
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
const WebSocket = require('ws');
|
|
2
|
+
|
|
3
|
+
const misc = require('./miscRequests');
|
|
4
|
+
const protocol = require('./protocol');
|
|
5
|
+
|
|
6
|
+
const quoteSessionGenerator = require('./quote/session');
|
|
7
|
+
const chartSessionGenerator = require('./chart/session');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {Object} Session
|
|
11
|
+
* @prop {'quote' | 'chart' | 'replay'} type Session type
|
|
12
|
+
* @prop {(data: {}) => null} onData When there is a data
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** @typedef {Object<string, Session>} SessionList Session list */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {(t: string, p: []) => null} SendPacket Send a custom packet
|
|
19
|
+
* @param {string} t Packet type
|
|
20
|
+
* @param {string[]} p Packet data
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @typedef {Object} ClientBridge
|
|
25
|
+
* @prop {SessionList} sessions
|
|
26
|
+
* @prop {SendPacket} send
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef { 'connected' | 'disconnected'
|
|
31
|
+
* | 'logged' | 'ping' | 'data'
|
|
32
|
+
* | 'error' | 'event'
|
|
33
|
+
* } ClientEvent
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
module.exports = class Client {
|
|
37
|
+
#ws;
|
|
38
|
+
|
|
39
|
+
#logged = false;
|
|
40
|
+
|
|
41
|
+
/** If the client is logged in */
|
|
42
|
+
get isLogged() {
|
|
43
|
+
return this.#logged;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** If the cient was closed */
|
|
47
|
+
get isOpen() {
|
|
48
|
+
return this.#ws.readyState === this.#ws.OPEN;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @type {SessionList} */
|
|
52
|
+
#sessions = {};
|
|
53
|
+
|
|
54
|
+
#callbacks = {
|
|
55
|
+
connected: [],
|
|
56
|
+
disconnected: [],
|
|
57
|
+
logged: [],
|
|
58
|
+
ping: [],
|
|
59
|
+
data: [],
|
|
60
|
+
|
|
61
|
+
error: [],
|
|
62
|
+
event: [],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @param {ClientEvent} ev Client event
|
|
67
|
+
* @param {...{}} data Packet data
|
|
68
|
+
*/
|
|
69
|
+
#handleEvent(ev, ...data) {
|
|
70
|
+
this.#callbacks[ev].forEach((e) => e(...data));
|
|
71
|
+
this.#callbacks.event.forEach((e) => e(ev, ...data));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
#handleError(...msgs) {
|
|
75
|
+
if (this.#callbacks.error.length === 0) console.error(...msgs);
|
|
76
|
+
else this.#handleEvent('error', ...msgs);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* When client is connected
|
|
81
|
+
* @param {() => void} cb Callback
|
|
82
|
+
* @event
|
|
83
|
+
*/
|
|
84
|
+
onConnected(cb) {
|
|
85
|
+
this.#callbacks.connected.push(cb);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* When client is disconnected
|
|
90
|
+
* @param {() => void} cb Callback
|
|
91
|
+
* @event
|
|
92
|
+
*/
|
|
93
|
+
onDisconnected(cb) {
|
|
94
|
+
this.#callbacks.disconnected.push(cb);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @typedef {Object} SocketSession
|
|
99
|
+
* @prop {string} session_id Socket session ID
|
|
100
|
+
* @prop {number} timestamp Session start timestamp
|
|
101
|
+
* @prop {number} timestampMs Session start milliseconds timestamp
|
|
102
|
+
* @prop {string} release Release
|
|
103
|
+
* @prop {string} studies_metadata_hash Studies metadata hash
|
|
104
|
+
* @prop {'json' | string} protocol Used protocol
|
|
105
|
+
* @prop {string} javastudies Javastudies
|
|
106
|
+
* @prop {number} auth_scheme_vsn Auth scheme type
|
|
107
|
+
* @prop {string} via Socket IP
|
|
108
|
+
|
|
109
|
+
* When client is logged
|
|
110
|
+
* @param {(SocketSession: SocketSession) => void} cb Callback
|
|
111
|
+
* @event
|
|
112
|
+
*/
|
|
113
|
+
onLogged(cb) {
|
|
114
|
+
this.#callbacks.logged.push(cb);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* When server is pinging the client
|
|
119
|
+
* @param {(i: number) => void} cb Callback
|
|
120
|
+
* @event
|
|
121
|
+
*/
|
|
122
|
+
onPing(cb) {
|
|
123
|
+
this.#callbacks.ping.push(cb);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* When unparsed data is received
|
|
128
|
+
* @param {(...{}) => void} cb Callback
|
|
129
|
+
* @event
|
|
130
|
+
*/
|
|
131
|
+
onData(cb) {
|
|
132
|
+
this.#callbacks.data.push(cb);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* When a client error happens
|
|
137
|
+
* @param {(...{}) => void} cb Callback
|
|
138
|
+
* @event
|
|
139
|
+
*/
|
|
140
|
+
onError(cb) {
|
|
141
|
+
this.#callbacks.error.push(cb);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* When a client event happens
|
|
146
|
+
* @param {(...{}) => void} cb Callback
|
|
147
|
+
* @event
|
|
148
|
+
*/
|
|
149
|
+
onEvent(cb) {
|
|
150
|
+
this.#callbacks.event.push(cb);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#parsePacket(str) {
|
|
154
|
+
if (!this.isOpen) return;
|
|
155
|
+
|
|
156
|
+
protocol.parseWSPacket(str).forEach((packet) => {
|
|
157
|
+
if (global.TW_DEBUG) console.log('§90§30§107 CLIENT §0 PACKET', packet);
|
|
158
|
+
if (typeof packet === 'number') { // Ping
|
|
159
|
+
this.#ws.send(protocol.formatWSPacket(`~h~${packet}`));
|
|
160
|
+
this.#handleEvent('ping', packet);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (packet.m === 'protocol_error') { // Error
|
|
165
|
+
this.#handleError('Client critical error:', packet.p);
|
|
166
|
+
this.#ws.close();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (packet.m && packet.p) { // Normal packet
|
|
171
|
+
const parsed = {
|
|
172
|
+
type: packet.m,
|
|
173
|
+
data: packet.p,
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const session = packet.p[0];
|
|
177
|
+
|
|
178
|
+
if (session && this.#sessions[session]) {
|
|
179
|
+
this.#sessions[session].onData(parsed);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!this.#logged) {
|
|
185
|
+
this.#handleEvent('logged', packet);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.#handleEvent('data', packet);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#sendQueue = [];
|
|
194
|
+
|
|
195
|
+
/** @type {SendPacket} Send a custom packet */
|
|
196
|
+
send(t, p = []) {
|
|
197
|
+
this.#sendQueue.push(protocol.formatWSPacket({ m: t, p }));
|
|
198
|
+
this.sendQueue();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Send all waiting packets */
|
|
202
|
+
sendQueue() {
|
|
203
|
+
while (this.isOpen && this.#logged && this.#sendQueue.length > 0) {
|
|
204
|
+
const packet = this.#sendQueue.shift();
|
|
205
|
+
this.#ws.send(packet);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @typedef {Object} ClientOptions
|
|
211
|
+
* @prop {string} [token] User auth token (in 'sessionid' cookie)
|
|
212
|
+
* @prop {boolean} [DEBUG] Enable debug mode
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
/** Client object
|
|
216
|
+
* @param {ClientOptions} clientOptions TradingView client options
|
|
217
|
+
*/
|
|
218
|
+
constructor(clientOptions = {}) {
|
|
219
|
+
if (clientOptions.DEBUG) global.TW_DEBUG = clientOptions.DEBUG;
|
|
220
|
+
|
|
221
|
+
this.#ws = new WebSocket('wss://widgetdata.tradingview.com/socket.io/websocket', {
|
|
222
|
+
origin: 'https://s.tradingview.com',
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
if (clientOptions.token) {
|
|
226
|
+
misc.getUser(clientOptions.token).then((user) => {
|
|
227
|
+
this.#sendQueue.unshift(protocol.formatWSPacket({
|
|
228
|
+
m: 'set_auth_token',
|
|
229
|
+
p: [user.authToken],
|
|
230
|
+
}));
|
|
231
|
+
this.#logged = true;
|
|
232
|
+
this.sendQueue();
|
|
233
|
+
}).catch((err) => {
|
|
234
|
+
this.#handleError('Credentials error:', err.message);
|
|
235
|
+
});
|
|
236
|
+
} else {
|
|
237
|
+
this.#sendQueue.unshift(protocol.formatWSPacket({
|
|
238
|
+
m: 'set_auth_token',
|
|
239
|
+
p: ['unauthorized_user_token'],
|
|
240
|
+
}));
|
|
241
|
+
this.#logged = true;
|
|
242
|
+
this.sendQueue();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
this.#ws.on('open', () => {
|
|
246
|
+
this.#handleEvent('connected');
|
|
247
|
+
this.sendQueue();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
this.#ws.on('close', () => {
|
|
251
|
+
this.#logged = false;
|
|
252
|
+
this.#handleEvent('disconnected');
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
this.#ws.on('message', (data) => this.#parsePacket(data));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** @type {ClientBridge} */
|
|
259
|
+
#clientBridge = {
|
|
260
|
+
sessions: this.#sessions,
|
|
261
|
+
send: (t, p) => this.send(t, p),
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
/** @namespace Session */
|
|
265
|
+
Session = {
|
|
266
|
+
Quote: quoteSessionGenerator(this.#clientBridge),
|
|
267
|
+
Chart: chartSessionGenerator(this.#clientBridge),
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Close the websocket connection
|
|
272
|
+
* @return {Promise<void>} When websocket is closed
|
|
273
|
+
*/
|
|
274
|
+
end() {
|
|
275
|
+
return new Promise((cb) => {
|
|
276
|
+
this.#ws.close();
|
|
277
|
+
cb();
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
};
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
const https = require('https');
|
|
2
2
|
|
|
3
|
+
const PineIndicator = require('./classes/PineIndicator');
|
|
4
|
+
|
|
3
5
|
const indicators = ['Recommend.Other', 'Recommend.All', 'Recommend.MA'];
|
|
4
|
-
const
|
|
6
|
+
const builtInIndicList = [];
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* @param {https.RequestOptions} options HTTPS Request options
|
|
@@ -132,7 +134,7 @@ module.exports = {
|
|
|
132
134
|
},
|
|
133
135
|
|
|
134
136
|
/**
|
|
135
|
-
* @typedef {Object}
|
|
137
|
+
* @typedef {Object} SearchMarketResult
|
|
136
138
|
* @property {string} id
|
|
137
139
|
* @property {string} exchange
|
|
138
140
|
* @property {string} fullExchange
|
|
@@ -147,9 +149,9 @@ module.exports = {
|
|
|
147
149
|
* Find a symbol
|
|
148
150
|
* @param {string} search Keywords
|
|
149
151
|
* @param {'stock' | 'futures' | 'forex' | 'cfd' | 'crypto' | 'index' | 'economic'} [filter]
|
|
150
|
-
* @returns {Promise<
|
|
152
|
+
* @returns {Promise<SearchMarketResult[]>} Search results
|
|
151
153
|
*/
|
|
152
|
-
async
|
|
154
|
+
async searchMarket(search, filter = '') {
|
|
153
155
|
const data = await request({
|
|
154
156
|
host: 'symbol-search.tradingview.com',
|
|
155
157
|
path: `/symbol_search/?text=${search.replace(/ /g, '%20')}&type=${filter}`,
|
|
@@ -179,7 +181,7 @@ module.exports = {
|
|
|
179
181
|
},
|
|
180
182
|
|
|
181
183
|
/**
|
|
182
|
-
* @typedef {Object}
|
|
184
|
+
* @typedef {Object} SearchIndicatorResult
|
|
183
185
|
* @property {string} id Script ID
|
|
184
186
|
* @property {string} version Script version
|
|
185
187
|
* @property {string} name Script complete name
|
|
@@ -187,18 +189,20 @@ module.exports = {
|
|
|
187
189
|
* @property {string} image Image ID https://tradingview.com/i/${image}
|
|
188
190
|
* @property {string | ''} source Script source (if available)
|
|
189
191
|
* @property {'study' | 'strategy'} type Script type (study / strategy)
|
|
190
|
-
* @property {'open_source' | 'closed_source' | 'invite_only'
|
|
192
|
+
* @property {'open_source' | 'closed_source' | 'invite_only'
|
|
193
|
+
* | 'private' | 'other'} access Script access type
|
|
194
|
+
* @property {() => Promise<PineIndicator>} get Get the full indicator informations
|
|
191
195
|
*/
|
|
192
196
|
|
|
193
197
|
/**
|
|
194
198
|
* Find an indicator
|
|
195
199
|
* @param {string} search Keywords
|
|
196
|
-
* @returns {Promise<
|
|
200
|
+
* @returns {Promise<SearchIndicatorResult[]>} Search results
|
|
197
201
|
*/
|
|
198
202
|
async searchIndicator(search = '') {
|
|
199
|
-
if (!
|
|
203
|
+
if (!builtInIndicList.length) {
|
|
200
204
|
await Promise.all(['standard', 'candlestick', 'fundamental'].map(async (type) => {
|
|
201
|
-
|
|
205
|
+
builtInIndicList.push(...await request({
|
|
202
206
|
host: 'pine-facade.tradingview.com',
|
|
203
207
|
path: `/pine-facade/list/?filter=${type}`,
|
|
204
208
|
}));
|
|
@@ -215,7 +219,7 @@ module.exports = {
|
|
|
215
219
|
}
|
|
216
220
|
|
|
217
221
|
return [
|
|
218
|
-
...
|
|
222
|
+
...builtInIndicList.filter((i) => (
|
|
219
223
|
norm(i.scriptName).includes(norm(search))
|
|
220
224
|
|| norm(i.extra.shortDescription).includes(norm(search))
|
|
221
225
|
)).map((ind) => ({
|
|
@@ -230,6 +234,9 @@ module.exports = {
|
|
|
230
234
|
access: 'closed_source',
|
|
231
235
|
source: '',
|
|
232
236
|
type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study',
|
|
237
|
+
get() {
|
|
238
|
+
return module.exports.getIndicator(ind.scriptIdPart, ind.version);
|
|
239
|
+
},
|
|
233
240
|
})),
|
|
234
241
|
|
|
235
242
|
...data.results.map((ind) => ({
|
|
@@ -244,42 +251,20 @@ module.exports = {
|
|
|
244
251
|
access: ['open_source', 'closed_source', 'invite_only'][ind.access - 1] || 'other',
|
|
245
252
|
source: ind.scriptSource,
|
|
246
253
|
type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study',
|
|
254
|
+
get() {
|
|
255
|
+
return module.exports.getIndicator(ind.scriptIdPart, ind.version);
|
|
256
|
+
},
|
|
247
257
|
})),
|
|
248
258
|
];
|
|
249
259
|
},
|
|
250
260
|
|
|
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
261
|
/**
|
|
276
262
|
* Get an indicator
|
|
277
263
|
* @param {string} id Indicator ID (Like: PUB;XXXXXXXXXXXXXXXXXXXXX)
|
|
278
|
-
* @param {string} version Wanted version of the indicator
|
|
279
|
-
* @
|
|
280
|
-
* @returns {Promise<Indicator>} Indicator
|
|
264
|
+
* @param {'last' | string} [version] Wanted version of the indicator
|
|
265
|
+
* @returns {Promise<PineIndicator>} Indicator
|
|
281
266
|
*/
|
|
282
|
-
async getIndicator(id, version
|
|
267
|
+
async getIndicator(id, version = 'last') {
|
|
283
268
|
const indicID = id.replace(/ |%/g, '%25');
|
|
284
269
|
|
|
285
270
|
let data = await request({
|
|
@@ -294,26 +279,24 @@ module.exports = {
|
|
|
294
279
|
}
|
|
295
280
|
|
|
296
281
|
if (!data.success || !data.result.metaInfo || !data.result.metaInfo.inputs) {
|
|
297
|
-
|
|
298
|
-
throw new Error('Inexistent or unsupported indicator');
|
|
282
|
+
throw new Error(`Inexistent or unsupported indicator: "${data.reason}"`);
|
|
299
283
|
}
|
|
300
284
|
|
|
301
285
|
const inputs = {};
|
|
302
286
|
|
|
303
287
|
data.result.metaInfo.inputs.forEach((input) => {
|
|
304
288
|
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
289
|
|
|
309
290
|
inputs[input.id] = {
|
|
310
291
|
name: input.name,
|
|
292
|
+
inline: input.inline || input.name.replace(/ /g, '_').replace(/[^a-zA-Z0-9_]/g, ''),
|
|
293
|
+
internalID: input.internalID,
|
|
294
|
+
tooltip: input.tooltip,
|
|
295
|
+
|
|
311
296
|
type: input.type,
|
|
312
|
-
value:
|
|
313
|
-
|
|
314
|
-
hidden: !!input.isHidden,
|
|
297
|
+
value: input.defval,
|
|
298
|
+
isHidden: !!input.isHidden,
|
|
315
299
|
isFake: !!input.isFake,
|
|
316
|
-
inline,
|
|
317
300
|
};
|
|
318
301
|
|
|
319
302
|
if (input.options) inputs[input.id].options = input.options;
|
|
@@ -322,22 +305,37 @@ module.exports = {
|
|
|
322
305
|
const plots = {};
|
|
323
306
|
|
|
324
307
|
Object.keys(data.result.metaInfo.styles).forEach((plotId) => {
|
|
325
|
-
|
|
308
|
+
const plotTitle = data
|
|
309
|
+
.result
|
|
310
|
+
.metaInfo
|
|
311
|
+
.styles[plotId]
|
|
312
|
+
.title
|
|
313
|
+
.replace(/ /g, '_')
|
|
314
|
+
.replace(/[^a-zA-Z0-9_]/g, '');
|
|
315
|
+
|
|
316
|
+
const titles = Object.values(plots);
|
|
317
|
+
|
|
318
|
+
if (titles.includes(plotTitle)) {
|
|
319
|
+
let i = 2;
|
|
320
|
+
while (titles.includes(`${plotTitle}_${i}`)) i += 1;
|
|
321
|
+
plots[plotId] = `${plotTitle}_${i}`;
|
|
322
|
+
} else plots[plotId] = plotTitle;
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
data.result.metaInfo.plots.forEach((plot) => {
|
|
326
|
+
if (!plot.target) return;
|
|
327
|
+
plots[plot.id] = `${plots[plot.target] ?? plot.target}_${plot.type}`;
|
|
326
328
|
});
|
|
327
329
|
|
|
328
|
-
return {
|
|
329
|
-
pineId: indicID,
|
|
330
|
-
pineVersion: version,
|
|
330
|
+
return new PineIndicator({
|
|
331
|
+
pineId: data.result.metaInfo.scriptIdPart || indicID,
|
|
332
|
+
pineVersion: data.result.metaInfo.pine.version || version,
|
|
331
333
|
description: data.result.metaInfo.description,
|
|
332
334
|
shortDescription: data.result.metaInfo.shortDescription,
|
|
333
|
-
typeID: {
|
|
334
|
-
study: 'Script@tv-scripting-101!',
|
|
335
|
-
strategy: 'StrategyScript@tv-scripting-101!',
|
|
336
|
-
}[type] || type,
|
|
337
335
|
inputs,
|
|
338
336
|
plots,
|
|
339
337
|
script: data.result.ilTemplate,
|
|
340
|
-
};
|
|
338
|
+
});
|
|
341
339
|
},
|
|
342
340
|
|
|
343
341
|
/**
|
|
@@ -404,6 +402,49 @@ module.exports = {
|
|
|
404
402
|
});
|
|
405
403
|
},
|
|
406
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Get user's private indicators from a 'sessionid' cookie
|
|
407
|
+
* @param {string} session User 'sessionid' cookie
|
|
408
|
+
* @returns {Promise<SearchIndicatorResult[]>} Search results
|
|
409
|
+
*/
|
|
410
|
+
async getPrivateIndicators(session) {
|
|
411
|
+
return new Promise((cb, err) => {
|
|
412
|
+
https.get('https://pine-facade.tradingview.com/pine-facade/list?filter=saved', {
|
|
413
|
+
headers: { cookie: `sessionid=${session}` },
|
|
414
|
+
}, (res) => {
|
|
415
|
+
let rs = '';
|
|
416
|
+
res.on('data', (d) => { rs += d; });
|
|
417
|
+
res.on('end', async () => {
|
|
418
|
+
try {
|
|
419
|
+
rs = JSON.parse(rs);
|
|
420
|
+
} catch (error) {
|
|
421
|
+
err(new Error('Can\'t parse private indicator list'));
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
cb(rs.map((ind) => ({
|
|
426
|
+
id: ind.scriptIdPart,
|
|
427
|
+
version: ind.version,
|
|
428
|
+
name: ind.scriptName,
|
|
429
|
+
author: {
|
|
430
|
+
id: -1,
|
|
431
|
+
username: '@ME@',
|
|
432
|
+
},
|
|
433
|
+
image: ind.imageUrl,
|
|
434
|
+
access: 'private',
|
|
435
|
+
source: ind.scriptSource,
|
|
436
|
+
type: (ind.extra && ind.extra.kind) ? ind.extra.kind : 'study',
|
|
437
|
+
get() {
|
|
438
|
+
return module.exports.getIndicator(ind.scriptIdPart, ind.version);
|
|
439
|
+
},
|
|
440
|
+
})));
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
res.on('error', err);
|
|
444
|
+
}).end();
|
|
445
|
+
});
|
|
446
|
+
},
|
|
447
|
+
|
|
407
448
|
/**
|
|
408
449
|
* User credentials
|
|
409
450
|
* @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
|
+
};
|