diff --git a/apps/api/src/services/data-provider/data-provider.module.ts b/apps/api/src/services/data-provider/data-provider.module.ts index 71b54f01e..0881fd036 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -8,6 +8,7 @@ import { FinancialModelingPrepService } from '@ghostfolio/api/services/data-prov import { GhostfolioService } from '@ghostfolio/api/services/data-provider/ghostfolio/ghostfolio.service'; import { GoogleSheetsService } from '@ghostfolio/api/services/data-provider/google-sheets/google-sheets.service'; import { ManualService } from '@ghostfolio/api/services/data-provider/manual/manual.service'; +import { MoexService } from '@ghostfolio/api/services/data-provider/moex/moex.service'; import { RapidApiService } from '@ghostfolio/api/services/data-provider/rapid-api/rapid-api.service'; import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; import { MarketDataModule } from '@ghostfolio/api/services/market-data/market-data.module'; @@ -43,6 +44,7 @@ import { DataProviderService } from './data-provider.service'; ManualService, RapidApiService, YahooFinanceService, + MoexService, { inject: [ AlphaVantageService, @@ -53,7 +55,8 @@ import { DataProviderService } from './data-provider.service'; GoogleSheetsService, ManualService, RapidApiService, - YahooFinanceService + YahooFinanceService, + MoexService ], provide: 'DataProviderInterfaces', useFactory: ( @@ -65,7 +68,8 @@ import { DataProviderService } from './data-provider.service'; googleSheetsService, manualService, rapidApiService, - yahooFinanceService + yahooFinanceService, + moexService ) => [ alphaVantageService, coinGeckoService, @@ -75,11 +79,17 @@ import { DataProviderService } from './data-provider.service'; googleSheetsService, manualService, rapidApiService, - yahooFinanceService + yahooFinanceService, + moexService ] }, YahooFinanceDataEnhancerService ], - exports: [DataProviderService, ManualService, YahooFinanceService] + exports: [ + DataProviderService, + ManualService, + YahooFinanceService, + MoexService + ] }) export class DataProviderModule {} diff --git a/apps/api/src/services/data-provider/moex/moex.service.ts b/apps/api/src/services/data-provider/moex/moex.service.ts new file mode 100644 index 000000000..6f100c7a8 --- /dev/null +++ b/apps/api/src/services/data-provider/moex/moex.service.ts @@ -0,0 +1,539 @@ +import { + DataProviderInterface, + GetDividendsParams, + GetHistoricalParams, + GetQuotesParams, + GetSearchParams +} from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; +import { YahooFinanceService } from '@ghostfolio/api/services/data-provider/yahoo-finance/yahoo-finance.service'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { + DataProviderInfo, + LookupResponse +} from '@ghostfolio/common/interfaces'; + +import { Injectable, Logger } from '@nestjs/common'; +import { $Enums, SymbolProfile } from '@prisma/client'; +import { + subYears, + format, + startOfYesterday, + subDays, + differenceInDays, + addYears, + differenceInMilliseconds +} from 'date-fns'; +import { createMoexCLient } from 'moex-iss-api-client'; +import { + IGetSecuritiesParams, + TGetSecuritiesParamsGroupBy +} from 'moex-iss-api-client/dist/client/security/requestTypes'; +import { ISecuritiesResponse } from 'moex-iss-api-client/dist/client/security/responseTypes'; + +const moexClient = createMoexCLient(); + +declare interface ResponseData { + columns: string[]; + data: (string | number | null)[][]; +} + +declare interface Response { + data: T; + issError: string; +} + +function response_data_to_map( + response: ResponseData, + keyColumnName: string +): Map> { + const result = new Map>(); + + response.data.forEach((x) => { + const item = new Map(); + response.columns.forEach((c, i) => item.set(c, x[i])); + result.set(item.get(keyColumnName), item); + }); + + return result; +} + +function getCurrency(currency: string): string { + if (currency === 'SUR' || currency === 'RUR') return 'RUB'; + + return currency; +} + +/// So, we try to guess sectors of security by looking into indexes, +/// in which this security was put by MOEX +const indexToSectorMapping = new Map([ + ['MOEXOG', ['Energy']], // MOEX Oil and Gas Index + ['MOEXEU', ['Utilities']], // MOEX Electric Utilities Index + ['MOEXTL', ['Communication Services']], // MOEX Telecommunication Index + ['MOEXMM', ['Basic Materials', 'Industrial']], // MOEX Metals and Mining Index + ['MOEXFN', ['Financial Services']], // MOEX Financials Index + ['MOEXCN', ['Consumer Defensive', 'Consumer Cyclical', 'Healthcare']], // MOEX Consumer Index + ['MOEXCH', ['Basic Materials']], // MOEX Chemicals Index + ['MOEXIT', ['Technology']], // MOEX Information Technologies Index + ['MOEXRE', ['Real Estate']], // MOEX Real Estate Index + ['MOEXTN', ['Consumer Cyclical', 'Industrial']] // MOEX Transportation Index +]); + +async function getSectors( + symbol: string +): Promise<{ name: string; weight: number }[]> { + const indicesResponse: Response<{ indices: ResponseData }> = + await moexClient.security.getSecurityIndexes({ security: symbol }); + const errorMessage = indicesResponse.issError; + if (errorMessage) { + Logger.warn(errorMessage, 'MoexService.getSectors'); + return []; + } + + const indices = response_data_to_map(indicesResponse.data.indices, 'SECID'); + + const sectorIncluded = new Set(); + const sectors = new Array<{ name: string; weight: number }>(); + + for (const [indexCode, indexSectors] of indexToSectorMapping.entries()) { + const index = indices.get(indexCode); + if (!index) { + continue; + } + + if (sectorIncluded.has(indexCode)) { + continue; + } + + sectorIncluded.add(indexCode); + indexSectors.forEach((x) => sectors.push({ name: x, weight: 1.0 })); + } + + return sectors; +} + +async function getDividendsFromMoex({ + from, + symbol, + to +}: GetDividendsParams): Promise<{ + [date: string]: IDataProviderHistoricalResponse; +}> { + const response: Response<{ dividends: ResponseData }> = + await moexClient.request(`securities/${symbol}/dividends`); + if (response.issError) { + Logger.warn(response.issError, 'MoexService.getDividends'); + return {}; + } + + const dividends = response_data_to_map( + response.data.dividends, + 'registryclosedate' + ); + const result: { + [date: string]: IDataProviderHistoricalResponse; + } = {}; + + for (const [key, value] of dividends.entries()) { + const date = new Date(key); + + if (date < from || date > to) { + continue; + } + + const price = value.get('value'); + if (typeof price === 'number') result[key] = { marketPrice: price }; + } + + return result; +} + +async function readBatchedResponse( + getNextBatch: (start: number) => Promise>, + extractor: (batch: Response) => ResponseData +): Promise { + let batch: ResponseData; + const wholeResponse: ResponseData = { columns: [], data: [] }; + + do { + const response: Response = await getNextBatch(wholeResponse.data.length); + if (response === undefined) { + break; + } + if (response.issError) { + Logger.warn(response.issError, 'MoexService.readBatchedResponse'); + break; + } + + batch = extractor(response); + if (wholeResponse.columns.length === 0) { + wholeResponse.columns = batch.columns; + } + + wholeResponse.data = [...wholeResponse.data, ...batch.data]; + } while (batch.data.length > 0); + + return wholeResponse; +} + +function getYahooSymbolFromMoex(symbol: string): string { + return `${symbol}.ME`; +} + +interface SecurityTypeMap { + [key: string]: [$Enums.AssetClass?, $Enums.AssetSubClass?]; +} + +/// MOEX security types were obtained here: https://iss.moex.com/iss/index.json (add `?lang=en` for english) +/// | id | security_type_name | ghostfolio_assetclass | ghostfolio_assetsubclass | +/// | ---- | --------------------- | --------------------- | ------------------------ | +/// | 1 | preferred_share | EQUITY | STOCK | +/// | 2 | corporate_bond | FIXED_INCOME | BOND | +/// | 3 | common_share | EQUITY | STOCK | +/// | 4 | cb_bond | FIXED_INCOME | BOND | +/// | 5 | currency | LIQUIDITY | CASH | +/// | 6 | futures | COMMODITY | | +/// | 7 | public_ppif | EQUITY | MUTUALFUND | +/// | 8 | interval_ppif | EQUITY | MUTUALFUND | +/// | 9 | private_ppif | EQUITY | MUTUALFUND | +/// | 10 | state_bond | FIXED_INCOME | BOND | +/// | 41 | subfederal_bond | FIXED_INCOME | BOND | +/// | 42 | ifi_bond | FIXED_INCOME | BOND | +/// | 43 | exchange_bond | FIXED_INCOME | BOND | +/// | 44 | stock_index | | | +/// | 45 | municipal_bond | FIXED_INCOME | BOND | +/// | 51 | depositary_receipt | EQUITY | STOCK | +/// | 52 | option | COMMODITY | | +/// | 53 | rts_index | | | +/// | 54 | ofz_bond | FIXED_INCOME | BOND | +/// | 55 | etf_ppif | EQUITY | ETF | +/// | 57 | stock_mortgage | REAL_ESTATE | | +/// | 58 | gold_metal | LIQUIDITY | PRECIOUS_METAL | +/// | 59 | silver_metal | LIQUIDITY | PRECIOUS_METAL | +/// | 60 | euro_bond | FIXED_INCOME | BOND | +/// | 62 | currency_futures | LIQUIDITY | CASH | +/// | 63 | stock_deposit | LIQUIDITY | CASH | +/// | 73 | currency_fixing | LIQUIDITY | CASH | +/// | 74 | exchange_ppif | EQUITY | ETF | +/// | 75 | currency_index | LIQUIDITY | CASH | +/// | 76 | currency_wap | LIQUIDITY | CASH | +/// | 78 | non_exchange_bond | FIXED_INCOME | BOND | +/// | 84 | stock_index_eq | | | +/// | 85 | stock_index_fi | | | +/// | 86 | stock_index_mx | | | +/// | 87 | stock_index_ie | | | +/// | 88 | stock_index_if | | | +/// | 89 | stock_index_ci | | | +/// | 90 | stock_index_im | | | +/// | 1030 | stock_index_namex | | | +/// | 1031 | option_on_shares | EQUITY | STOCK | +/// | 1034 | stock_index_rusfar | | | +/// | 1155 | stock_index_pf | | | +/// | 1291 | option_on_currency | | | +/// | 1293 | option_on_indices | | | +/// | 1295 | option_on_commodities | COMMODITY | | +/// | 1337 | futures_spread | COMMODITY | | +/// | 1338 | futures_collateral | COMMODITY | | +/// | 1347 | currency_otcindices | LIQUIDITY | CASH | +/// | 1352 | other_metal | COMMODITY | PRECIOUS_METAL | +/// | 1403 | stock_index_ri | | | +const securityTypeMap: SecurityTypeMap = { + preferred_share: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], + corporate_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], + common_share: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], + cb_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], + currency: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], + futures: [$Enums.AssetClass.COMMODITY], + public_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.MUTUALFUND], + interval_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.MUTUALFUND], + private_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.MUTUALFUND], + state_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], + subfederal_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], + ifi_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], + exchange_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], + municipal_bond: [$Enums.AssetClass.FIXED_INCOME, $Enums.AssetSubClass.BOND], + depositary_receipt: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], + option: [$Enums.AssetClass.COMMODITY], + etf_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.ETF], + stock_mortgage: [$Enums.AssetClass.REAL_ESTATE], + gold_metal: [ + $Enums.AssetClass.LIQUIDITY, + $Enums.AssetSubClass.PRECIOUS_METAL + ], + silver_metal: [ + $Enums.AssetClass.LIQUIDITY, + $Enums.AssetSubClass.PRECIOUS_METAL + ], + currency_futures: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], + stock_deposit: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], + currency_fixing: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], + exchange_ppif: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.ETF], + currency_index: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], + currency_wap: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], + non_exchange_bond: [ + $Enums.AssetClass.FIXED_INCOME, + $Enums.AssetSubClass.BOND + ], + option_on_shares: [$Enums.AssetClass.EQUITY, $Enums.AssetSubClass.STOCK], + option_on_commodities: [$Enums.AssetClass.COMMODITY], + futures_spread: [$Enums.AssetClass.COMMODITY], + futures_collateral: [$Enums.AssetClass.COMMODITY], + currency_otcindices: [$Enums.AssetClass.LIQUIDITY, $Enums.AssetSubClass.CASH], + other_metal: [ + $Enums.AssetClass.COMMODITY, + $Enums.AssetSubClass.PRECIOUS_METAL + ] +}; + +@Injectable() +export class MoexService implements DataProviderInterface { + public constructor( + private readonly yahooFinanceService: YahooFinanceService + ) {} + + canHandle(): boolean { + return true; + } + + async getAssetProfile({ + symbol + }: { + symbol: string; + }): Promise> { + const securitySpecificationResponse = + await moexClient.security.getSecuritySpecification({ security: symbol }); + const errorMessage = securitySpecificationResponse.issError; + if (errorMessage) { + Logger.warn(errorMessage, 'MoexService.getAssetProfile'); + return {}; + } + + const securitySpecification = response_data_to_map( + securitySpecificationResponse.data.description, + 'name' + ); + + const issueDate = securitySpecification.get('ISSUEDATE'); + const faceunit = securitySpecification.get('FACEUNIT'); + const isin = securitySpecification.get('ISIN'); + const latname = securitySpecification.get('LATNAME'); + const shortname = securitySpecification.get('SHORTNAME'); + const name = securitySpecification.get('NAME'); + const secid = securitySpecification.get('SECID'); + const type = securitySpecification.get('TYPE'); + const [assetClass, assetSubClass] = + securityTypeMap[type.get('value').toString()] ?? []; + + return { + assetClass: assetClass, + assetSubClass: assetSubClass, + createdAt: issueDate ? new Date(issueDate.get('value')) : new Date(), + currency: faceunit + ? getCurrency(faceunit.get('value').toString()) + : 'RUB', + dataSource: this.getName(), + id: symbol, + isin: isin ? isin.get('value').toString() : null, + name: (latname ?? shortname ?? name ?? secid).get('value').toString(), + sectors: await getSectors(symbol), + symbol: symbol, + countries: isin + ? [ + { + code: isin.get('value').toString().substring(0, 2), + weight: 1 + } + ] + : null + }; + } + + getDataProviderInfo(): DataProviderInfo { + return { + isPremium: false + }; + } + + // MOEX endpoint for dividends isn't documented and sometimes doesn't return newer dividends. + // YAHOO endpoint for dividends sometimes doesn't respect date filters. + // So, we'll requests dividends for 2 years more from both providers and merge data. + // If dividends date from MOEX and YAHOO differs for 2 days or less, we'll assume it's the same payout given amount is the same. + // Payouts considered the same if they differ less that 1/100 of the currency. + async getDividends({ from, symbol, to }: GetDividendsParams): Promise<{ + [date: string]: IDataProviderHistoricalResponse; + }> { + const twoYearsAgo = subYears(from, 2); + const twoYearsAhead = addYears(to, 2); + const [dividends, dividendsFromYahoo] = await Promise.all([ + getDividendsFromMoex({ from: twoYearsAgo, symbol, to: twoYearsAhead }), + this.yahooFinanceService.getDividends({ + from: twoYearsAgo, + symbol: getYahooSymbolFromMoex(symbol), + to: twoYearsAhead + }) + ]); + + const dateAlmostTheSame = (x: Date, y: Date) => + Math.abs(differenceInDays(x, y)) <= 2; + const payoutsAlmostTheSame = (x: number, y: number) => + 100 * Math.abs(x - y) < 1; + + for (const [yahooDateStr, yahooDividends] of Object.entries( + dividendsFromYahoo + )) { + const yahooDate = new Date(yahooDateStr); + const sameDividendIndex = Object.entries(dividends).findIndex( + (x) => + dateAlmostTheSame(new Date(x[0]), yahooDate) && + payoutsAlmostTheSame(x[1].marketPrice, yahooDividends.marketPrice) + ); + if (sameDividendIndex === -1) { + dividends[yahooDateStr] = yahooDividends; + } + } + + const result: { [date: string]: IDataProviderHistoricalResponse } = {}; + + Object.entries(dividends) + .map( + ([dateStr, dividends]) => + [dateStr, dividends, new Date(dateStr)] as [ + string, + IDataProviderHistoricalResponse, + Date + ] + ) + .filter(([, , date]) => date >= from) + .filter(([, , date]) => date <= to) + .sort(([, , a], [, , b]) => differenceInMilliseconds(a, b)) + .forEach(([dateStr, dividends]) => (result[dateStr] = dividends)); + + return result; + } + + async getHistorical({ from, symbol, to }: GetHistoricalParams): Promise<{ + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + }> { + const params: Record = { + sort_order: 'desc', + marketprice_board: '1', + from: format(from, 'yyyy-MM-dd'), + to: format(to, 'yyyy-MM-dd') + }; + + const historyResponse = await readBatchedResponse<{ + history: ResponseData; + }>( + async (x) => { + params['start'] = x; + return await moexClient.request( + `history/engines/stock/markets/shares/securities/${symbol}`, + params + ); + }, + (x) => x.data.history + ); + + const history = response_data_to_map(historyResponse, 'TRADEDATE'); + + const result: { + [symbol: string]: { [date: string]: IDataProviderHistoricalResponse }; + } = {}; + result[symbol] = {}; + + for (const [key, value] of history.entries()) { + const price = value.get('LEGALCLOSEPRICE'); + if (typeof price === 'number') + result[symbol][key] = { marketPrice: price }; + } + + return result; + } + + getMaxNumberOfSymbolsPerRequest?(): number { + return 1; + } + + getName(): $Enums.DataSource { + return $Enums.DataSource.MOEX; + } + + async getQuotes({ + symbols + }: GetQuotesParams): Promise<{ [symbol: string]: IDataProviderResponse }> { + const result: { [symbol: string]: IDataProviderResponse } = {}; + + for (const symbol of symbols) { + const profile = await this.getAssetProfile({ symbol: symbol }); + + for ( + let date = startOfYesterday(); + !result[symbol]; + date = subDays(date, 1) + ) { + const history = await this.getHistorical({ + from: date, + to: date, + symbol: symbol + }); + + for (const [, v] of Object.entries(history[symbol])) { + result[symbol] = { + currency: profile.currency, + dataSource: profile.dataSource, + marketPrice: v.marketPrice, + marketState: 'closed' + }; + } + } + } + + return result; + } + + getTestSymbol(): string { + return 'SBER'; + } + + async search({ query }: GetSearchParams): Promise { + // MOEX doesn't support search for queries less than 3 symbols + if (query.length < 3) { + return { items: [] }; + } + + const params: IGetSecuritiesParams & TGetSecuritiesParamsGroupBy = { + q: query + }; + + const searchResponse = await readBatchedResponse( + async (x) => { + params['start'] = x; + return await moexClient.security.getSecurities(params); + }, + (x) => x.data.securities + ); + + const search = response_data_to_map(searchResponse, 'secid'); + + const result: LookupResponse = { items: [] }; + for (const k of search.keys()) { + if (typeof k != 'string') { + continue; + } + const profile = await this.getAssetProfile({ symbol: k }); + result.items.push({ + assetClass: profile.assetClass, + assetSubClass: profile.assetSubClass, + currency: profile.currency, + dataProviderInfo: this.getDataProviderInfo(), + dataSource: this.getName(), + name: profile.name, + symbol: k + }); + } + + return result; + } +} diff --git a/package-lock.json b/package-lock.json index 58aa36721..086480f85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,6 +71,7 @@ "jsonpath": "1.1.1", "lodash": "4.17.21", "marked": "15.0.4", + "moex-iss-api-client": "0.4.2", "ms": "3.0.0-canary.1", "ng-extract-i18n-merge": "2.15.1", "ngx-device-detector": "9.0.0", @@ -16129,7 +16130,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "devOptional": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -17871,7 +17871,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "devOptional": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -20325,7 +20324,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", - "devOptional": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -26489,6 +26487,26 @@ "pathe": "^2.0.1" } }, + "node_modules/moex-iss-api-client": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/moex-iss-api-client/-/moex-iss-api-client-0.4.2.tgz", + "integrity": "sha512-gm0nI/d0aTprLhX5K8+3CD/S741juBSsG3ZlU1fkofOxPLyyGnIjrxxmHaCopmwoXG7OCeyP7Qm7QkImbKh3Ew==", + "license": "MIT", + "dependencies": { + "axios": "^0.28.0" + } + }, + "node_modules/moex-iss-api-client/node_modules/axios": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.1.tgz", + "integrity": "sha512-iUcGA5a7p0mVb4Gm/sy+FSECNkPFT4y7wt6OM/CDpO/OnNCvSs3PoMG8ibrC9jRoGYU0gUK5pXVC4NPXq6lHRQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -29436,7 +29454,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, "license": "MIT" }, "node_modules/prr": { diff --git a/package.json b/package.json index c3ed868f8..16acee491 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "jsonpath": "1.1.1", "lodash": "4.17.21", "marked": "15.0.4", + "moex-iss-api-client": "0.4.2", "ms": "3.0.0-canary.1", "ng-extract-i18n-merge": "2.15.1", "ngx-device-detector": "9.0.0", diff --git a/prisma/migrations/20240501071657_added_moex_to_data_sources/migration.sql b/prisma/migrations/20240501071657_added_moex_to_data_sources/migration.sql new file mode 100644 index 000000000..639e7aa9b --- /dev/null +++ b/prisma/migrations/20240501071657_added_moex_to_data_sources/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "DataSource" ADD VALUE 'MOEX'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b063b1d89..6376215dd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -307,6 +307,7 @@ enum DataSource { MANUAL RAPID_API YAHOO + MOEX } enum MarketDataState {