symbols-storage.ts 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267
  1. import {
  2. LibrarySymbolInfo,
  3. SearchSymbolResultItem,
  4. } from '../../../charting_library/datafeed-api';
  5. import {
  6. getErrorMessage,
  7. logMessage,
  8. } from './helpers';
  9. import { Requester } from './requester';
  10. interface SymbolInfoMap {
  11. [symbol: string]: LibrarySymbolInfo | undefined;
  12. }
  13. interface ExchangeDataResponseOptionalValues {
  14. 'ticker': string;
  15. 'minmov2': number;
  16. 'minmove2': number;
  17. 'minmov': number;
  18. 'minmovement': number;
  19. 'supported-resolutions': string[];
  20. 'force-session-rebuild': boolean;
  21. 'has-intraday': boolean;
  22. 'has-daily': boolean;
  23. 'has-weekly-and-monthly': boolean;
  24. 'has-empty-bars': boolean;
  25. 'has-no-volume': boolean;
  26. 'intraday-multipliers': string[];
  27. 'volume-precision': number;
  28. }
  29. interface ExchangeDataResponseMandatoryValues {
  30. 'type': string;
  31. 'timezone': LibrarySymbolInfo['timezone'];
  32. 'description': string;
  33. 'exchange-listed': string;
  34. 'exchange-traded': string;
  35. 'session-regular': string;
  36. 'fractional': boolean;
  37. 'pricescale': number;
  38. }
  39. // Here is some black magic with types to get compile-time checks of names and types
  40. type ValueOrArray<T> = T | T[];
  41. type ExchangeDataResponse =
  42. {
  43. symbol: string[];
  44. } &
  45. {
  46. [K in keyof ExchangeDataResponseMandatoryValues]: ValueOrArray<ExchangeDataResponseMandatoryValues[K]>;
  47. } &
  48. {
  49. [K in keyof ExchangeDataResponseOptionalValues]?: ValueOrArray<ExchangeDataResponseOptionalValues[K]>;
  50. };
  51. function extractField<Field extends keyof ExchangeDataResponseMandatoryValues>(data: ExchangeDataResponse, field: Field, arrayIndex: number): ExchangeDataResponseMandatoryValues[Field];
  52. function extractField<Field extends keyof ExchangeDataResponseOptionalValues>(data: ExchangeDataResponse, field: Field, arrayIndex: number): ExchangeDataResponseOptionalValues[Field] | undefined;
  53. function extractField<Field extends keyof ExchangeDataResponseMandatoryValues>(data: ExchangeDataResponse, field: Field, arrayIndex: number): (ExchangeDataResponseMandatoryValues & ExchangeDataResponseOptionalValues)[Field] | undefined {
  54. const value = data[field];
  55. return Array.isArray(value) ? value[arrayIndex] : value;
  56. }
  57. export class SymbolsStorage {
  58. private readonly _exchangesList: string[] = ['NYSE', 'FOREX', 'AMEX'];
  59. private readonly _symbolsInfo: SymbolInfoMap = {};
  60. private readonly _symbolsList: string[] = [];
  61. private readonly _datafeedUrl: string;
  62. private readonly _readyPromise: Promise<void>;
  63. private readonly _datafeedSupportedResolutions: string[];
  64. private readonly _requester: Requester;
  65. public constructor(datafeedUrl: string, datafeedSupportedResolutions: string[], requester: Requester) {
  66. this._datafeedUrl = datafeedUrl;
  67. this._datafeedSupportedResolutions = datafeedSupportedResolutions;
  68. this._requester = requester;
  69. this._readyPromise = this._init();
  70. this._readyPromise.catch((error: Error) => {
  71. // seems it is impossible
  72. console.error(`SymbolsStorage: Cannot init, error=${error.toString()}`);
  73. });
  74. }
  75. // BEWARE: this function does not consider symbol's exchange
  76. public resolveSymbol(symbolName: string): Promise<LibrarySymbolInfo> {
  77. return this._readyPromise.then(() => {
  78. const symbolInfo = this._symbolsInfo[symbolName];
  79. if (symbolInfo === undefined) {
  80. return Promise.reject('invalid symbol');
  81. }
  82. return Promise.resolve(symbolInfo);
  83. });
  84. }
  85. public searchSymbols(searchString: string, exchange: string, symbolType: string, maxSearchResults: number): Promise<SearchSymbolResultItem[]> {
  86. interface WeightedItem {
  87. symbolInfo: LibrarySymbolInfo;
  88. weight: number;
  89. }
  90. return this._readyPromise.then(() => {
  91. const weightedResult: WeightedItem[] = [];
  92. const queryIsEmpty = searchString.length === 0;
  93. searchString = searchString.toUpperCase();
  94. for (const symbolName of this._symbolsList) {
  95. const symbolInfo = this._symbolsInfo[symbolName];
  96. if (symbolInfo === undefined) {
  97. continue;
  98. }
  99. if (symbolType.length > 0 && symbolInfo.type !== symbolType) {
  100. continue;
  101. }
  102. if (exchange && exchange.length > 0 && symbolInfo.exchange !== exchange) {
  103. continue;
  104. }
  105. const positionInName = symbolInfo.name.toUpperCase().indexOf(searchString);
  106. const positionInDescription = symbolInfo.description.toUpperCase().indexOf(searchString);
  107. if (queryIsEmpty || positionInName >= 0 || positionInDescription >= 0) {
  108. const alreadyExists = weightedResult.some((item: WeightedItem) => item.symbolInfo === symbolInfo);
  109. if (!alreadyExists) {
  110. const weight = positionInName >= 0 ? positionInName : 8000 + positionInDescription;
  111. weightedResult.push({ symbolInfo: symbolInfo, weight: weight });
  112. }
  113. }
  114. }
  115. const result = weightedResult
  116. .sort((item1: WeightedItem, item2: WeightedItem) => item1.weight - item2.weight)
  117. .slice(0, maxSearchResults)
  118. .map((item: WeightedItem) => {
  119. const symbolInfo = item.symbolInfo;
  120. return {
  121. symbol: symbolInfo.name,
  122. full_name: symbolInfo.full_name,
  123. description: symbolInfo.description,
  124. exchange: symbolInfo.exchange,
  125. params: [],
  126. type: symbolInfo.type,
  127. ticker: symbolInfo.name,
  128. };
  129. });
  130. return Promise.resolve(result);
  131. });
  132. }
  133. private _init(): Promise<void> {
  134. interface BooleanMap {
  135. [key: string]: boolean | undefined;
  136. }
  137. const promises: Promise<void>[] = [];
  138. const alreadyRequestedExchanges: BooleanMap = {};
  139. for (const exchange of this._exchangesList) {
  140. if (alreadyRequestedExchanges[exchange]) {
  141. continue;
  142. }
  143. alreadyRequestedExchanges[exchange] = true;
  144. promises.push(this._requestExchangeData(exchange));
  145. }
  146. return Promise.all(promises)
  147. .then(() => {
  148. this._symbolsList.sort();
  149. logMessage('SymbolsStorage: All exchanges data loaded');
  150. });
  151. }
  152. private _requestExchangeData(exchange: string): Promise<void> {
  153. return new Promise((resolve: () => void, reject: (error: Error) => void) => {
  154. this._requester.sendRequest<ExchangeDataResponse>(this._datafeedUrl, 'symbol_info', { group: exchange })
  155. .then((response: ExchangeDataResponse) => {
  156. try {
  157. this._onExchangeDataReceived(exchange, response);
  158. } catch (error) {
  159. reject(error);
  160. return;
  161. }
  162. resolve();
  163. })
  164. .catch((reason?: string | Error) => {
  165. logMessage(`SymbolsStorage: Request data for exchange '${exchange}' failed, reason=${getErrorMessage(reason)}`);
  166. resolve();
  167. });
  168. });
  169. }
  170. private _onExchangeDataReceived(exchange: string, data: ExchangeDataResponse): void {
  171. let symbolIndex = 0;
  172. try {
  173. const symbolsCount = data.symbol.length;
  174. const tickerPresent = data.ticker !== undefined;
  175. for (; symbolIndex < symbolsCount; ++symbolIndex) {
  176. const symbolName = data.symbol[symbolIndex];
  177. const listedExchange = extractField(data, 'exchange-listed', symbolIndex);
  178. const tradedExchange = extractField(data, 'exchange-traded', symbolIndex);
  179. const fullName = tradedExchange + ':' + symbolName;
  180. const ticker = tickerPresent ? (extractField(data, 'ticker', symbolIndex) as string) : symbolName;
  181. const symbolInfo: LibrarySymbolInfo = {
  182. ticker: ticker,
  183. name: symbolName,
  184. base_name: [listedExchange + ':' + symbolName],
  185. full_name: fullName,
  186. listed_exchange: listedExchange,
  187. exchange: tradedExchange,
  188. description: extractField(data, 'description', symbolIndex),
  189. has_intraday: definedValueOrDefault(extractField(data, 'has-intraday', symbolIndex), false),
  190. has_no_volume: definedValueOrDefault(extractField(data, 'has-no-volume', symbolIndex), false),
  191. minmov: extractField(data, 'minmovement', symbolIndex) || extractField(data, 'minmov', symbolIndex) || 0,
  192. minmove2: extractField(data, 'minmove2', symbolIndex) || extractField(data, 'minmov2', symbolIndex),
  193. fractional: extractField(data, 'fractional', symbolIndex),
  194. pricescale: extractField(data, 'pricescale', symbolIndex),
  195. type: extractField(data, 'type', symbolIndex),
  196. session: extractField(data, 'session-regular', symbolIndex),
  197. timezone: extractField(data, 'timezone', symbolIndex),
  198. supported_resolutions: definedValueOrDefault(extractField(data, 'supported-resolutions', symbolIndex), this._datafeedSupportedResolutions),
  199. force_session_rebuild: extractField(data, 'force-session-rebuild', symbolIndex),
  200. has_daily: definedValueOrDefault(extractField(data, 'has-daily', symbolIndex), true),
  201. intraday_multipliers: definedValueOrDefault(extractField(data, 'intraday-multipliers', symbolIndex), ['1', '5', '15', '30', '60']),
  202. has_weekly_and_monthly: extractField(data, 'has-weekly-and-monthly', symbolIndex),
  203. has_empty_bars: extractField(data, 'has-empty-bars', symbolIndex),
  204. volume_precision: definedValueOrDefault(extractField(data, 'volume-precision', symbolIndex), 0),
  205. };
  206. this._symbolsInfo[ticker] = symbolInfo;
  207. this._symbolsInfo[symbolName] = symbolInfo;
  208. this._symbolsInfo[fullName] = symbolInfo;
  209. this._symbolsList.push(symbolName);
  210. }
  211. } catch (error) {
  212. throw new Error(`SymbolsStorage: API error when processing exchange ${exchange} symbol #${symbolIndex} (${data.symbol[symbolIndex]}): ${error.message}`);
  213. }
  214. }
  215. }
  216. function definedValueOrDefault<T>(value: T | undefined, defaultValue: T): T {
  217. return value !== undefined ? value : defaultValue;
  218. }