cosmos-explorer/public/datafeeds/udf/src/udf-compatible-datafeed-base.ts
2021-11-21 09:42:01 +08:00

371 lines
12 KiB
TypeScript

import {
DatafeedConfiguration,
ErrorCallback,
GetMarksCallback,
HistoryCallback,
HistoryDepth,
IDatafeedChartApi,
IDatafeedQuotesApi,
IExternalDatafeed,
LibrarySymbolInfo,
Mark,
OnReadyCallback,
QuotesCallback,
ResolutionBackValues,
ResolutionString,
ResolveCallback,
SearchSymbolResultItem,
SearchSymbolsCallback,
ServerTimeCallback,
SubscribeBarsCallback,
TimescaleMark,
SymbolResolveExtension,
} from '../../../charting_library/datafeed-api';
import {
getErrorMessage,
logMessage,
RequestParams,
UdfErrorResponse,
} from './helpers';
import {
GetBarsResult,
HistoryProvider,
} from './history-provider';
import { IQuotesProvider } from './iquotes-provider';
import { DataPulseProvider } from './data-pulse-provider';
import { QuotesPulseProvider } from './quotes-pulse-provider';
import { SymbolsStorage } from './symbols-storage';
import { Requester } from './requester';
export interface UdfCompatibleConfiguration extends DatafeedConfiguration {
// tslint:disable:tv-variable-name
supports_search?: boolean;
supports_group_request?: boolean;
// tslint:enable:tv-variable-name
}
export interface ResolveSymbolResponse extends LibrarySymbolInfo {
s: undefined;
}
// it is hack to let's TypeScript make code flow analysis
export interface UdfSearchSymbolsResponse extends Array<SearchSymbolResultItem> {
s?: undefined;
}
export const enum Constants {
SearchItemsLimit = 30,
}
type UdfDatafeedMarkType<T extends TimescaleMark | Mark> = {
[K in keyof T]: T[K] | T[K][];
} & {
id: (string | number)[];
};
type UdfDatafeedMark = UdfDatafeedMarkType<Mark>;
type UdfDatafeedTimescaleMark = UdfDatafeedMarkType<TimescaleMark>;
function extractField<Field extends keyof Mark>(data: UdfDatafeedMark, field: Field, arrayIndex: number): Mark[Field];
function extractField<Field extends keyof TimescaleMark>(data: UdfDatafeedTimescaleMark, field: Field, arrayIndex: number): TimescaleMark[Field];
function extractField<Field extends keyof(TimescaleMark | Mark)>(data: UdfDatafeedMark | UdfDatafeedTimescaleMark, field: Field, arrayIndex: number): (TimescaleMark | Mark)[Field] {
const value = data[field];
return Array.isArray(value) ? value[arrayIndex] : value;
}
/**
* This class implements interaction with UDF-compatible datafeed.
* See UDF protocol reference at https://github.com/tradingview/charting_library/wiki/UDF
*/
export class UDFCompatibleDatafeedBase implements IExternalDatafeed, IDatafeedQuotesApi, IDatafeedChartApi {
protected _configuration: UdfCompatibleConfiguration = defaultConfiguration();
private readonly _datafeedURL: string;
private readonly _configurationReadyPromise: Promise<void>;
private _symbolsStorage: SymbolsStorage | null = null;
private readonly _historyProvider: HistoryProvider;
private readonly _dataPulseProvider: DataPulseProvider;
private readonly _quotesProvider: IQuotesProvider;
private readonly _quotesPulseProvider: QuotesPulseProvider;
private readonly _requester: Requester;
protected constructor(datafeedURL: string, quotesProvider: IQuotesProvider, requester: Requester, updateFrequency: number = 10 * 1000) {
this._datafeedURL = datafeedURL;
this._requester = requester;
this._historyProvider = new HistoryProvider(datafeedURL, this._requester);
this._quotesProvider = quotesProvider;
this._dataPulseProvider = new DataPulseProvider(this._historyProvider, updateFrequency);
this._quotesPulseProvider = new QuotesPulseProvider(this._quotesProvider);
this._configurationReadyPromise = this._requestConfiguration()
.then((configuration: UdfCompatibleConfiguration | null) => {
if (configuration === null) {
configuration = defaultConfiguration();
}
this._setupWithConfiguration(configuration);
});
}
public onReady(callback: OnReadyCallback): void {
this._configurationReadyPromise.then(() => {
callback(this._configuration);
});
}
public getQuotes(symbols: string[], onDataCallback: QuotesCallback, onErrorCallback: (msg: string) => void): void {
this._quotesProvider.getQuotes(symbols).then(onDataCallback).catch(onErrorCallback);
}
public subscribeQuotes(symbols: string[], fastSymbols: string[], onRealtimeCallback: QuotesCallback, listenerGuid: string): void {
this._quotesPulseProvider.subscribeQuotes(symbols, fastSymbols, onRealtimeCallback, listenerGuid);
}
public unsubscribeQuotes(listenerGuid: string): void {
this._quotesPulseProvider.unsubscribeQuotes(listenerGuid);
}
public calculateHistoryDepth(resolution: ResolutionString, resolutionBack: ResolutionBackValues, intervalBack: number): HistoryDepth | undefined {
return undefined;
}
public getMarks(symbolInfo: LibrarySymbolInfo, from: number, to: number, onDataCallback: GetMarksCallback<Mark>, resolution: ResolutionString): void {
if (!this._configuration.supports_marks) {
return;
}
const requestParams: RequestParams = {
symbol: symbolInfo.ticker || '',
from: from,
to: to,
resolution: resolution,
};
this._send<Mark[] | UdfDatafeedMark>('marks', requestParams)
.then((response: Mark[] | UdfDatafeedMark) => {
if (!Array.isArray(response)) {
const result: Mark[] = [];
for (let i = 0; i < response.id.length; ++i) {
result.push({
id: extractField(response, 'id', i),
time: extractField(response, 'time', i),
color: extractField(response, 'color', i),
text: extractField(response, 'text', i),
label: extractField(response, 'label', i),
labelFontColor: extractField(response, 'labelFontColor', i),
minSize: extractField(response, 'minSize', i),
});
}
response = result;
}
onDataCallback(response);
})
.catch((error?: string | Error) => {
logMessage(`UdfCompatibleDatafeed: Request marks failed: ${getErrorMessage(error)}`);
onDataCallback([]);
});
}
public getTimescaleMarks(symbolInfo: LibrarySymbolInfo, from: number, to: number, onDataCallback: GetMarksCallback<TimescaleMark>, resolution: ResolutionString): void {
if (!this._configuration.supports_timescale_marks) {
return;
}
const requestParams: RequestParams = {
symbol: symbolInfo.ticker || '',
from: from,
to: to,
resolution: resolution,
};
this._send<TimescaleMark[] | UdfDatafeedTimescaleMark>('timescale_marks', requestParams)
.then((response: TimescaleMark[] | UdfDatafeedTimescaleMark) => {
if (!Array.isArray(response)) {
const result: TimescaleMark[] = [];
for (let i = 0; i < response.id.length; ++i) {
result.push({
id: extractField(response, 'id', i),
time: extractField(response, 'time', i),
color: extractField(response, 'color', i),
label: extractField(response, 'label', i),
tooltip: extractField(response, 'tooltip', i),
});
}
response = result;
}
onDataCallback(response);
})
.catch((error?: string | Error) => {
logMessage(`UdfCompatibleDatafeed: Request timescale marks failed: ${getErrorMessage(error)}`);
onDataCallback([]);
});
}
public getServerTime(callback: ServerTimeCallback): void {
if (!this._configuration.supports_time) {
return;
}
this._send<string>('time')
.then((response: string) => {
const time = parseInt(response);
if (!isNaN(time)) {
callback(time);
}
})
.catch((error?: string | Error) => {
logMessage(`UdfCompatibleDatafeed: Fail to load server time, error=${getErrorMessage(error)}`);
});
}
public searchSymbols(userInput: string, exchange: string, symbolType: string, onResult: SearchSymbolsCallback): void {
if (this._configuration.supports_search) {
const params: RequestParams = {
limit: Constants.SearchItemsLimit,
query: userInput.toUpperCase(),
type: symbolType,
exchange: exchange,
};
this._send<UdfSearchSymbolsResponse | UdfErrorResponse>('search', params)
.then((response: UdfSearchSymbolsResponse | UdfErrorResponse) => {
if (response.s !== undefined) {
logMessage(`UdfCompatibleDatafeed: search symbols error=${response.errmsg}`);
onResult([]);
return;
}
onResult(response);
})
.catch((reason?: string | Error) => {
logMessage(`UdfCompatibleDatafeed: Search symbols for '${userInput}' failed. Error=${getErrorMessage(reason)}`);
onResult([]);
});
} else {
if (this._symbolsStorage === null) {
throw new Error('UdfCompatibleDatafeed: inconsistent configuration (symbols storage)');
}
this._symbolsStorage.searchSymbols(userInput, exchange, symbolType, Constants.SearchItemsLimit)
.then(onResult)
.catch(onResult.bind(null, []));
}
}
public resolveSymbol(symbolName: string, onResolve: ResolveCallback, onError: ErrorCallback, extension?: SymbolResolveExtension): void {
logMessage('Resolve requested');
const currencyCode = extension && extension.currencyCode;
const resolveRequestStartTime = Date.now();
function onResultReady(symbolInfo: LibrarySymbolInfo): void {
logMessage(`Symbol resolved: ${Date.now() - resolveRequestStartTime}ms`);
onResolve(symbolInfo);
}
if (!this._configuration.supports_group_request) {
const params: RequestParams = {
symbol: symbolName,
};
if (currencyCode !== undefined) {
params.currencyCode = currencyCode;
}
this._send<ResolveSymbolResponse | UdfErrorResponse>('symbols', params)
.then((response: ResolveSymbolResponse | UdfErrorResponse) => {
if (response.s !== undefined) {
onError('unknown_symbol');
} else {
onResultReady(response);
}
})
.catch((reason?: string | Error) => {
logMessage(`UdfCompatibleDatafeed: Error resolving symbol: ${getErrorMessage(reason)}`);
onError('unknown_symbol');
});
} else {
if (this._symbolsStorage === null) {
throw new Error('UdfCompatibleDatafeed: inconsistent configuration (symbols storage)');
}
this._symbolsStorage.resolveSymbol(symbolName, currencyCode).then(onResultReady).catch(onError);
}
}
public getBars(symbolInfo: LibrarySymbolInfo, resolution: ResolutionString, rangeStartDate: number, rangeEndDate: number, onResult: HistoryCallback, onError: ErrorCallback): void {
this._historyProvider.getBars(symbolInfo, resolution, rangeStartDate, rangeEndDate)
.then((result: GetBarsResult) => {
onResult(result.bars, result.meta);
})
.catch(onError);
}
public subscribeBars(symbolInfo: LibrarySymbolInfo, resolution: ResolutionString, onTick: SubscribeBarsCallback, listenerGuid: string, onResetCacheNeededCallback: () => void): void {
this._dataPulseProvider.subscribeBars(symbolInfo, resolution, onTick, listenerGuid);
}
public unsubscribeBars(listenerGuid: string): void {
this._dataPulseProvider.unsubscribeBars(listenerGuid);
}
protected _requestConfiguration(): Promise<UdfCompatibleConfiguration | null> {
return this._send<UdfCompatibleConfiguration>('config')
.catch((reason?: string | Error) => {
logMessage(`UdfCompatibleDatafeed: Cannot get datafeed configuration - use default, error=${getErrorMessage(reason)}`);
return null;
});
}
private _send<T>(urlPath: string, params?: RequestParams): Promise<T> {
return this._requester.sendRequest<T>(this._datafeedURL, urlPath, params);
}
private _setupWithConfiguration(configurationData: UdfCompatibleConfiguration): void {
this._configuration = configurationData;
if (configurationData.exchanges === undefined) {
configurationData.exchanges = [];
}
if (!configurationData.supports_search && !configurationData.supports_group_request) {
throw new Error('Unsupported datafeed configuration. Must either support search, or support group request');
}
if (configurationData.supports_group_request || !configurationData.supports_search) {
this._symbolsStorage = new SymbolsStorage(this._datafeedURL, configurationData.supported_resolutions || [], this._requester);
}
logMessage(`UdfCompatibleDatafeed: Initialized with ${JSON.stringify(configurationData)}`);
}
}
function defaultConfiguration(): UdfCompatibleConfiguration {
return {
supports_search: false,
supports_group_request: true,
supported_resolutions: [
'1' as ResolutionString,
'5' as ResolutionString,
'15' as ResolutionString,
'30' as ResolutionString,
'60' as ResolutionString,
'1D' as ResolutionString,
'1W' as ResolutionString,
'1M' as ResolutionString,
],
supports_marks: false,
supports_timescale_marks: false,
};
}