SHOW:
|
|
- or go back to the newest paste.
1 | /* | |
2 | This class implements interaction with UDF-compatible datafeed. | |
3 | ||
4 | See UDF protocol reference at | |
5 | https://github.com/tradingview/charting_library/wiki/UDF | |
6 | */ | |
7 | ||
8 | var Datafeeds = {}; | |
9 | ||
10 | Datafeeds.UDFCompatibleDatafeed = function (datafeedURL, updateFrequency, protocolVersion) { | |
11 | ||
12 | this._datafeedURL = datafeedURL; | |
13 | this._configuration = undefined; | |
14 | ||
15 | this._symbolSearch = null; | |
16 | this._symbolsStorage = null; | |
17 | this._barsPulseUpdater = new Datafeeds.DataPulseUpdater(this, updateFrequency || 10 * 1000); | |
18 | this._quotesPulseUpdater = new Datafeeds.QuotesPulseUpdater(this); | |
19 | this._protocolVersion = protocolVersion || 2; | |
20 | ||
21 | this._enableLogging = false; | |
22 | this._initializationFinished = false; | |
23 | this._callbacks = {}; | |
24 | ||
25 | this._initialize(); | |
26 | }; | |
27 | ||
28 | Datafeeds.UDFCompatibleDatafeed.prototype.defaultConfiguration = function () { | |
29 | return { | |
30 | supports_search: false, | |
31 | supports_group_request: true, | |
32 | supported_resolutions: ['1', '5', '15', '30', '60', '1D', '1W', '1M'], | |
33 | supports_marks: false, | |
34 | supports_timescale_marks: false | |
35 | }; | |
36 | }; | |
37 | ||
38 | Datafeeds.UDFCompatibleDatafeed.prototype.getServerTime = function (callback) { | |
39 | if (this._configuration.supports_time) { | |
40 | this._send(this._datafeedURL + '/time', {}) | |
41 | .done(function (response) { | |
42 | callback(+response.d); | |
43 | }) | |
44 | .fail(function () { | |
45 | ||
46 | }); | |
47 | } | |
48 | }; | |
49 | ||
50 | Datafeeds.UDFCompatibleDatafeed.prototype.on = function (event, callback) { | |
51 | ||
52 | if (!this._callbacks.hasOwnProperty(event)) { | |
53 | this._callbacks[event] = []; | |
54 | } | |
55 | ||
56 | this._callbacks[event].push(callback); | |
57 | return this; | |
58 | }; | |
59 | ||
60 | Datafeeds.UDFCompatibleDatafeed.prototype._fireEvent = function (event, argument) { | |
61 | if (this._callbacks.hasOwnProperty(event)) { | |
62 | var callbacksChain = this._callbacks[event]; | |
63 | for (var i = 0; i < callbacksChain.length; ++i) { | |
64 | callbacksChain[i](argument); | |
65 | } | |
66 | ||
67 | this._callbacks[event] = []; | |
68 | } | |
69 | }; | |
70 | ||
71 | Datafeeds.UDFCompatibleDatafeed.prototype.onInitialized = function () { | |
72 | this._initializationFinished = true; | |
73 | this._fireEvent('initialized'); | |
74 | }; | |
75 | ||
76 | Datafeeds.UDFCompatibleDatafeed.prototype._logMessage = function (message) { | |
77 | if (this._enableLogging) { | |
78 | var now = new Date(); | |
79 | console.log(now.toLocaleTimeString() + '.' + now.getMilliseconds() + '> ' + message); | |
80 | } | |
81 | }; | |
82 | ||
83 | Datafeeds.UDFCompatibleDatafeed.prototype._send = function (url, params) { | |
84 | var request = url; | |
85 | var data = ''; | |
86 | if (params) { | |
87 | for (var i = 0; i < Object.keys(params).length; ++i) { | |
88 | var key = Object.keys(params)[i]; | |
89 | var value = encodeURIComponent(params[key]); | |
90 | //request += (i == 0 ? "?" : "&") + key + "=" + value; | |
91 | data += (i == 0 ? "" : ", ") + key + ":" + "'" + value + "'"; | |
92 | } | |
93 | } | |
94 | ||
95 | this._logMessage("New request: " + request); | |
96 | return $.ajax({ | |
97 | type: "POST", | |
98 | url: url, | |
99 | data: "{" + data + "}", | |
100 | contentType: "application/json; charset=utf-8", | |
101 | dataType: "json", | |
102 | }); | |
103 | } | |
104 | ||
105 | Datafeeds.UDFCompatibleDatafeed.prototype._initialize = function () { | |
106 | ||
107 | var that = this; | |
108 | ||
109 | this._send(this._datafeedURL + '/config') | |
110 | .done(function (response) { | |
111 | var configurationData = response.d; | |
112 | that._setupWithConfiguration(configurationData); | |
113 | }) | |
114 | .fail(function (reason) { | |
115 | that._setupWithConfiguration(that.defaultConfiguration()); | |
116 | }); | |
117 | }; | |
118 | ||
119 | Datafeeds.UDFCompatibleDatafeed.prototype.onReady = function (callback) { | |
120 | var that = this; | |
121 | if (this._configuration) { | |
122 | setTimeout(function () { | |
123 | callback(that._configuration); | |
124 | }, 0); | |
125 | } else { | |
126 | this.on('configuration_ready', function () { | |
127 | callback(that._configuration); | |
128 | }); | |
129 | } | |
130 | }; | |
131 | ||
132 | Datafeeds.UDFCompatibleDatafeed.prototype._setupWithConfiguration = function (configurationData) { | |
133 | this._configuration = configurationData; | |
134 | ||
135 | if (!configurationData.exchanges) { | |
136 | configurationData.exchanges = []; | |
137 | } | |
138 | ||
139 | // @obsolete; remove in 1.5 | |
140 | var supportedResolutions = configurationData.supported_resolutions || configurationData.supportedResolutions; | |
141 | configurationData.supported_resolutions = supportedResolutions; | |
142 | ||
143 | // @obsolete; remove in 1.5 | |
144 | var symbolsTypes = configurationData.symbols_types || configurationData.symbolsTypes; | |
145 | configurationData.symbols_types = symbolsTypes; | |
146 | ||
147 | if (!configurationData.supports_search && !configurationData.supports_group_request) { | |
148 | throw 'Unsupported datafeed configuration. Must either support search, or support group request'; | |
149 | } | |
150 | ||
151 | if (!configurationData.supports_search) { | |
152 | this._symbolSearch = new Datafeeds.SymbolSearchComponent(this); | |
153 | } | |
154 | ||
155 | if (configurationData.supports_group_request) { | |
156 | // this component will call onInitialized() by itself | |
157 | this._symbolsStorage = new Datafeeds.SymbolsStorage(this); | |
158 | } else { | |
159 | this.onInitialized(); | |
160 | } | |
161 | ||
162 | this._fireEvent('configuration_ready'); | |
163 | this._logMessage('Initialized with ' + JSON.stringify(configurationData)); | |
164 | }; | |
165 | ||
166 | // =============================================================================================================================== | |
167 | // The functions set below is the implementation of JavaScript API. | |
168 | ||
169 | Datafeeds.UDFCompatibleDatafeed.prototype.getMarks = function (symbolInfo, rangeStart, rangeEnd, onDataCallback, resolution) { | |
170 | if (this._configuration.supports_marks) { | |
171 | this._send(this._datafeedURL + '/marks', { | |
172 | symbol: symbolInfo.ticker.toUpperCase(), | |
173 | from: rangeStart, | |
174 | to: rangeEnd, | |
175 | resolution: resolution | |
176 | }) | |
177 | .done(function (response) { | |
178 | onDataCallback(JSON.parse(response.d)); | |
179 | }) | |
180 | .fail(function () { | |
181 | onDataCallback([]); | |
182 | }); | |
183 | } | |
184 | }; | |
185 | ||
186 | Datafeeds.UDFCompatibleDatafeed.prototype.getTimescaleMarks = function (symbolInfo, rangeStart, rangeEnd, onDataCallback, resolution) { | |
187 | if (this._configuration.supports_timescale_marks) { | |
188 | this._send(this._datafeedURL + '/timescale_marks', { | |
189 | symbol: symbolInfo.ticker.toUpperCase(), | |
190 | from: rangeStart, | |
191 | to: rangeEnd, | |
192 | resolution: resolution | |
193 | }) | |
194 | .done(function (response) { | |
195 | onDataCallback(JSON.parse(response.d)); | |
196 | }) | |
197 | .fail(function () { | |
198 | onDataCallback([]); | |
199 | }); | |
200 | } | |
201 | }; | |
202 | ||
203 | Datafeeds.UDFCompatibleDatafeed.prototype.searchSymbols = function (searchString, exchange, type, onResultReadyCallback) { | |
204 | var MAX_SEARCH_RESULTS = 30; | |
205 | ||
206 | if (!this._configuration) { | |
207 | onResultReadyCallback([]); | |
208 | return; | |
209 | } | |
210 | ||
211 | if (this._configuration.supports_search) { | |
212 | ||
213 | this._send(this._datafeedURL + '/search', { | |
214 | limit: MAX_SEARCH_RESULTS, | |
215 | query: searchString.toUpperCase(), | |
216 | type: type, | |
217 | exchange: exchange | |
218 | }) | |
219 | .done(function (response) { | |
220 | var data = response.d; | |
221 | ||
222 | for (var i = 0; i < data.length; ++i) { | |
223 | if (!data[i].params) { | |
224 | data[i].params = []; | |
225 | } | |
226 | } | |
227 | ||
228 | if (typeof data.s == 'undefined' || data.s != 'error') { | |
229 | onResultReadyCallback(data); | |
230 | } else { | |
231 | onResultReadyCallback([]); | |
232 | } | |
233 | ||
234 | }) | |
235 | .fail(function (reason) { | |
236 | onResultReadyCallback([]); | |
237 | }); | |
238 | } else { | |
239 | ||
240 | if (!this._symbolSearch) { | |
241 | throw 'Datafeed error: inconsistent configuration (symbol search)'; | |
242 | } | |
243 | ||
244 | var searchArgument = { | |
245 | searchString: searchString, | |
246 | exchange: exchange, | |
247 | type: type, | |
248 | onResultReadyCallback: onResultReadyCallback | |
249 | }; | |
250 | ||
251 | if (this._initializationFinished) { | |
252 | this._symbolSearch.searchSymbols(searchArgument, MAX_SEARCH_RESULTS); | |
253 | } else { | |
254 | ||
255 | var that = this; | |
256 | ||
257 | this.on('initialized', function () { | |
258 | that._symbolSearch.searchSymbols(searchArgument, MAX_SEARCH_RESULTS); | |
259 | }); | |
260 | } | |
261 | } | |
262 | }; | |
263 | ||
264 | Datafeeds.UDFCompatibleDatafeed.prototype._symbolResolveURL = '/symbols'; | |
265 | ||
266 | // BEWARE: this function does not consider symbol's exchange | |
267 | Datafeeds.UDFCompatibleDatafeed.prototype.resolveSymbol = function (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) { | |
268 | ||
269 | var that = this; | |
270 | ||
271 | if (!this._initializationFinished) { | |
272 | this.on('initialized', function () { | |
273 | that.resolveSymbol(symbolName, onSymbolResolvedCallback, onResolveErrorCallback); | |
274 | }); | |
275 | ||
276 | return; | |
277 | } | |
278 | ||
279 | var resolveRequestStartTime = Date.now(); | |
280 | that._logMessage('Resolve requested'); | |
281 | ||
282 | function onResultReady(data) { | |
283 | var postProcessedData = data; | |
284 | if (that.postProcessSymbolInfo) { | |
285 | postProcessedData = that.postProcessSymbolInfo(postProcessedData); | |
286 | } | |
287 | ||
288 | that._logMessage('Symbol resolved: ' + (Date.now() - resolveRequestStartTime)); | |
289 | ||
290 | onSymbolResolvedCallback(postProcessedData); | |
291 | } | |
292 | ||
293 | if (!this._configuration.supports_group_request) { | |
294 | this._send(this._datafeedURL + this._symbolResolveURL, { | |
295 | symbol: symbolName ? symbolName.toUpperCase() : '' | |
296 | }) | |
297 | .done(function (response) { | |
298 | var data = response.d; | |
299 | ||
300 | if (data.s && data.s != 'ok') { | |
301 | onResolveErrorCallback('unknown_symbol'); | |
302 | } else { | |
303 | onResultReady(data); | |
304 | } | |
305 | }) | |
306 | .fail(function (reason) { | |
307 | that._logMessage('Error resolving symbol: ' + JSON.stringify([reason])); | |
308 | onResolveErrorCallback('unknown_symbol'); | |
309 | }); | |
310 | } else { | |
311 | if (this._initializationFinished) { | |
312 | this._symbolsStorage.resolveSymbol(symbolName, onResultReady, onResolveErrorCallback); | |
313 | } else { | |
314 | this.on('initialized', function () { | |
315 | that._symbolsStorage.resolveSymbol(symbolName, onResultReady, onResolveErrorCallback); | |
316 | }); | |
317 | } | |
318 | } | |
319 | }; | |
320 | ||
321 | Datafeeds.UDFCompatibleDatafeed.prototype._historyURL = '/history'; | |
322 | ||
323 | Datafeeds.UDFCompatibleDatafeed.prototype.getBars = function (symbolInfo, resolution, rangeStartDate, rangeEndDate, onDataCallback, onErrorCallback) { | |
324 | ||
325 | // timestamp sample: 1399939200 | |
326 | if (rangeStartDate > 0 && (rangeStartDate + '').length > 10) { | |
327 | throw ['Got a JS time instead of Unix one.', rangeStartDate, rangeEndDate]; | |
328 | } | |
329 | ||
330 | var that = this; | |
331 | ||
332 | var requestStartTime = Date.now(); | |
333 | ||
334 | ||
335 | //Additional Code | |
336 | ||
337 | if (TradingViewResolutionConstant.IsIntraday(resolution)) { | |
338 | try { | |
339 | resolution = globalCurrentStockResolution; | |
340 | } | |
341 | catch (Ex) { | |
342 | ||
343 | } | |
344 | } | |
345 | ||
346 | ||
347 | ||
348 | this._send(this._datafeedURL + this._historyURL, { | |
349 | symbol: symbolInfo.ticker.toUpperCase(), | |
350 | resolution: resolution, | |
351 | from: rangeStartDate, | |
352 | to: rangeEndDate | |
353 | }) | |
354 | .done(function (response) { | |
355 | ||
356 | var data = response.d; | |
357 | ||
358 | var nodata = data.s == 'no_data'; | |
359 | ||
360 | if (data.s != 'ok' && !nodata) { | |
361 | if (!!onErrorCallback) { | |
362 | onErrorCallback(data.s); | |
363 | } | |
364 | ||
365 | return; | |
366 | } | |
367 | ||
368 | var bars = []; | |
369 | ||
370 | // data is JSON having format {s: "status" (ok, no_data, error), | |
371 | // v: [volumes], t: [times], o: [opens], h: [highs], l: [lows], c:[closes], nb: "optional_unixtime_if_no_data"} | |
372 | var barsCount = nodata ? 0 : data.t.length; | |
373 | ||
374 | var volumePresent = typeof data.v != 'undefined'; | |
375 | var ohlPresent = typeof data.o != 'undefined'; | |
376 | ||
377 | for (var i = 0; i < barsCount; ++i) { | |
378 | ||
379 | var barValue = { | |
380 | time: data.t[i] * 1000, | |
381 | close: data.c[i] | |
382 | }; | |
383 | ||
384 | if (ohlPresent) { | |
385 | barValue.open = data.o[i]; | |
386 | barValue.high = data.h[i]; | |
387 | barValue.low = data.l[i]; | |
388 | } else { | |
389 | barValue.open = barValue.high = barValue.low = barValue.close; | |
390 | } | |
391 | ||
392 | if (volumePresent) { | |
393 | barValue.volume = data.v[i]; | |
394 | } | |
395 | ||
396 | bars.push(barValue); | |
397 | } | |
398 | ||
399 | onDataCallback(bars, { version: that._protocolVersion, noData: nodata, nextTime: data.nb || data.nextTime }); | |
400 | }) | |
401 | .fail(function (arg) { | |
402 | console.warn(['getBars(): HTTP error', arg]); | |
403 | ||
404 | if (!!onErrorCallback) { | |
405 | onErrorCallback('network error: ' + JSON.stringify(arg)); | |
406 | } | |
407 | }); | |
408 | }; | |
409 | ||
410 | Datafeeds.UDFCompatibleDatafeed.prototype.subscribeBars = function (symbolInfo, resolution, onRealtimeCallback, listenerGUID) { | |
411 | this._barsPulseUpdater.subscribeDataListener(symbolInfo, resolution, onRealtimeCallback, listenerGUID); | |
412 | }; | |
413 | ||
414 | Datafeeds.UDFCompatibleDatafeed.prototype.unsubscribeBars = function (listenerGUID) { | |
415 | this._barsPulseUpdater.unsubscribeDataListener(listenerGUID); | |
416 | }; | |
417 | ||
418 | Datafeeds.UDFCompatibleDatafeed.prototype.calculateHistoryDepth = function (period, resolutionBack, intervalBack) { | |
419 | }; | |
420 | ||
421 | Datafeeds.UDFCompatibleDatafeed.prototype.getQuotes = function (symbols, onDataCallback, onErrorCallback) { | |
422 | this._send(this._datafeedURL + '/quotes', { symbols: symbols }) | |
423 | .done(function (response) { | |
424 | var data = JSON.parse(response.d); | |
425 | if (data.s == 'ok') { | |
426 | // JSON format is {s: "status", [{s: "symbol_status", n: "symbol_name", v: {"field1": "value1", "field2": "value2", ..., "fieldN": "valueN"}}]} | |
427 | if (onDataCallback) { | |
428 | onDataCallback(data.d); | |
429 | } | |
430 | } else { | |
431 | if (onErrorCallback) { | |
432 | onErrorCallback(data.errmsg); | |
433 | } | |
434 | } | |
435 | }) | |
436 | .fail(function (arg) { | |
437 | if (onErrorCallback) { | |
438 | onErrorCallback('network error: ' + arg); | |
439 | } | |
440 | }); | |
441 | }; | |
442 | ||
443 | Datafeeds.UDFCompatibleDatafeed.prototype.subscribeQuotes = function (symbols, fastSymbols, onRealtimeCallback, listenerGUID) { | |
444 | this._quotesPulseUpdater.subscribeDataListener(symbols, fastSymbols, onRealtimeCallback, listenerGUID); | |
445 | }; | |
446 | ||
447 | Datafeeds.UDFCompatibleDatafeed.prototype.unsubscribeQuotes = function (listenerGUID) { | |
448 | this._quotesPulseUpdater.unsubscribeDataListener(listenerGUID); | |
449 | }; | |
450 | ||
451 | // ================================================================================================================================================== | |
452 | // ================================================================================================================================================== | |
453 | // ================================================================================================================================================== | |
454 | ||
455 | /* | |
456 | It's a symbol storage component for ExternalDatafeed. This component can | |
457 | * interact to UDF-compatible datafeed which supports whole group info requesting | |
458 | * do symbol resolving -- return symbol info by its name | |
459 | */ | |
460 | Datafeeds.SymbolsStorage = function (datafeed) { | |
461 | this._datafeed = datafeed; | |
462 | ||
463 | this._exchangesList = ['NYSE', 'FOREX', 'AMEX']; | |
464 | this._exchangesWaitingForData = {}; | |
465 | this._exchangesDataCache = {}; | |
466 | ||
467 | this._symbolsInfo = {}; | |
468 | this._symbolsList = []; | |
469 | ||
470 | this._requestFullSymbolsList(); | |
471 | }; | |
472 | ||
473 | Datafeeds.SymbolsStorage.prototype._requestFullSymbolsList = function () { | |
474 | ||
475 | var that = this; | |
476 | var datafeed = this._datafeed; | |
477 | ||
478 | for (var i = 0; i < this._exchangesList.length; ++i) { | |
479 | ||
480 | var exchange = this._exchangesList[i]; | |
481 | ||
482 | if (this._exchangesDataCache.hasOwnProperty(exchange)) { | |
483 | continue; | |
484 | } | |
485 | ||
486 | this._exchangesDataCache[exchange] = true; | |
487 | ||
488 | this._exchangesWaitingForData[exchange] = 'waiting_for_data'; | |
489 | ||
490 | //this._datafeed._send(this._datafeed._datafeedURL + '/symbol_info', { | |
491 | // group: exchange | |
492 | //}) | |
493 | // .done(function (exchange) { | |
494 | // return function (response) { | |
495 | // that._onExchangeDataReceived(exchange, JSON.parse(response)); | |
496 | // that._onAnyExchangeResponseReceived(exchange); | |
497 | // }; | |
498 | // }(exchange)) //jshint ignore:line | |
499 | // .fail(function (exchange) { | |
500 | // return function (reason) { | |
501 | // that._onAnyExchangeResponseReceived(exchange); | |
502 | // }; | |
503 | // }(exchange)); //jshint ignore:line | |
504 | } | |
505 | }; | |
506 | ||
507 | Datafeeds.SymbolsStorage.prototype._onExchangeDataReceived = function (exchangeName, data) { | |
508 | ||
509 | function tableField(data, name, index) { | |
510 | return data[name] instanceof Array ? | |
511 | data[name][index] : | |
512 | data[name]; | |
513 | } | |
514 | ||
515 | try { | |
516 | for (var symbolIndex = 0; symbolIndex < data.symbol.length; ++symbolIndex) { | |
517 | ||
518 | var symbolName = data.symbol[symbolIndex]; | |
519 | var listedExchange = tableField(data, 'exchange-listed', symbolIndex); | |
520 | var tradedExchange = tableField(data, 'exchange-traded', symbolIndex); | |
521 | var fullName = tradedExchange + ':' + symbolName; | |
522 | ||
523 | // This feature support is not implemented yet | |
524 | // var hasDWM = tableField(data, "has-dwm", symbolIndex); | |
525 | ||
526 | var hasIntraday = tableField(data, 'has-intraday', symbolIndex); | |
527 | ||
528 | var tickerPresent = typeof data.ticker != 'undefined'; | |
529 | ||
530 | var symbolInfo = { | |
531 | name: symbolName, | |
532 | base_name: [listedExchange + ':' + symbolName], | |
533 | description: tableField(data, 'description', symbolIndex), | |
534 | full_name: fullName, | |
535 | legs: [fullName], | |
536 | has_intraday: hasIntraday, | |
537 | has_no_volume: tableField(data, 'has-no-volume', symbolIndex), | |
538 | listed_exchange: listedExchange, | |
539 | exchange: tradedExchange, | |
540 | minmov: tableField(data, 'minmovement', symbolIndex) || tableField(data, 'minmov', symbolIndex), | |
541 | minmove2: tableField(data, 'minmove2', symbolIndex) || tableField(data, 'minmov2', symbolIndex), | |
542 | fractional: tableField(data, 'fractional', symbolIndex), | |
543 | pointvalue: tableField(data, 'pointvalue', symbolIndex), | |
544 | pricescale: tableField(data, 'pricescale', symbolIndex), | |
545 | type: tableField(data, 'type', symbolIndex), | |
546 | session: tableField(data, 'session-regular', symbolIndex), | |
547 | ticker: tickerPresent ? tableField(data, 'ticker', symbolIndex) : symbolName, | |
548 | timezone: tableField(data, 'timezone', symbolIndex), | |
549 | supported_resolutions: tableField(data, 'supported-resolutions', symbolIndex) || this._datafeed.defaultConfiguration().supported_resolutions, | |
550 | force_session_rebuild: tableField(data, 'force-session-rebuild', symbolIndex) || false, | |
551 | has_daily: tableField(data, 'has-daily', symbolIndex) || true, | |
552 | intraday_multipliers: tableField(data, 'intraday-multipliers', symbolIndex) || ['1', '5', '15', '30', '60'], | |
553 | has_fractional_volume: tableField(data, 'has-fractional-volume', symbolIndex) || false, | |
554 | has_weekly_and_monthly: tableField(data, 'has-weekly-and-monthly', symbolIndex) || false, | |
555 | has_empty_bars: tableField(data, 'has-empty-bars', symbolIndex) || false, | |
556 | volume_precision: tableField(data, 'volume-precision', symbolIndex) || 0 | |
557 | }; | |
558 | ||
559 | this._symbolsInfo[symbolInfo.ticker] = this._symbolsInfo[symbolName] = this._symbolsInfo[fullName] = symbolInfo; | |
560 | this._symbolsList.push(symbolName); | |
561 | } | |
562 | } | |
563 | catch (error) { | |
564 | throw 'API error when processing exchange `' + exchangeName + '` symbol #' + symbolIndex + ': ' + error; | |
565 | } | |
566 | }; | |
567 | ||
568 | Datafeeds.SymbolsStorage.prototype._onAnyExchangeResponseReceived = function (exchangeName) { | |
569 | ||
570 | delete this._exchangesWaitingForData[exchangeName]; | |
571 | ||
572 | var allDataReady = Object.keys(this._exchangesWaitingForData).length === 0; | |
573 | ||
574 | if (allDataReady) { | |
575 | this._symbolsList.sort(); | |
576 | this._datafeed._logMessage('All exchanges data ready'); | |
577 | this._datafeed.onInitialized(); | |
578 | } | |
579 | }; | |
580 | ||
581 | // BEWARE: this function does not consider symbol's exchange | |
582 | Datafeeds.SymbolsStorage.prototype.resolveSymbol = function (symbolName, onSymbolResolvedCallback, onResolveErrorCallback) { | |
583 | var that = this; | |
584 | ||
585 | setTimeout(function () { | |
586 | if (!that._symbolsInfo.hasOwnProperty(symbolName)) { | |
587 | onResolveErrorCallback('invalid symbol'); | |
588 | } else { | |
589 | onSymbolResolvedCallback(that._symbolsInfo[symbolName]); | |
590 | } | |
591 | }, 0); | |
592 | }; | |
593 | ||
594 | // ================================================================================================================================================== | |
595 | // ================================================================================================================================================== | |
596 | // ================================================================================================================================================== | |
597 | ||
598 | /* | |
599 | It's a symbol search component for ExternalDatafeed. This component can do symbol search only. | |
600 | This component strongly depends on SymbolsDataStorage and cannot work without it. Maybe, it would be | |
601 | better to merge it to SymbolsDataStorage. | |
602 | */ | |
603 | ||
604 | Datafeeds.SymbolSearchComponent = function (datafeed) { | |
605 | this._datafeed = datafeed; | |
606 | }; | |
607 | ||
608 | // searchArgument = { searchString, onResultReadyCallback} | |
609 | Datafeeds.SymbolSearchComponent.prototype.searchSymbols = function (searchArgument, maxSearchResults) { | |
610 | ||
611 | if (!this._datafeed._symbolsStorage) { | |
612 | throw 'Cannot use local symbol search when no groups information is available'; | |
613 | } | |
614 | ||
615 | var symbolsStorage = this._datafeed._symbolsStorage; | |
616 | ||
617 | var results = []; // array of WeightedItem { item, weight } | |
618 | var queryIsEmpty = !searchArgument.searchString || searchArgument.searchString.length === 0; | |
619 | var searchStringUpperCase = searchArgument.searchString.toUpperCase(); | |
620 | ||
621 | for (var i = 0; i < symbolsStorage._symbolsList.length; ++i) { | |
622 | var symbolName = symbolsStorage._symbolsList[i]; | |
623 | var item = symbolsStorage._symbolsInfo[symbolName]; | |
624 | ||
625 | if (searchArgument.type && searchArgument.type.length > 0 && item.type != searchArgument.type) { | |
626 | continue; | |
627 | } | |
628 | ||
629 | if (searchArgument.exchange && searchArgument.exchange.length > 0 && item.exchange != searchArgument.exchange) { | |
630 | continue; | |
631 | } | |
632 | ||
633 | var positionInName = item.name.toUpperCase().indexOf(searchStringUpperCase); | |
634 | var positionInDescription = item.description.toUpperCase().indexOf(searchStringUpperCase); | |
635 | ||
636 | if (queryIsEmpty || positionInName >= 0 || positionInDescription >= 0) { | |
637 | var found = false; | |
638 | for (var resultIndex = 0; resultIndex < results.length; resultIndex++) { | |
639 | if (results[resultIndex].item == item) { | |
640 | found = true; | |
641 | break; | |
642 | } | |
643 | } | |
644 | ||
645 | if (!found) { | |
646 | var weight = positionInName >= 0 ? positionInName : 8000 + positionInDescription; | |
647 | results.push({ item: item, weight: weight }); | |
648 | } | |
649 | } | |
650 | } | |
651 | ||
652 | searchArgument.onResultReadyCallback( | |
653 | results | |
654 | .sort(function (weightedItem1, weightedItem2) { | |
655 | return weightedItem1.weight - weightedItem2.weight; | |
656 | }) | |
657 | .map(function (weightedItem) { | |
658 | var item = weightedItem.item; | |
659 | return { | |
660 | symbol: item.name, | |
661 | full_name: item.full_name, | |
662 | description: item.description, | |
663 | exchange: item.exchange, | |
664 | params: [], | |
665 | type: item.type, | |
666 | ticker: item.name | |
667 | }; | |
668 | }) | |
669 | .slice(0, Math.min(results.length, maxSearchResults)) | |
670 | ); | |
671 | }; | |
672 | ||
673 | // ================================================================================================================================================== | |
674 | // ================================================================================================================================================== | |
675 | // ================================================================================================================================================== | |
676 | ||
677 | /* | |
678 | This is a pulse updating components for ExternalDatafeed. They emulates realtime updates with periodic requests. | |
679 | */ | |
680 | ||
681 | Datafeeds.DataPulseUpdater = function (datafeed, updateFrequency) { | |
682 | this._datafeed = datafeed; | |
683 | this._subscribers = {}; | |
684 | ||
685 | this._requestsPending = 0; | |
686 | var that = this; | |
687 | ||
688 | var update = function () { | |
689 | ||
690 | try { | |
691 | var iFrameChart = $("#tv_chart_container").children("iframe").contents().find("body")[0]; | |
692 | var marketStateSpanTag = $(iFrameChart).find("span")[64]; | |
693 | var marketState = marketStateSpanTag.innerText; | |
694 | } | |
695 | catch (ex) { | |
696 | ||
697 | } | |
698 | ||
699 | if (marketState == 'closed') { | |
700 | return; | |
701 | } | |
702 | ||
703 | if (that._requestsPending > 0) { | |
704 | return; | |
705 | } | |
706 | ||
707 | for (var listenerGUID in that._subscribers) { | |
708 | var subscriptionRecord = that._subscribers[listenerGUID]; | |
709 | var resolution = subscriptionRecord.resolution; | |
710 | ||
711 | var datesRangeRight = parseInt((new Date().valueOf()) / 1000); | |
712 | ||
713 | // BEWARE: please note we really need 2 bars, not the only last one | |
714 | // see the explanation below. `10` is the `large enough` value to work around holidays | |
715 | var datesRangeLeft = datesRangeRight - that.periodLengthSeconds(resolution, 10); | |
716 | ||
717 | that._requestsPending++; | |
718 | ||
719 | (function (_subscriptionRecord) { | |
720 | ||
721 | that._datafeed.getBars(_subscriptionRecord.symbolInfo, resolution, datesRangeLeft, datesRangeRight, function (bars) { | |
722 | that._requestsPending--; | |
723 | ||
724 | // means the subscription was cancelled while waiting for data | |
725 | if (!that._subscribers.hasOwnProperty(listenerGUID)) { | |
726 | return; | |
727 | } | |
728 | ||
729 | if (bars.length === 0) { | |
730 | return; | |
731 | } | |
732 | ||
733 | var lastBar = bars[bars.length - 1]; | |
734 | if (!isNaN(_subscriptionRecord.lastBarTime) && lastBar.time < _subscriptionRecord.lastBarTime) { | |
735 | return; | |
736 | } | |
737 | ||
738 | var subscribers = _subscriptionRecord.listeners; | |
739 | ||
740 | // BEWARE: this one isn't working when first update comes and this update makes a new bar. In this case | |
741 | // _subscriptionRecord.lastBarTime = NaN | |
742 | var isNewBar = !isNaN(_subscriptionRecord.lastBarTime) && lastBar.time > _subscriptionRecord.lastBarTime; | |
743 | ||
744 | // Pulse updating may miss some trades data (ie, if pulse period = 10 secods and new bar is started 5 seconds later after the last update, the | |
745 | // old bar's last 5 seconds trades will be lost). Thus, at fist we should broadcast old bar updates when it's ready. | |
746 | if (isNewBar) { | |
747 | ||
748 | if (bars.length < 2) { | |
749 | throw 'Not enough bars in history for proper pulse update. Need at least 2.'; | |
750 | } | |
751 | ||
752 | var previousBar = bars[bars.length - 2]; | |
753 | for (var i = 0; i < subscribers.length; ++i) { | |
754 | subscribers[i](previousBar); | |
755 | } | |
756 | } | |
757 | ||
758 | _subscriptionRecord.lastBarTime = lastBar.time; | |
759 | ||
760 | for (var i = 0; i < subscribers.length; ++i) { | |
761 | subscribers[i](lastBar); | |
762 | } | |
763 | }, | |
764 | ||
765 | // on error | |
766 | function () { | |
767 | that._requestsPending--; | |
768 | }); | |
769 | })(subscriptionRecord); //jshint ignore:line | |
770 | ||
771 | } | |
772 | }; | |
773 | ||
774 | ||
775 | var realTimeUpdate = function (latestData, timeframe) { | |
776 | ||
777 | //console.log('Chart Data: ' + latestData.StockInfo.StockCode); | |
778 | ||
779 | for (var listenerGUID in that._subscribers) { | |
780 | var subscriptionRecord = that._subscribers[listenerGUID]; | |
781 | var resolution = subscriptionRecord.resolution; | |
782 | ||
783 | var datesRangeRight = parseInt((new Date().valueOf()) / 1000); | |
784 | ||
785 | // BEWARE: please note we really need 2 bars, not the only last one | |
786 | // see the explanation below. `10` is the `large enough` value to work around holidays | |
787 | var datesRangeLeft = datesRangeRight - that.periodLengthSeconds(resolution, 10); | |
788 | ||
789 | if (latestData.StockInfo.StockCode.toUpperCase() == | |
790 | subscriptionRecord.symbolInfo.ticker.toUpperCase()) { | |
791 | ||
792 | if (TradingViewResolutionConstant.IsIntraday(resolution) || | |
793 | resolution == timeframe) { | |
794 | ||
795 | var barValue = { | |
796 | time: latestData.StockHistory.DateUnixEpochFormat * 1000, | |
797 | close: latestData.StockHistory.Last, | |
798 | open: latestData.StockHistory.Open, | |
799 | low: latestData.StockHistory.Low, | |
800 | high: latestData.StockHistory.High | |
801 | }; | |
802 | ||
803 | if (latestData.StockInfo.StockType == 2) { | |
804 | barValue.volume = latestData.StockHistory.Value; | |
805 | } | |
806 | else { | |
807 | barValue.volume = latestData.StockHistory.Volume; | |
808 | } | |
809 | ||
810 | ||
811 | var subscribers = subscriptionRecord.listeners; | |
812 | ||
813 | subscriptionRecord.lastBarTime = barValue.time; | |
814 | ||
815 | for (var i = 0; i < subscribers.length; ++i) { | |
816 | subscribers[i](barValue); | |
817 | } | |
818 | ||
819 | } | |
820 | } | |
821 | ||
822 | ||
823 | ||
824 | } | |
825 | }; | |
826 | ||
827 | ||
828 | ||
829 | try { | |
830 | stockChartHub.on('sendRealTimeIndividualStockPrice', function (data, timeframe) { | |
831 | //console.log(data); | |
832 | realTimeUpdate(data, timeframe); | |
833 | }); | |
834 | ||
835 | //stockChartHub.client.sendRealTimeIndividualStockPrice = function (data, timeframe) { | |
836 | // realTimeUpdate(data, timeframe); | |
837 | //}; | |
838 | } | |
839 | catch (ex) { | |
840 | ||
841 | } | |
842 | ||
843 | ||
844 | ||
845 | ||
846 | ||
847 | if (typeof updateFrequency != 'undefined' && updateFrequency > 0) { | |
848 | //setInterval(update, updateFrequency); | |
849 | } | |
850 | }; | |
851 | ||
852 | Datafeeds.DataPulseUpdater.prototype.unsubscribeDataListener = function (listenerGUID) { | |
853 | this._datafeed._logMessage('Unsubscribing ' + listenerGUID); | |
854 | delete this._subscribers[listenerGUID]; | |
855 | }; | |
856 | ||
857 | Datafeeds.DataPulseUpdater.prototype.subscribeDataListener = function (symbolInfo, resolution, newDataCallback, listenerGUID) { | |
858 | ||
859 | this._datafeed._logMessage('Subscribing ' + listenerGUID); | |
860 | ||
861 | var key = symbolInfo.name + ', ' + resolution; | |
862 | ||
863 | if (!this._subscribers.hasOwnProperty(listenerGUID)) { | |
864 | ||
865 | this._subscribers[listenerGUID] = { | |
866 | symbolInfo: symbolInfo, | |
867 | resolution: resolution, | |
868 | lastBarTime: NaN, | |
869 | listeners: [] | |
870 | }; | |
871 | } | |
872 | ||
873 | this._subscribers[listenerGUID].listeners.push(newDataCallback); | |
874 | }; | |
875 | ||
876 | Datafeeds.DataPulseUpdater.prototype.periodLengthSeconds = function (resolution, requiredPeriodsCount) { | |
877 | var daysCount = 0; | |
878 | ||
879 | if (resolution == 'D') { | |
880 | daysCount = requiredPeriodsCount; | |
881 | } else if (resolution == 'M') { | |
882 | daysCount = 31 * requiredPeriodsCount; | |
883 | } else if (resolution == 'W') { | |
884 | daysCount = 7 * requiredPeriodsCount; | |
885 | } else { | |
886 | daysCount = requiredPeriodsCount * resolution / (24 * 60); | |
887 | } | |
888 | ||
889 | return daysCount * 24 * 60 * 60; | |
890 | }; | |
891 | ||
892 | Datafeeds.QuotesPulseUpdater = function (datafeed) { | |
893 | this._datafeed = datafeed; | |
894 | this._subscribers = {}; | |
895 | this._updateInterval = 60 * 1000; | |
896 | this._fastUpdateInterval = 10 * 1000; | |
897 | this._requestsPending = 0; | |
898 | ||
899 | var that = this; | |
900 | ||
901 | setInterval(function () { | |
902 | that._updateQuotes(function (subscriptionRecord) { return subscriptionRecord.symbols; }); | |
903 | }, this._updateInterval); | |
904 | ||
905 | setInterval(function () { | |
906 | that._updateQuotes(function (subscriptionRecord) { return subscriptionRecord.fastSymbols.length > 0 ? subscriptionRecord.fastSymbols : subscriptionRecord.symbols; }); | |
907 | }, this._fastUpdateInterval); | |
908 | }; | |
909 | ||
910 | Datafeeds.QuotesPulseUpdater.prototype.subscribeDataListener = function (symbols, fastSymbols, newDataCallback, listenerGUID) { | |
911 | if (!this._subscribers.hasOwnProperty(listenerGUID)) { | |
912 | this._subscribers[listenerGUID] = { | |
913 | symbols: symbols, | |
914 | fastSymbols: fastSymbols, | |
915 | listeners: [] | |
916 | }; | |
917 | } | |
918 | ||
919 | this._subscribers[listenerGUID].listeners.push(newDataCallback); | |
920 | }; | |
921 | ||
922 | Datafeeds.QuotesPulseUpdater.prototype.unsubscribeDataListener = function (listenerGUID) { | |
923 | delete this._subscribers[listenerGUID]; | |
924 | }; | |
925 | ||
926 | Datafeeds.QuotesPulseUpdater.prototype._updateQuotes = function (symbolsGetter) { | |
927 | if (this._requestsPending > 0) { | |
928 | return; | |
929 | } | |
930 | ||
931 | var that = this; | |
932 | for (var listenerGUID in this._subscribers) { | |
933 | this._requestsPending++; | |
934 | ||
935 | var subscriptionRecord = this._subscribers[listenerGUID]; | |
936 | this._datafeed.getQuotes(symbolsGetter(subscriptionRecord), | |
937 | ||
938 | // onDataCallback | |
939 | (function (subscribers, guid) { | |
940 | return function (data) { | |
941 | that._requestsPending--; | |
942 | ||
943 | // means the subscription was cancelled while waiting for data | |
944 | if (!that._subscribers.hasOwnProperty(guid)) { | |
945 | return; | |
946 | } | |
947 | ||
948 | for (var i = 0; i < subscribers.length; ++i) { | |
949 | subscribers[i](data); | |
950 | } | |
951 | }; | |
952 | }(subscriptionRecord.listeners, listenerGUID)), //jshint ignore:line | |
953 | // onErrorCallback | |
954 | function (error) { | |
955 | that._requestsPending--; | |
956 | }); //jshint ignore:line | |
957 | } | |
958 | }; |