binbot-charts 0.0.20 → 0.0.21
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/README.md +2 -0
- package/dist/components/TVChartContainer.js +45 -24
- package/dist/components/datafeed.js +2 -13
- package/dist/{index.js → components/index.js} +1 -1
- package/dist/datafeeds/README.md +3 -0
- package/dist/datafeeds/udf/README.md +46 -0
- package/dist/datafeeds/udf/dist/bundle.js +1 -0
- package/dist/datafeeds/udf/lib/data-pulse-provider.js +104 -0
- package/dist/datafeeds/udf/lib/helpers.js +20 -0
- package/dist/datafeeds/udf/lib/history-provider.js +73 -0
- package/dist/datafeeds/udf/lib/iquotes-provider.js +1 -0
- package/dist/datafeeds/udf/lib/quotes-provider.js +25 -0
- package/dist/datafeeds/udf/lib/quotes-pulse-provider.js +44 -0
- package/dist/datafeeds/udf/lib/requester.js +28 -0
- package/dist/datafeeds/udf/lib/symbols-storage.js +180 -0
- package/dist/datafeeds/udf/lib/udf-compatible-datafeed-base.js +252 -0
- package/dist/datafeeds/udf/lib/udf-compatible-datafeed.js +10 -0
- package/dist/datafeeds/udf/package.json +17 -0
- package/dist/datafeeds/udf/rollup.config.js +25 -0
- package/dist/datafeeds/udf/src/data-pulse-provider.ts +152 -0
- package/dist/datafeeds/udf/src/helpers.ts +38 -0
- package/dist/datafeeds/udf/src/history-provider.ts +134 -0
- package/dist/datafeeds/udf/src/iquotes-provider.ts +14 -0
- package/dist/datafeeds/udf/src/quotes-provider.ts +37 -0
- package/dist/datafeeds/udf/src/quotes-pulse-provider.ts +85 -0
- package/dist/datafeeds/udf/src/requester.ts +39 -0
- package/dist/datafeeds/udf/src/symbols-storage.ts +298 -0
- package/dist/datafeeds/udf/src/udf-compatible-datafeed-base.ts +369 -0
- package/dist/datafeeds/udf/src/udf-compatible-datafeed.ts +11 -0
- package/dist/datafeeds/udf/tsconfig.json +25 -0
- package/package.json +9 -6
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { getErrorMessage, logMessage, } from './helpers';
|
|
2
|
+
function extractField(data, field, arrayIndex, valueIsArray) {
|
|
3
|
+
const value = data[field];
|
|
4
|
+
if (Array.isArray(value) && (!valueIsArray || Array.isArray(value[0]))) {
|
|
5
|
+
return value[arrayIndex];
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function symbolKey(symbol, currency, unit) {
|
|
10
|
+
// here we're using a separator that quite possible shouldn't be in a real symbol name
|
|
11
|
+
return symbol + (currency !== undefined ? '_%|#|%_' + currency : '') + (unit !== undefined ? '_%|#|%_' + unit : '');
|
|
12
|
+
}
|
|
13
|
+
export class SymbolsStorage {
|
|
14
|
+
constructor(datafeedUrl, datafeedSupportedResolutions, requester) {
|
|
15
|
+
this._exchangesList = ['NYSE', 'FOREX', 'AMEX'];
|
|
16
|
+
this._symbolsInfo = {};
|
|
17
|
+
this._symbolsList = [];
|
|
18
|
+
this._datafeedUrl = datafeedUrl;
|
|
19
|
+
this._datafeedSupportedResolutions = datafeedSupportedResolutions;
|
|
20
|
+
this._requester = requester;
|
|
21
|
+
this._readyPromise = this._init();
|
|
22
|
+
this._readyPromise.catch((error) => {
|
|
23
|
+
// seems it is impossible
|
|
24
|
+
// tslint:disable-next-line:no-console
|
|
25
|
+
console.error(`SymbolsStorage: Cannot init, error=${error.toString()}`);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// BEWARE: this function does not consider symbol's exchange
|
|
29
|
+
resolveSymbol(symbolName, currencyCode, unitId) {
|
|
30
|
+
return this._readyPromise.then(() => {
|
|
31
|
+
const symbolInfo = this._symbolsInfo[symbolKey(symbolName, currencyCode, unitId)];
|
|
32
|
+
if (symbolInfo === undefined) {
|
|
33
|
+
return Promise.reject('invalid symbol');
|
|
34
|
+
}
|
|
35
|
+
return Promise.resolve(symbolInfo);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
searchSymbols(searchString, exchange, symbolType, maxSearchResults) {
|
|
39
|
+
return this._readyPromise.then(() => {
|
|
40
|
+
const weightedResult = [];
|
|
41
|
+
const queryIsEmpty = searchString.length === 0;
|
|
42
|
+
searchString = searchString.toUpperCase();
|
|
43
|
+
for (const symbolName of this._symbolsList) {
|
|
44
|
+
const symbolInfo = this._symbolsInfo[symbolName];
|
|
45
|
+
if (symbolInfo === undefined) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (symbolType.length > 0 && symbolInfo.type !== symbolType) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (exchange && exchange.length > 0 && symbolInfo.exchange !== exchange) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const positionInName = symbolInfo.name.toUpperCase().indexOf(searchString);
|
|
55
|
+
const positionInDescription = symbolInfo.description.toUpperCase().indexOf(searchString);
|
|
56
|
+
if (queryIsEmpty || positionInName >= 0 || positionInDescription >= 0) {
|
|
57
|
+
const alreadyExists = weightedResult.some((item) => item.symbolInfo === symbolInfo);
|
|
58
|
+
if (!alreadyExists) {
|
|
59
|
+
const weight = positionInName >= 0 ? positionInName : 8000 + positionInDescription;
|
|
60
|
+
weightedResult.push({ symbolInfo: symbolInfo, weight: weight });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const result = weightedResult
|
|
65
|
+
.sort((item1, item2) => item1.weight - item2.weight)
|
|
66
|
+
.slice(0, maxSearchResults)
|
|
67
|
+
.map((item) => {
|
|
68
|
+
const symbolInfo = item.symbolInfo;
|
|
69
|
+
return {
|
|
70
|
+
symbol: symbolInfo.name,
|
|
71
|
+
full_name: symbolInfo.full_name,
|
|
72
|
+
description: symbolInfo.description,
|
|
73
|
+
exchange: symbolInfo.exchange,
|
|
74
|
+
params: [],
|
|
75
|
+
type: symbolInfo.type,
|
|
76
|
+
ticker: symbolInfo.name,
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
return Promise.resolve(result);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
_init() {
|
|
83
|
+
const promises = [];
|
|
84
|
+
const alreadyRequestedExchanges = {};
|
|
85
|
+
for (const exchange of this._exchangesList) {
|
|
86
|
+
if (alreadyRequestedExchanges[exchange]) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
alreadyRequestedExchanges[exchange] = true;
|
|
90
|
+
promises.push(this._requestExchangeData(exchange));
|
|
91
|
+
}
|
|
92
|
+
return Promise.all(promises)
|
|
93
|
+
.then(() => {
|
|
94
|
+
this._symbolsList.sort();
|
|
95
|
+
logMessage('SymbolsStorage: All exchanges data loaded');
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
_requestExchangeData(exchange) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
this._requester.sendRequest(this._datafeedUrl, 'symbol_info', { group: exchange })
|
|
101
|
+
.then((response) => {
|
|
102
|
+
try {
|
|
103
|
+
this._onExchangeDataReceived(exchange, response);
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
reject(error);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
resolve();
|
|
110
|
+
})
|
|
111
|
+
.catch((reason) => {
|
|
112
|
+
logMessage(`SymbolsStorage: Request data for exchange '${exchange}' failed, reason=${getErrorMessage(reason)}`);
|
|
113
|
+
resolve();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
_onExchangeDataReceived(exchange, data) {
|
|
118
|
+
let symbolIndex = 0;
|
|
119
|
+
try {
|
|
120
|
+
const symbolsCount = data.symbol.length;
|
|
121
|
+
const tickerPresent = data.ticker !== undefined;
|
|
122
|
+
for (; symbolIndex < symbolsCount; ++symbolIndex) {
|
|
123
|
+
const symbolName = data.symbol[symbolIndex];
|
|
124
|
+
const listedExchange = extractField(data, 'exchange-listed', symbolIndex);
|
|
125
|
+
const tradedExchange = extractField(data, 'exchange-traded', symbolIndex);
|
|
126
|
+
const fullName = tradedExchange + ':' + symbolName;
|
|
127
|
+
const currencyCode = extractField(data, 'currency-code', symbolIndex);
|
|
128
|
+
const unitId = extractField(data, 'unit-id', symbolIndex);
|
|
129
|
+
const ticker = tickerPresent ? extractField(data, 'ticker', symbolIndex) : symbolName;
|
|
130
|
+
const symbolInfo = {
|
|
131
|
+
ticker: ticker,
|
|
132
|
+
name: symbolName,
|
|
133
|
+
base_name: [listedExchange + ':' + symbolName],
|
|
134
|
+
full_name: fullName,
|
|
135
|
+
listed_exchange: listedExchange,
|
|
136
|
+
exchange: tradedExchange,
|
|
137
|
+
currency_code: currencyCode,
|
|
138
|
+
original_currency_code: extractField(data, 'original-currency-code', symbolIndex),
|
|
139
|
+
unit_id: unitId,
|
|
140
|
+
original_unit_id: extractField(data, 'original-unit-id', symbolIndex),
|
|
141
|
+
unit_conversion_types: extractField(data, 'unit-conversion-types', symbolIndex, true),
|
|
142
|
+
description: extractField(data, 'description', symbolIndex),
|
|
143
|
+
has_intraday: definedValueOrDefault(extractField(data, 'has-intraday', symbolIndex), false),
|
|
144
|
+
has_no_volume: definedValueOrDefault(extractField(data, 'has-no-volume', symbolIndex), false),
|
|
145
|
+
minmov: extractField(data, 'minmovement', symbolIndex) || extractField(data, 'minmov', symbolIndex) || 0,
|
|
146
|
+
minmove2: extractField(data, 'minmove2', symbolIndex) || extractField(data, 'minmov2', symbolIndex),
|
|
147
|
+
fractional: extractField(data, 'fractional', symbolIndex),
|
|
148
|
+
pricescale: extractField(data, 'pricescale', symbolIndex),
|
|
149
|
+
type: extractField(data, 'type', symbolIndex),
|
|
150
|
+
session: extractField(data, 'session-regular', symbolIndex),
|
|
151
|
+
session_holidays: extractField(data, 'session-holidays', symbolIndex),
|
|
152
|
+
corrections: extractField(data, 'corrections', symbolIndex),
|
|
153
|
+
timezone: extractField(data, 'timezone', symbolIndex),
|
|
154
|
+
supported_resolutions: definedValueOrDefault(extractField(data, 'supported-resolutions', symbolIndex, true), this._datafeedSupportedResolutions),
|
|
155
|
+
has_daily: definedValueOrDefault(extractField(data, 'has-daily', symbolIndex), true),
|
|
156
|
+
intraday_multipliers: definedValueOrDefault(extractField(data, 'intraday-multipliers', symbolIndex, true), ['1', '5', '15', '30', '60']),
|
|
157
|
+
has_weekly_and_monthly: extractField(data, 'has-weekly-and-monthly', symbolIndex),
|
|
158
|
+
has_empty_bars: extractField(data, 'has-empty-bars', symbolIndex),
|
|
159
|
+
volume_precision: definedValueOrDefault(extractField(data, 'volume-precision', symbolIndex), 0),
|
|
160
|
+
format: 'price',
|
|
161
|
+
};
|
|
162
|
+
this._symbolsInfo[ticker] = symbolInfo;
|
|
163
|
+
this._symbolsInfo[symbolName] = symbolInfo;
|
|
164
|
+
this._symbolsInfo[fullName] = symbolInfo;
|
|
165
|
+
if (currencyCode !== undefined || unitId !== undefined) {
|
|
166
|
+
this._symbolsInfo[symbolKey(ticker, currencyCode, unitId)] = symbolInfo;
|
|
167
|
+
this._symbolsInfo[symbolKey(symbolName, currencyCode, unitId)] = symbolInfo;
|
|
168
|
+
this._symbolsInfo[symbolKey(fullName, currencyCode, unitId)] = symbolInfo;
|
|
169
|
+
}
|
|
170
|
+
this._symbolsList.push(symbolName);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
throw new Error(`SymbolsStorage: API error when processing exchange ${exchange} symbol #${symbolIndex} (${data.symbol[symbolIndex]}): ${error.message}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function definedValueOrDefault(value, defaultValue) {
|
|
179
|
+
return value !== undefined ? value : defaultValue;
|
|
180
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { getErrorMessage, logMessage, } from './helpers';
|
|
2
|
+
import { HistoryProvider, } from './history-provider';
|
|
3
|
+
import { DataPulseProvider } from './data-pulse-provider';
|
|
4
|
+
import { QuotesPulseProvider } from './quotes-pulse-provider';
|
|
5
|
+
import { SymbolsStorage } from './symbols-storage';
|
|
6
|
+
function extractField(data, field, arrayIndex) {
|
|
7
|
+
const value = data[field];
|
|
8
|
+
return Array.isArray(value) ? value[arrayIndex] : value;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* This class implements interaction with UDF-compatible datafeed.
|
|
12
|
+
* See UDF protocol reference at https://github.com/tradingview/charting_library/wiki/UDF
|
|
13
|
+
*/
|
|
14
|
+
export class UDFCompatibleDatafeedBase {
|
|
15
|
+
constructor(datafeedURL, quotesProvider, requester, updateFrequency = 10 * 1000) {
|
|
16
|
+
this._configuration = defaultConfiguration();
|
|
17
|
+
this._symbolsStorage = null;
|
|
18
|
+
this._datafeedURL = datafeedURL;
|
|
19
|
+
this._requester = requester;
|
|
20
|
+
this._historyProvider = new HistoryProvider(datafeedURL, this._requester);
|
|
21
|
+
this._quotesProvider = quotesProvider;
|
|
22
|
+
this._dataPulseProvider = new DataPulseProvider(this._historyProvider, updateFrequency);
|
|
23
|
+
this._quotesPulseProvider = new QuotesPulseProvider(this._quotesProvider);
|
|
24
|
+
this._configurationReadyPromise = this._requestConfiguration()
|
|
25
|
+
.then((configuration) => {
|
|
26
|
+
if (configuration === null) {
|
|
27
|
+
configuration = defaultConfiguration();
|
|
28
|
+
}
|
|
29
|
+
this._setupWithConfiguration(configuration);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
onReady(callback) {
|
|
33
|
+
this._configurationReadyPromise.then(() => {
|
|
34
|
+
callback(this._configuration);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
getQuotes(symbols, onDataCallback, onErrorCallback) {
|
|
38
|
+
this._quotesProvider.getQuotes(symbols).then(onDataCallback).catch(onErrorCallback);
|
|
39
|
+
}
|
|
40
|
+
subscribeQuotes(symbols, fastSymbols, onRealtimeCallback, listenerGuid) {
|
|
41
|
+
this._quotesPulseProvider.subscribeQuotes(symbols, fastSymbols, onRealtimeCallback, listenerGuid);
|
|
42
|
+
}
|
|
43
|
+
unsubscribeQuotes(listenerGuid) {
|
|
44
|
+
this._quotesPulseProvider.unsubscribeQuotes(listenerGuid);
|
|
45
|
+
}
|
|
46
|
+
getMarks(symbolInfo, from, to, onDataCallback, resolution) {
|
|
47
|
+
if (!this._configuration.supports_marks) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const requestParams = {
|
|
51
|
+
symbol: symbolInfo.ticker || '',
|
|
52
|
+
from: from,
|
|
53
|
+
to: to,
|
|
54
|
+
resolution: resolution,
|
|
55
|
+
};
|
|
56
|
+
this._send('marks', requestParams)
|
|
57
|
+
.then((response) => {
|
|
58
|
+
if (!Array.isArray(response)) {
|
|
59
|
+
const result = [];
|
|
60
|
+
for (let i = 0; i < response.id.length; ++i) {
|
|
61
|
+
result.push({
|
|
62
|
+
id: extractField(response, 'id', i),
|
|
63
|
+
time: extractField(response, 'time', i),
|
|
64
|
+
color: extractField(response, 'color', i),
|
|
65
|
+
text: extractField(response, 'text', i),
|
|
66
|
+
label: extractField(response, 'label', i),
|
|
67
|
+
labelFontColor: extractField(response, 'labelFontColor', i),
|
|
68
|
+
minSize: extractField(response, 'minSize', i),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
response = result;
|
|
72
|
+
}
|
|
73
|
+
onDataCallback(response);
|
|
74
|
+
})
|
|
75
|
+
.catch((error) => {
|
|
76
|
+
logMessage(`UdfCompatibleDatafeed: Request marks failed: ${getErrorMessage(error)}`);
|
|
77
|
+
onDataCallback([]);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
getTimescaleMarks(symbolInfo, from, to, onDataCallback, resolution) {
|
|
81
|
+
if (!this._configuration.supports_timescale_marks) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const requestParams = {
|
|
85
|
+
symbol: symbolInfo.ticker || '',
|
|
86
|
+
from: from,
|
|
87
|
+
to: to,
|
|
88
|
+
resolution: resolution,
|
|
89
|
+
};
|
|
90
|
+
this._send('timescale_marks', requestParams)
|
|
91
|
+
.then((response) => {
|
|
92
|
+
if (!Array.isArray(response)) {
|
|
93
|
+
const result = [];
|
|
94
|
+
for (let i = 0; i < response.id.length; ++i) {
|
|
95
|
+
result.push({
|
|
96
|
+
id: extractField(response, 'id', i),
|
|
97
|
+
time: extractField(response, 'time', i),
|
|
98
|
+
color: extractField(response, 'color', i),
|
|
99
|
+
label: extractField(response, 'label', i),
|
|
100
|
+
tooltip: extractField(response, 'tooltip', i),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
response = result;
|
|
104
|
+
}
|
|
105
|
+
onDataCallback(response);
|
|
106
|
+
})
|
|
107
|
+
.catch((error) => {
|
|
108
|
+
logMessage(`UdfCompatibleDatafeed: Request timescale marks failed: ${getErrorMessage(error)}`);
|
|
109
|
+
onDataCallback([]);
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
getServerTime(callback) {
|
|
113
|
+
if (!this._configuration.supports_time) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this._send('time')
|
|
117
|
+
.then((response) => {
|
|
118
|
+
const time = parseInt(response);
|
|
119
|
+
if (!isNaN(time)) {
|
|
120
|
+
callback(time);
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
.catch((error) => {
|
|
124
|
+
logMessage(`UdfCompatibleDatafeed: Fail to load server time, error=${getErrorMessage(error)}`);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
searchSymbols(userInput, exchange, symbolType, onResult) {
|
|
128
|
+
if (this._configuration.supports_search) {
|
|
129
|
+
const params = {
|
|
130
|
+
limit: 30 /* SearchItemsLimit */,
|
|
131
|
+
query: userInput.toUpperCase(),
|
|
132
|
+
type: symbolType,
|
|
133
|
+
exchange: exchange,
|
|
134
|
+
};
|
|
135
|
+
this._send('search', params)
|
|
136
|
+
.then((response) => {
|
|
137
|
+
if (response.s !== undefined) {
|
|
138
|
+
logMessage(`UdfCompatibleDatafeed: search symbols error=${response.errmsg}`);
|
|
139
|
+
onResult([]);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
onResult(response);
|
|
143
|
+
})
|
|
144
|
+
.catch((reason) => {
|
|
145
|
+
logMessage(`UdfCompatibleDatafeed: Search symbols for '${userInput}' failed. Error=${getErrorMessage(reason)}`);
|
|
146
|
+
onResult([]);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
if (this._symbolsStorage === null) {
|
|
151
|
+
throw new Error('UdfCompatibleDatafeed: inconsistent configuration (symbols storage)');
|
|
152
|
+
}
|
|
153
|
+
this._symbolsStorage.searchSymbols(userInput, exchange, symbolType, 30 /* SearchItemsLimit */)
|
|
154
|
+
.then(onResult)
|
|
155
|
+
.catch(onResult.bind(null, []));
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
resolveSymbol(symbolName, onResolve, onError, extension) {
|
|
159
|
+
logMessage('Resolve requested');
|
|
160
|
+
const currencyCode = extension && extension.currencyCode;
|
|
161
|
+
const unitId = extension && extension.unitId;
|
|
162
|
+
const resolveRequestStartTime = Date.now();
|
|
163
|
+
function onResultReady(symbolInfo) {
|
|
164
|
+
logMessage(`Symbol resolved: ${Date.now() - resolveRequestStartTime}ms`);
|
|
165
|
+
onResolve(symbolInfo);
|
|
166
|
+
}
|
|
167
|
+
if (!this._configuration.supports_group_request) {
|
|
168
|
+
const params = {
|
|
169
|
+
symbol: symbolName,
|
|
170
|
+
};
|
|
171
|
+
if (currencyCode !== undefined) {
|
|
172
|
+
params.currencyCode = currencyCode;
|
|
173
|
+
}
|
|
174
|
+
if (unitId !== undefined) {
|
|
175
|
+
params.unitId = unitId;
|
|
176
|
+
}
|
|
177
|
+
this._send('symbols', params)
|
|
178
|
+
.then((response) => {
|
|
179
|
+
if (response.s !== undefined) {
|
|
180
|
+
onError('unknown_symbol');
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
onResultReady(response);
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
.catch((reason) => {
|
|
187
|
+
logMessage(`UdfCompatibleDatafeed: Error resolving symbol: ${getErrorMessage(reason)}`);
|
|
188
|
+
onError('unknown_symbol');
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
if (this._symbolsStorage === null) {
|
|
193
|
+
throw new Error('UdfCompatibleDatafeed: inconsistent configuration (symbols storage)');
|
|
194
|
+
}
|
|
195
|
+
this._symbolsStorage.resolveSymbol(symbolName, currencyCode, unitId).then(onResultReady).catch(onError);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
getBars(symbolInfo, resolution, periodParams, onResult, onError) {
|
|
199
|
+
this._historyProvider.getBars(symbolInfo, resolution, periodParams)
|
|
200
|
+
.then((result) => {
|
|
201
|
+
onResult(result.bars, result.meta);
|
|
202
|
+
})
|
|
203
|
+
.catch(onError);
|
|
204
|
+
}
|
|
205
|
+
subscribeBars(symbolInfo, resolution, onTick, listenerGuid, onResetCacheNeededCallback) {
|
|
206
|
+
this._dataPulseProvider.subscribeBars(symbolInfo, resolution, onTick, listenerGuid);
|
|
207
|
+
}
|
|
208
|
+
unsubscribeBars(listenerGuid) {
|
|
209
|
+
this._dataPulseProvider.unsubscribeBars(listenerGuid);
|
|
210
|
+
}
|
|
211
|
+
_requestConfiguration() {
|
|
212
|
+
return this._send('config')
|
|
213
|
+
.catch((reason) => {
|
|
214
|
+
logMessage(`UdfCompatibleDatafeed: Cannot get datafeed configuration - use default, error=${getErrorMessage(reason)}`);
|
|
215
|
+
return null;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
_send(urlPath, params) {
|
|
219
|
+
return this._requester.sendRequest(this._datafeedURL, urlPath, params);
|
|
220
|
+
}
|
|
221
|
+
_setupWithConfiguration(configurationData) {
|
|
222
|
+
this._configuration = configurationData;
|
|
223
|
+
if (configurationData.exchanges === undefined) {
|
|
224
|
+
configurationData.exchanges = [];
|
|
225
|
+
}
|
|
226
|
+
if (!configurationData.supports_search && !configurationData.supports_group_request) {
|
|
227
|
+
throw new Error('Unsupported datafeed configuration. Must either support search, or support group request');
|
|
228
|
+
}
|
|
229
|
+
if (configurationData.supports_group_request || !configurationData.supports_search) {
|
|
230
|
+
this._symbolsStorage = new SymbolsStorage(this._datafeedURL, configurationData.supported_resolutions || [], this._requester);
|
|
231
|
+
}
|
|
232
|
+
logMessage(`UdfCompatibleDatafeed: Initialized with ${JSON.stringify(configurationData)}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
function defaultConfiguration() {
|
|
236
|
+
return {
|
|
237
|
+
supports_search: false,
|
|
238
|
+
supports_group_request: true,
|
|
239
|
+
supported_resolutions: [
|
|
240
|
+
'1',
|
|
241
|
+
'5',
|
|
242
|
+
'15',
|
|
243
|
+
'30',
|
|
244
|
+
'60',
|
|
245
|
+
'1D',
|
|
246
|
+
'1W',
|
|
247
|
+
'1M',
|
|
248
|
+
],
|
|
249
|
+
supports_marks: false,
|
|
250
|
+
supports_timescale_marks: false,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { UDFCompatibleDatafeedBase } from './udf-compatible-datafeed-base';
|
|
2
|
+
import { QuotesProvider } from './quotes-provider';
|
|
3
|
+
import { Requester } from './requester';
|
|
4
|
+
export class UDFCompatibleDatafeed extends UDFCompatibleDatafeedBase {
|
|
5
|
+
constructor(datafeedURL, updateFrequency = 10 * 1000) {
|
|
6
|
+
const requester = new Requester();
|
|
7
|
+
const quotesProvider = new QuotesProvider(datafeedURL, requester);
|
|
8
|
+
super(datafeedURL, quotesProvider, requester, updateFrequency);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"private": true,
|
|
3
|
+
"dependencies": {
|
|
4
|
+
"tslib": "2.3.0"
|
|
5
|
+
},
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@rollup/plugin-node-resolve": "~9.0.0",
|
|
8
|
+
"rollup": "~2.28.2",
|
|
9
|
+
"rollup-plugin-terser": "~7.0.2",
|
|
10
|
+
"typescript": "4.3.5"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"compile": "tsc",
|
|
14
|
+
"bundle-js": "rollup -c rollup.config.js",
|
|
15
|
+
"build": "npm run compile && npm run bundle-js"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* globals process */
|
|
2
|
+
|
|
3
|
+
import { terser } from 'rollup-plugin-terser';
|
|
4
|
+
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
5
|
+
|
|
6
|
+
const environment = process.env.ENV || 'development';
|
|
7
|
+
const isDevelopmentEnv = (environment === 'development');
|
|
8
|
+
|
|
9
|
+
export default [
|
|
10
|
+
{
|
|
11
|
+
input: 'lib/udf-compatible-datafeed.js',
|
|
12
|
+
output: {
|
|
13
|
+
name: 'Datafeeds',
|
|
14
|
+
format: 'umd',
|
|
15
|
+
file: 'dist/bundle.js',
|
|
16
|
+
},
|
|
17
|
+
plugins: [
|
|
18
|
+
nodeResolve(),
|
|
19
|
+
!isDevelopmentEnv && terser({
|
|
20
|
+
ecma: 2018,
|
|
21
|
+
output: { inline_script: true },
|
|
22
|
+
}),
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LibrarySymbolInfo,
|
|
3
|
+
SubscribeBarsCallback,
|
|
4
|
+
} from '../../../charting_library/datafeed-api';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
GetBarsResult,
|
|
8
|
+
HistoryProvider,
|
|
9
|
+
} from './history-provider';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
getErrorMessage,
|
|
13
|
+
logMessage,
|
|
14
|
+
} from './helpers';
|
|
15
|
+
|
|
16
|
+
interface DataSubscriber {
|
|
17
|
+
symbolInfo: LibrarySymbolInfo;
|
|
18
|
+
resolution: string;
|
|
19
|
+
lastBarTime: number | null;
|
|
20
|
+
listener: SubscribeBarsCallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface DataSubscribers {
|
|
24
|
+
[guid: string]: DataSubscriber;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export class DataPulseProvider {
|
|
28
|
+
private readonly _subscribers: DataSubscribers = {};
|
|
29
|
+
private _requestsPending: number = 0;
|
|
30
|
+
private readonly _historyProvider: HistoryProvider;
|
|
31
|
+
|
|
32
|
+
public constructor(historyProvider: HistoryProvider, updateFrequency: number) {
|
|
33
|
+
this._historyProvider = historyProvider;
|
|
34
|
+
setInterval(this._updateData.bind(this), updateFrequency);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public subscribeBars(symbolInfo: LibrarySymbolInfo, resolution: string, newDataCallback: SubscribeBarsCallback, listenerGuid: string): void {
|
|
38
|
+
if (this._subscribers.hasOwnProperty(listenerGuid)) {
|
|
39
|
+
logMessage(`DataPulseProvider: already has subscriber with id=${listenerGuid}`);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this._subscribers[listenerGuid] = {
|
|
44
|
+
lastBarTime: null,
|
|
45
|
+
listener: newDataCallback,
|
|
46
|
+
resolution: resolution,
|
|
47
|
+
symbolInfo: symbolInfo,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
logMessage(`DataPulseProvider: subscribed for #${listenerGuid} - {${symbolInfo.name}, ${resolution}}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public unsubscribeBars(listenerGuid: string): void {
|
|
54
|
+
delete this._subscribers[listenerGuid];
|
|
55
|
+
logMessage(`DataPulseProvider: unsubscribed for #${listenerGuid}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private _updateData(): void {
|
|
59
|
+
if (this._requestsPending > 0) {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
this._requestsPending = 0;
|
|
64
|
+
for (const listenerGuid in this._subscribers) { // tslint:disable-line:forin
|
|
65
|
+
this._requestsPending += 1;
|
|
66
|
+
this._updateDataForSubscriber(listenerGuid)
|
|
67
|
+
.then(() => {
|
|
68
|
+
this._requestsPending -= 1;
|
|
69
|
+
logMessage(`DataPulseProvider: data for #${listenerGuid} updated successfully, pending=${this._requestsPending}`);
|
|
70
|
+
})
|
|
71
|
+
.catch((reason?: string | Error) => {
|
|
72
|
+
this._requestsPending -= 1;
|
|
73
|
+
logMessage(`DataPulseProvider: data for #${listenerGuid} updated with error=${getErrorMessage(reason)}, pending=${this._requestsPending}`);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private _updateDataForSubscriber(listenerGuid: string): Promise<void> {
|
|
79
|
+
const subscriptionRecord = this._subscribers[listenerGuid];
|
|
80
|
+
|
|
81
|
+
const rangeEndTime = parseInt((Date.now() / 1000).toString());
|
|
82
|
+
|
|
83
|
+
// BEWARE: please note we really need 2 bars, not the only last one
|
|
84
|
+
// see the explanation below. `10` is the `large enough` value to work around holidays
|
|
85
|
+
const rangeStartTime = rangeEndTime - periodLengthSeconds(subscriptionRecord.resolution, 10);
|
|
86
|
+
|
|
87
|
+
return this._historyProvider.getBars(
|
|
88
|
+
subscriptionRecord.symbolInfo,
|
|
89
|
+
subscriptionRecord.resolution,
|
|
90
|
+
{
|
|
91
|
+
from: rangeStartTime,
|
|
92
|
+
to: rangeEndTime,
|
|
93
|
+
countBack: 2,
|
|
94
|
+
firstDataRequest: false,
|
|
95
|
+
})
|
|
96
|
+
.then((result: GetBarsResult) => {
|
|
97
|
+
this._onSubscriberDataReceived(listenerGuid, result);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private _onSubscriberDataReceived(listenerGuid: string, result: GetBarsResult): void {
|
|
102
|
+
// means the subscription was cancelled while waiting for data
|
|
103
|
+
if (!this._subscribers.hasOwnProperty(listenerGuid)) {
|
|
104
|
+
logMessage(`DataPulseProvider: Data comes for already unsubscribed subscription #${listenerGuid}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const bars = result.bars;
|
|
109
|
+
if (bars.length === 0) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const lastBar = bars[bars.length - 1];
|
|
114
|
+
const subscriptionRecord = this._subscribers[listenerGuid];
|
|
115
|
+
|
|
116
|
+
if (subscriptionRecord.lastBarTime !== null && lastBar.time < subscriptionRecord.lastBarTime) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const isNewBar = subscriptionRecord.lastBarTime !== null && lastBar.time > subscriptionRecord.lastBarTime;
|
|
121
|
+
|
|
122
|
+
// 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
|
|
123
|
+
// old bar's last 5 seconds trades will be lost). Thus, at fist we should broadcast old bar updates when it's ready.
|
|
124
|
+
if (isNewBar) {
|
|
125
|
+
if (bars.length < 2) {
|
|
126
|
+
throw new Error('Not enough bars in history for proper pulse update. Need at least 2.');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const previousBar = bars[bars.length - 2];
|
|
130
|
+
subscriptionRecord.listener(previousBar);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
subscriptionRecord.lastBarTime = lastBar.time;
|
|
134
|
+
subscriptionRecord.listener(lastBar);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function periodLengthSeconds(resolution: string, requiredPeriodsCount: number): number {
|
|
139
|
+
let daysCount = 0;
|
|
140
|
+
|
|
141
|
+
if (resolution === 'D' || resolution === '1D') {
|
|
142
|
+
daysCount = requiredPeriodsCount;
|
|
143
|
+
} else if (resolution === 'M' || resolution === '1M') {
|
|
144
|
+
daysCount = 31 * requiredPeriodsCount;
|
|
145
|
+
} else if (resolution === 'W' || resolution === '1W') {
|
|
146
|
+
daysCount = 7 * requiredPeriodsCount;
|
|
147
|
+
} else {
|
|
148
|
+
daysCount = requiredPeriodsCount * parseInt(resolution) / (24 * 60);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return daysCount * 24 * 60 * 60;
|
|
152
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface RequestParams {
|
|
2
|
+
[paramName: string]: string | string[] | number;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export interface UdfResponse {
|
|
6
|
+
s: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UdfOkResponse extends UdfResponse {
|
|
10
|
+
s: 'ok';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UdfErrorResponse {
|
|
14
|
+
s: 'error';
|
|
15
|
+
errmsg: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* If you want to enable logs from datafeed set it to `true`
|
|
20
|
+
*/
|
|
21
|
+
const isLoggingEnabled = false;
|
|
22
|
+
export function logMessage(message: string): void {
|
|
23
|
+
if (isLoggingEnabled) {
|
|
24
|
+
const now = new Date();
|
|
25
|
+
// tslint:disable-next-line:no-console
|
|
26
|
+
console.log(`${now.toLocaleTimeString()}.${now.getMilliseconds()}> ${message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getErrorMessage(error: string | Error | undefined): string {
|
|
31
|
+
if (error === undefined) {
|
|
32
|
+
return '';
|
|
33
|
+
} else if (typeof error === 'string') {
|
|
34
|
+
return error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return error.message;
|
|
38
|
+
}
|