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,134 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Bar,
|
|
3
|
+
HistoryMetadata,
|
|
4
|
+
LibrarySymbolInfo,
|
|
5
|
+
PeriodParams,
|
|
6
|
+
} from '../../../charting_library/datafeed-api';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getErrorMessage,
|
|
10
|
+
RequestParams,
|
|
11
|
+
UdfErrorResponse,
|
|
12
|
+
UdfOkResponse,
|
|
13
|
+
UdfResponse,
|
|
14
|
+
} from './helpers';
|
|
15
|
+
|
|
16
|
+
import { Requester } from './requester';
|
|
17
|
+
// tslint:disable: no-any
|
|
18
|
+
interface HistoryPartialDataResponse extends UdfOkResponse {
|
|
19
|
+
t: any;
|
|
20
|
+
c: any;
|
|
21
|
+
o?: never;
|
|
22
|
+
h?: never;
|
|
23
|
+
l?: never;
|
|
24
|
+
v?: never;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HistoryFullDataResponse extends UdfOkResponse {
|
|
28
|
+
t: any;
|
|
29
|
+
c: any;
|
|
30
|
+
o: any;
|
|
31
|
+
h: any;
|
|
32
|
+
l: any;
|
|
33
|
+
v: any;
|
|
34
|
+
}
|
|
35
|
+
// tslint:enable: no-any
|
|
36
|
+
interface HistoryNoDataResponse extends UdfResponse {
|
|
37
|
+
s: 'no_data';
|
|
38
|
+
nextTime?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type HistoryResponse = HistoryFullDataResponse | HistoryPartialDataResponse | HistoryNoDataResponse;
|
|
42
|
+
|
|
43
|
+
export type PeriodParamsWithOptionalCountback = Omit<PeriodParams, 'countBack'> & { countBack?: number };
|
|
44
|
+
|
|
45
|
+
export interface GetBarsResult {
|
|
46
|
+
bars: Bar[];
|
|
47
|
+
meta: HistoryMetadata;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class HistoryProvider {
|
|
51
|
+
private _datafeedUrl: string;
|
|
52
|
+
private readonly _requester: Requester;
|
|
53
|
+
|
|
54
|
+
public constructor(datafeedUrl: string, requester: Requester) {
|
|
55
|
+
this._datafeedUrl = datafeedUrl;
|
|
56
|
+
this._requester = requester;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public getBars(symbolInfo: LibrarySymbolInfo, resolution: string, periodParams: PeriodParamsWithOptionalCountback): Promise<GetBarsResult> {
|
|
60
|
+
const requestParams: RequestParams = {
|
|
61
|
+
symbol: symbolInfo.ticker || '',
|
|
62
|
+
resolution: resolution,
|
|
63
|
+
from: periodParams.from,
|
|
64
|
+
to: periodParams.to,
|
|
65
|
+
};
|
|
66
|
+
if (periodParams.countBack !== undefined) {
|
|
67
|
+
requestParams.countback = periodParams.countBack;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (symbolInfo.currency_code !== undefined) {
|
|
71
|
+
requestParams.currencyCode = symbolInfo.currency_code;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (symbolInfo.unit_id !== undefined) {
|
|
75
|
+
requestParams.unitId = symbolInfo.unit_id;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return new Promise((resolve: (result: GetBarsResult) => void, reject: (reason: string) => void) => {
|
|
79
|
+
this._requester.sendRequest<HistoryResponse>(this._datafeedUrl, 'history', requestParams)
|
|
80
|
+
.then((response: HistoryResponse | UdfErrorResponse) => {
|
|
81
|
+
if (response.s !== 'ok' && response.s !== 'no_data') {
|
|
82
|
+
reject(response.errmsg);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const bars: Bar[] = [];
|
|
87
|
+
const meta: HistoryMetadata = {
|
|
88
|
+
noData: false,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (response.s === 'no_data') {
|
|
92
|
+
meta.noData = true;
|
|
93
|
+
meta.nextTime = response.nextTime;
|
|
94
|
+
} else {
|
|
95
|
+
const volumePresent = response.v !== undefined;
|
|
96
|
+
const ohlPresent = response.o !== undefined;
|
|
97
|
+
|
|
98
|
+
for (let i = 0; i < response.t.length; ++i) {
|
|
99
|
+
const barValue: Bar = {
|
|
100
|
+
time: response.t[i] * 1000,
|
|
101
|
+
close: parseFloat(response.c[i]),
|
|
102
|
+
open: parseFloat(response.c[i]),
|
|
103
|
+
high: parseFloat(response.c[i]),
|
|
104
|
+
low: parseFloat(response.c[i]),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (ohlPresent) {
|
|
108
|
+
barValue.open = parseFloat((response as HistoryFullDataResponse).o[i]);
|
|
109
|
+
barValue.high = parseFloat((response as HistoryFullDataResponse).h[i]);
|
|
110
|
+
barValue.low = parseFloat((response as HistoryFullDataResponse).l[i]);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (volumePresent) {
|
|
114
|
+
barValue.volume = parseFloat((response as HistoryFullDataResponse).v[i]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
bars.push(barValue);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
resolve({
|
|
122
|
+
bars: bars,
|
|
123
|
+
meta: meta,
|
|
124
|
+
});
|
|
125
|
+
})
|
|
126
|
+
.catch((reason?: string | Error) => {
|
|
127
|
+
const reasonString = getErrorMessage(reason);
|
|
128
|
+
// tslint:disable-next-line:no-console
|
|
129
|
+
console.warn(`HistoryProvider: getBars() failed, error=${reasonString}`);
|
|
130
|
+
reject(reasonString);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QuoteData } from '../../../charting_library/datafeed-api';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
UdfOkResponse,
|
|
5
|
+
} from './helpers';
|
|
6
|
+
|
|
7
|
+
export interface UdfQuotesResponse extends UdfOkResponse {
|
|
8
|
+
d: QuoteData[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IQuotesProvider {
|
|
12
|
+
// tslint:disable-next-line:variable-name tv-variable-name
|
|
13
|
+
getQuotes(symbols: string[]): Promise<QuoteData[]>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { UdfQuotesResponse, IQuotesProvider } from './iquotes-provider';
|
|
2
|
+
import { QuoteData } from '../../../charting_library/datafeed-api';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
getErrorMessage,
|
|
6
|
+
logMessage,
|
|
7
|
+
UdfErrorResponse,
|
|
8
|
+
} from './helpers';
|
|
9
|
+
import { Requester } from './requester';
|
|
10
|
+
|
|
11
|
+
export class QuotesProvider implements IQuotesProvider {
|
|
12
|
+
private readonly _datafeedUrl: string;
|
|
13
|
+
private readonly _requester: Requester;
|
|
14
|
+
|
|
15
|
+
public constructor(datafeedUrl: string, requester: Requester) {
|
|
16
|
+
this._datafeedUrl = datafeedUrl;
|
|
17
|
+
this._requester = requester;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public getQuotes(symbols: string[]): Promise<QuoteData[]> {
|
|
21
|
+
return new Promise((resolve: (data: QuoteData[]) => void, reject: (reason: string) => void) => {
|
|
22
|
+
this._requester.sendRequest<UdfQuotesResponse>(this._datafeedUrl, 'quotes', { symbols: symbols })
|
|
23
|
+
.then((response: UdfQuotesResponse | UdfErrorResponse) => {
|
|
24
|
+
if (response.s === 'ok') {
|
|
25
|
+
resolve(response.d);
|
|
26
|
+
} else {
|
|
27
|
+
reject(response.errmsg);
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.catch((error?: string | Error) => {
|
|
31
|
+
const errorMessage = getErrorMessage(error);
|
|
32
|
+
logMessage(`QuotesProvider: getQuotes failed, error=${errorMessage}`);
|
|
33
|
+
reject(`network error: ${errorMessage}`);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import {
|
|
2
|
+
QuoteData,
|
|
3
|
+
QuotesCallback,
|
|
4
|
+
} from '../../../charting_library/datafeed-api';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
getErrorMessage,
|
|
8
|
+
logMessage,
|
|
9
|
+
} from './helpers';
|
|
10
|
+
|
|
11
|
+
import { IQuotesProvider } from './iquotes-provider';
|
|
12
|
+
|
|
13
|
+
interface QuoteSubscriber {
|
|
14
|
+
symbols: string[];
|
|
15
|
+
fastSymbols: string[];
|
|
16
|
+
listener: QuotesCallback;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface QuoteSubscribers {
|
|
20
|
+
[listenerId: string]: QuoteSubscriber;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const enum SymbolsType {
|
|
24
|
+
General,
|
|
25
|
+
Fast,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const enum UpdateTimeouts {
|
|
29
|
+
Fast = 10 * 1000,
|
|
30
|
+
General = 60 * 1000,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class QuotesPulseProvider {
|
|
34
|
+
private readonly _quotesProvider: IQuotesProvider;
|
|
35
|
+
private readonly _subscribers: QuoteSubscribers = {};
|
|
36
|
+
private _requestsPending: number = 0;
|
|
37
|
+
|
|
38
|
+
public constructor(quotesProvider: IQuotesProvider) {
|
|
39
|
+
this._quotesProvider = quotesProvider;
|
|
40
|
+
|
|
41
|
+
setInterval(this._updateQuotes.bind(this, SymbolsType.Fast), UpdateTimeouts.Fast);
|
|
42
|
+
setInterval(this._updateQuotes.bind(this, SymbolsType.General), UpdateTimeouts.General);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public subscribeQuotes(symbols: string[], fastSymbols: string[], onRealtimeCallback: QuotesCallback, listenerGuid: string): void {
|
|
46
|
+
this._subscribers[listenerGuid] = {
|
|
47
|
+
symbols: symbols,
|
|
48
|
+
fastSymbols: fastSymbols,
|
|
49
|
+
listener: onRealtimeCallback,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
logMessage(`QuotesPulseProvider: subscribed quotes with #${listenerGuid}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public unsubscribeQuotes(listenerGuid: string): void {
|
|
56
|
+
delete this._subscribers[listenerGuid];
|
|
57
|
+
logMessage(`QuotesPulseProvider: unsubscribed quotes with #${listenerGuid}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
private _updateQuotes(updateType: SymbolsType): void {
|
|
61
|
+
if (this._requestsPending > 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const listenerGuid in this._subscribers) { // tslint:disable-line:forin
|
|
66
|
+
this._requestsPending++;
|
|
67
|
+
|
|
68
|
+
const subscriptionRecord = this._subscribers[listenerGuid];
|
|
69
|
+
this._quotesProvider.getQuotes(updateType === SymbolsType.Fast ? subscriptionRecord.fastSymbols : subscriptionRecord.symbols)
|
|
70
|
+
.then((data: QuoteData[]) => {
|
|
71
|
+
this._requestsPending--;
|
|
72
|
+
if (!this._subscribers.hasOwnProperty(listenerGuid)) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
subscriptionRecord.listener(data);
|
|
77
|
+
logMessage(`QuotesPulseProvider: data for #${listenerGuid} (${updateType}) updated successfully, pending=${this._requestsPending}`);
|
|
78
|
+
})
|
|
79
|
+
.catch((reason?: string | Error) => {
|
|
80
|
+
this._requestsPending--;
|
|
81
|
+
logMessage(`QuotesPulseProvider: data for #${listenerGuid} (${updateType}) updated with error=${getErrorMessage(reason)}, pending=${this._requestsPending}`);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { RequestParams, UdfResponse, UdfErrorResponse, logMessage } from './helpers';
|
|
2
|
+
|
|
3
|
+
export class Requester {
|
|
4
|
+
private _headers: HeadersInit | undefined;
|
|
5
|
+
|
|
6
|
+
public constructor(headers?: HeadersInit) {
|
|
7
|
+
if (headers) {
|
|
8
|
+
this._headers = headers;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
public sendRequest<T extends UdfResponse>(datafeedUrl: string, urlPath: string, params?: RequestParams): Promise<T | UdfErrorResponse>;
|
|
13
|
+
public sendRequest<T>(datafeedUrl: string, urlPath: string, params?: RequestParams): Promise<T>;
|
|
14
|
+
public sendRequest<T>(datafeedUrl: string, urlPath: string, params?: RequestParams): Promise<T> {
|
|
15
|
+
if (params !== undefined) {
|
|
16
|
+
const paramKeys = Object.keys(params);
|
|
17
|
+
if (paramKeys.length !== 0) {
|
|
18
|
+
urlPath += '?';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
urlPath += paramKeys.map((key: string) => {
|
|
22
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(params[key].toString())}`;
|
|
23
|
+
}).join('&');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
logMessage('New request: ' + urlPath);
|
|
27
|
+
|
|
28
|
+
// Send user cookies if the URL is on the same origin as the calling script.
|
|
29
|
+
const options: RequestInit = { credentials: 'same-origin' };
|
|
30
|
+
|
|
31
|
+
if (this._headers !== undefined) {
|
|
32
|
+
options.headers = this._headers;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return fetch(`${datafeedUrl}/${urlPath}`, options)
|
|
36
|
+
.then((response: Response) => response.text())
|
|
37
|
+
.then((responseTest: string) => JSON.parse(responseTest));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LibrarySymbolInfo,
|
|
3
|
+
SearchSymbolResultItem,
|
|
4
|
+
ResolutionString,
|
|
5
|
+
} from '../../../charting_library/datafeed-api';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getErrorMessage,
|
|
9
|
+
logMessage,
|
|
10
|
+
} from './helpers';
|
|
11
|
+
|
|
12
|
+
import { Requester } from './requester';
|
|
13
|
+
|
|
14
|
+
interface SymbolInfoMap {
|
|
15
|
+
[symbol: string]: LibrarySymbolInfo | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ExchangeDataResponseSymbolData {
|
|
19
|
+
'type': string;
|
|
20
|
+
'timezone': LibrarySymbolInfo['timezone'];
|
|
21
|
+
'description': string;
|
|
22
|
+
|
|
23
|
+
'exchange-listed': string;
|
|
24
|
+
'exchange-traded': string;
|
|
25
|
+
|
|
26
|
+
'session-regular': string;
|
|
27
|
+
'corrections'?: string;
|
|
28
|
+
'session-holidays'?: string;
|
|
29
|
+
|
|
30
|
+
'fractional': boolean;
|
|
31
|
+
|
|
32
|
+
'pricescale': number;
|
|
33
|
+
|
|
34
|
+
'ticker'?: string;
|
|
35
|
+
|
|
36
|
+
'minmov2'?: number;
|
|
37
|
+
'minmove2'?: number;
|
|
38
|
+
|
|
39
|
+
'minmov'?: number;
|
|
40
|
+
'minmovement'?: number;
|
|
41
|
+
|
|
42
|
+
'supported-resolutions'?: ResolutionString[];
|
|
43
|
+
'intraday-multipliers'?: string[];
|
|
44
|
+
|
|
45
|
+
'has-intraday'?: boolean;
|
|
46
|
+
'has-daily'?: boolean;
|
|
47
|
+
'has-weekly-and-monthly'?: boolean;
|
|
48
|
+
'has-empty-bars'?: boolean;
|
|
49
|
+
'has-no-volume'?: boolean;
|
|
50
|
+
'currency-code'?: string;
|
|
51
|
+
'original-currency-code'?: string;
|
|
52
|
+
'unit-id'?: string;
|
|
53
|
+
'original-unit-id'?: string;
|
|
54
|
+
'unit-conversion-types'?: string[];
|
|
55
|
+
|
|
56
|
+
'volume-precision'?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Here is some black magic with types to get compile-time checks of names and types
|
|
60
|
+
type PickArrayedObjectFields<T> = Pick<T, {
|
|
61
|
+
// tslint:disable-next-line:no-any
|
|
62
|
+
[K in keyof T]-?: NonNullable<T[K]> extends any[] ? K : never;
|
|
63
|
+
}[keyof T]>;
|
|
64
|
+
|
|
65
|
+
type ExchangeDataResponseArrayedSymbolData = PickArrayedObjectFields<ExchangeDataResponseSymbolData>;
|
|
66
|
+
type ExchangeDataResponseNonArrayedSymbolData = Pick<ExchangeDataResponseSymbolData, Exclude<keyof ExchangeDataResponseSymbolData, keyof ExchangeDataResponseArrayedSymbolData>>;
|
|
67
|
+
|
|
68
|
+
type ExchangeDataResponse =
|
|
69
|
+
{
|
|
70
|
+
symbol: string[];
|
|
71
|
+
} &
|
|
72
|
+
{
|
|
73
|
+
[K in keyof ExchangeDataResponseSymbolData]: ExchangeDataResponseSymbolData[K] | NonNullable<ExchangeDataResponseSymbolData[K]>[];
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function extractField<Field extends keyof ExchangeDataResponseNonArrayedSymbolData>(data: ExchangeDataResponse, field: Field, arrayIndex: number): ExchangeDataResponseNonArrayedSymbolData[Field];
|
|
77
|
+
function extractField<Field extends keyof ExchangeDataResponseArrayedSymbolData>(data: ExchangeDataResponse, field: Field, arrayIndex: number, valueIsArray: true): ExchangeDataResponseArrayedSymbolData[Field];
|
|
78
|
+
function extractField<Field extends keyof ExchangeDataResponseSymbolData>(data: ExchangeDataResponse, field: Field, arrayIndex: number, valueIsArray?: boolean): ExchangeDataResponseSymbolData[Field] {
|
|
79
|
+
const value: ExchangeDataResponse[keyof ExchangeDataResponseSymbolData] = data[field];
|
|
80
|
+
|
|
81
|
+
if (Array.isArray(value) && (!valueIsArray || Array.isArray(value[0]))) {
|
|
82
|
+
return value[arrayIndex] as ExchangeDataResponseSymbolData[Field];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return value as ExchangeDataResponseSymbolData[Field];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function symbolKey(symbol: string, currency?: string, unit?: string): string {
|
|
89
|
+
// here we're using a separator that quite possible shouldn't be in a real symbol name
|
|
90
|
+
return symbol + (currency !== undefined ? '_%|#|%_' + currency : '') + (unit !== undefined ? '_%|#|%_' + unit : '');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class SymbolsStorage {
|
|
94
|
+
private readonly _exchangesList: string[] = ['NYSE', 'FOREX', 'AMEX'];
|
|
95
|
+
private readonly _symbolsInfo: SymbolInfoMap = {};
|
|
96
|
+
private readonly _symbolsList: string[] = [];
|
|
97
|
+
private readonly _datafeedUrl: string;
|
|
98
|
+
private readonly _readyPromise: Promise<void>;
|
|
99
|
+
private readonly _datafeedSupportedResolutions: ResolutionString[];
|
|
100
|
+
private readonly _requester: Requester;
|
|
101
|
+
|
|
102
|
+
public constructor(datafeedUrl: string, datafeedSupportedResolutions: ResolutionString[], requester: Requester) {
|
|
103
|
+
this._datafeedUrl = datafeedUrl;
|
|
104
|
+
this._datafeedSupportedResolutions = datafeedSupportedResolutions;
|
|
105
|
+
this._requester = requester;
|
|
106
|
+
this._readyPromise = this._init();
|
|
107
|
+
this._readyPromise.catch((error: Error) => {
|
|
108
|
+
// seems it is impossible
|
|
109
|
+
// tslint:disable-next-line:no-console
|
|
110
|
+
console.error(`SymbolsStorage: Cannot init, error=${error.toString()}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// BEWARE: this function does not consider symbol's exchange
|
|
115
|
+
public resolveSymbol(symbolName: string, currencyCode?: string, unitId?: string): Promise<LibrarySymbolInfo> {
|
|
116
|
+
return this._readyPromise.then(() => {
|
|
117
|
+
const symbolInfo = this._symbolsInfo[symbolKey(symbolName, currencyCode, unitId)];
|
|
118
|
+
if (symbolInfo === undefined) {
|
|
119
|
+
return Promise.reject('invalid symbol');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return Promise.resolve(symbolInfo);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public searchSymbols(searchString: string, exchange: string, symbolType: string, maxSearchResults: number): Promise<SearchSymbolResultItem[]> {
|
|
127
|
+
interface WeightedItem {
|
|
128
|
+
symbolInfo: LibrarySymbolInfo;
|
|
129
|
+
weight: number;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return this._readyPromise.then(() => {
|
|
133
|
+
const weightedResult: WeightedItem[] = [];
|
|
134
|
+
const queryIsEmpty = searchString.length === 0;
|
|
135
|
+
|
|
136
|
+
searchString = searchString.toUpperCase();
|
|
137
|
+
|
|
138
|
+
for (const symbolName of this._symbolsList) {
|
|
139
|
+
const symbolInfo = this._symbolsInfo[symbolName];
|
|
140
|
+
|
|
141
|
+
if (symbolInfo === undefined) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (symbolType.length > 0 && symbolInfo.type !== symbolType) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (exchange && exchange.length > 0 && symbolInfo.exchange !== exchange) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const positionInName = symbolInfo.name.toUpperCase().indexOf(searchString);
|
|
154
|
+
const positionInDescription = symbolInfo.description.toUpperCase().indexOf(searchString);
|
|
155
|
+
|
|
156
|
+
if (queryIsEmpty || positionInName >= 0 || positionInDescription >= 0) {
|
|
157
|
+
const alreadyExists = weightedResult.some((item: WeightedItem) => item.symbolInfo === symbolInfo);
|
|
158
|
+
if (!alreadyExists) {
|
|
159
|
+
const weight = positionInName >= 0 ? positionInName : 8000 + positionInDescription;
|
|
160
|
+
weightedResult.push({ symbolInfo: symbolInfo, weight: weight });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const result = weightedResult
|
|
166
|
+
.sort((item1: WeightedItem, item2: WeightedItem) => item1.weight - item2.weight)
|
|
167
|
+
.slice(0, maxSearchResults)
|
|
168
|
+
.map((item: WeightedItem) => {
|
|
169
|
+
const symbolInfo = item.symbolInfo;
|
|
170
|
+
return {
|
|
171
|
+
symbol: symbolInfo.name,
|
|
172
|
+
full_name: symbolInfo.full_name,
|
|
173
|
+
description: symbolInfo.description,
|
|
174
|
+
exchange: symbolInfo.exchange,
|
|
175
|
+
params: [],
|
|
176
|
+
type: symbolInfo.type,
|
|
177
|
+
ticker: symbolInfo.name,
|
|
178
|
+
};
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
return Promise.resolve(result);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private _init(): Promise<void> {
|
|
186
|
+
interface BooleanMap {
|
|
187
|
+
[key: string]: boolean | undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const promises: Promise<void>[] = [];
|
|
191
|
+
const alreadyRequestedExchanges: BooleanMap = {};
|
|
192
|
+
|
|
193
|
+
for (const exchange of this._exchangesList) {
|
|
194
|
+
if (alreadyRequestedExchanges[exchange]) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
alreadyRequestedExchanges[exchange] = true;
|
|
199
|
+
promises.push(this._requestExchangeData(exchange));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return Promise.all(promises)
|
|
203
|
+
.then(() => {
|
|
204
|
+
this._symbolsList.sort();
|
|
205
|
+
logMessage('SymbolsStorage: All exchanges data loaded');
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private _requestExchangeData(exchange: string): Promise<void> {
|
|
210
|
+
return new Promise((resolve: () => void, reject: (error: Error) => void) => {
|
|
211
|
+
this._requester.sendRequest<ExchangeDataResponse>(this._datafeedUrl, 'symbol_info', { group: exchange })
|
|
212
|
+
.then((response: ExchangeDataResponse) => {
|
|
213
|
+
try {
|
|
214
|
+
this._onExchangeDataReceived(exchange, response);
|
|
215
|
+
} catch (error) {
|
|
216
|
+
reject(error);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
resolve();
|
|
221
|
+
})
|
|
222
|
+
.catch((reason?: string | Error) => {
|
|
223
|
+
logMessage(`SymbolsStorage: Request data for exchange '${exchange}' failed, reason=${getErrorMessage(reason)}`);
|
|
224
|
+
resolve();
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private _onExchangeDataReceived(exchange: string, data: ExchangeDataResponse): void {
|
|
230
|
+
let symbolIndex = 0;
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
const symbolsCount = data.symbol.length;
|
|
234
|
+
const tickerPresent = data.ticker !== undefined;
|
|
235
|
+
|
|
236
|
+
for (; symbolIndex < symbolsCount; ++symbolIndex) {
|
|
237
|
+
const symbolName = data.symbol[symbolIndex];
|
|
238
|
+
const listedExchange = extractField(data, 'exchange-listed', symbolIndex);
|
|
239
|
+
const tradedExchange = extractField(data, 'exchange-traded', symbolIndex);
|
|
240
|
+
const fullName = tradedExchange + ':' + symbolName;
|
|
241
|
+
const currencyCode = extractField(data, 'currency-code', symbolIndex);
|
|
242
|
+
const unitId = extractField(data, 'unit-id', symbolIndex);
|
|
243
|
+
|
|
244
|
+
const ticker = tickerPresent ? (extractField(data, 'ticker', symbolIndex) as string) : symbolName;
|
|
245
|
+
|
|
246
|
+
const symbolInfo: LibrarySymbolInfo = {
|
|
247
|
+
ticker: ticker,
|
|
248
|
+
name: symbolName,
|
|
249
|
+
base_name: [listedExchange + ':' + symbolName],
|
|
250
|
+
full_name: fullName,
|
|
251
|
+
listed_exchange: listedExchange,
|
|
252
|
+
exchange: tradedExchange,
|
|
253
|
+
currency_code: currencyCode,
|
|
254
|
+
original_currency_code: extractField(data, 'original-currency-code', symbolIndex),
|
|
255
|
+
unit_id: unitId,
|
|
256
|
+
original_unit_id: extractField(data, 'original-unit-id', symbolIndex),
|
|
257
|
+
unit_conversion_types: extractField(data, 'unit-conversion-types', symbolIndex, true),
|
|
258
|
+
description: extractField(data, 'description', symbolIndex),
|
|
259
|
+
has_intraday: definedValueOrDefault(extractField(data, 'has-intraday', symbolIndex), false),
|
|
260
|
+
has_no_volume: definedValueOrDefault(extractField(data, 'has-no-volume', symbolIndex), false),
|
|
261
|
+
minmov: extractField(data, 'minmovement', symbolIndex) || extractField(data, 'minmov', symbolIndex) || 0,
|
|
262
|
+
minmove2: extractField(data, 'minmove2', symbolIndex) || extractField(data, 'minmov2', symbolIndex),
|
|
263
|
+
fractional: extractField(data, 'fractional', symbolIndex),
|
|
264
|
+
pricescale: extractField(data, 'pricescale', symbolIndex),
|
|
265
|
+
type: extractField(data, 'type', symbolIndex),
|
|
266
|
+
session: extractField(data, 'session-regular', symbolIndex),
|
|
267
|
+
session_holidays: extractField(data, 'session-holidays', symbolIndex),
|
|
268
|
+
corrections: extractField(data, 'corrections', symbolIndex),
|
|
269
|
+
timezone: extractField(data, 'timezone', symbolIndex),
|
|
270
|
+
supported_resolutions: definedValueOrDefault(extractField(data, 'supported-resolutions', symbolIndex, true), this._datafeedSupportedResolutions),
|
|
271
|
+
has_daily: definedValueOrDefault(extractField(data, 'has-daily', symbolIndex), true),
|
|
272
|
+
intraday_multipliers: definedValueOrDefault(extractField(data, 'intraday-multipliers', symbolIndex, true), ['1', '5', '15', '30', '60']),
|
|
273
|
+
has_weekly_and_monthly: extractField(data, 'has-weekly-and-monthly', symbolIndex),
|
|
274
|
+
has_empty_bars: extractField(data, 'has-empty-bars', symbolIndex),
|
|
275
|
+
volume_precision: definedValueOrDefault(extractField(data, 'volume-precision', symbolIndex), 0),
|
|
276
|
+
format: 'price',
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
this._symbolsInfo[ticker] = symbolInfo;
|
|
280
|
+
this._symbolsInfo[symbolName] = symbolInfo;
|
|
281
|
+
this._symbolsInfo[fullName] = symbolInfo;
|
|
282
|
+
if (currencyCode !== undefined || unitId !== undefined) {
|
|
283
|
+
this._symbolsInfo[symbolKey(ticker, currencyCode, unitId)] = symbolInfo;
|
|
284
|
+
this._symbolsInfo[symbolKey(symbolName, currencyCode, unitId)] = symbolInfo;
|
|
285
|
+
this._symbolsInfo[symbolKey(fullName, currencyCode, unitId)] = symbolInfo;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this._symbolsList.push(symbolName);
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
throw new Error(`SymbolsStorage: API error when processing exchange ${exchange} symbol #${symbolIndex} (${data.symbol[symbolIndex]}): ${error.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function definedValueOrDefault<T>(value: T | undefined, defaultValue: T): T {
|
|
297
|
+
return value !== undefined ? value : defaultValue;
|
|
298
|
+
}
|