diff --git a/CHANGELOG.md b/CHANGELOG.md index d2ee88e6..19a35437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the _Yahoo Finance_ data enhancer for countries, sectors and urls + ### Changed - Enabled the configuration to immediately remove queue jobs on complete diff --git a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts index 481bd0cc..88487bfb 100644 --- a/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts +++ b/apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts @@ -33,7 +33,8 @@ export class AlphaVantageService implements DataProviderInterface { aSymbol: string ): Promise> { return { - dataSource: this.getName() + dataSource: this.getName(), + symbol: aSymbol }; } diff --git a/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts b/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts index 9d4c0704..d7b7d00a 100644 --- a/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts +++ b/apps/api/src/services/data-provider/data-enhancer/data-enhancer.module.ts @@ -1,15 +1,27 @@ +import { ConfigurationModule } from '@ghostfolio/api/services/configuration.module'; +import { CryptocurrencyModule } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.module'; import { TrackinsightDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/trackinsight/trackinsight.service'; +import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service'; import { Module } from '@nestjs/common'; @Module({ - exports: ['DataEnhancers', TrackinsightDataEnhancerService], + exports: [ + 'DataEnhancers', + TrackinsightDataEnhancerService, + YahooFinanceDataEnhancerService + ], + imports: [ConfigurationModule, CryptocurrencyModule], providers: [ + TrackinsightDataEnhancerService, + YahooFinanceDataEnhancerService, { - inject: [TrackinsightDataEnhancerService], + inject: [ + TrackinsightDataEnhancerService, + YahooFinanceDataEnhancerService + ], provide: 'DataEnhancers', - useFactory: (trackinsight) => [trackinsight] - }, - TrackinsightDataEnhancerService + useFactory: (trackinsight, yahooFinance) => [trackinsight, yahooFinance] + } ] }) export class DataEnhancerModule {} diff --git a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts index 4e76d7ed..ec68dd2e 100644 --- a/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts +++ b/apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts @@ -1,11 +1,13 @@ import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; +import { Injectable } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import bent from 'bent'; const getJSON = bent('json'); +@Injectable() export class TrackinsightDataEnhancerService implements DataEnhancerInterface { private static baseUrl = 'https://data.trackinsight.com'; private static countries = require('countries-list/dist/countries.json'); diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.spec.ts similarity index 60% rename from apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts rename to apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.spec.ts index e18b6b58..1dd7f0f0 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.spec.ts +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.spec.ts @@ -1,7 +1,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; -import { YahooFinanceService } from './yahoo-finance.service'; +import { YahooFinanceDataEnhancerService } from './yahoo-finance.service'; jest.mock( '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service', @@ -25,16 +25,16 @@ jest.mock( } ); -describe('YahooFinanceService', () => { +describe('YahooFinanceDataEnhancerService', () => { let configurationService: ConfigurationService; let cryptocurrencyService: CryptocurrencyService; - let yahooFinanceService: YahooFinanceService; + let yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService; beforeAll(async () => { configurationService = new ConfigurationService(); cryptocurrencyService = new CryptocurrencyService(); - yahooFinanceService = new YahooFinanceService( + yahooFinanceDataEnhancerService = new YahooFinanceDataEnhancerService( configurationService, cryptocurrencyService ); @@ -42,25 +42,37 @@ describe('YahooFinanceService', () => { it('convertFromYahooFinanceSymbol', async () => { expect( - await yahooFinanceService.convertFromYahooFinanceSymbol('BRK-B') + await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol( + 'BRK-B' + ) ).toEqual('BRK-B'); expect( - await yahooFinanceService.convertFromYahooFinanceSymbol('BTC-USD') + await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol( + 'BTC-USD' + ) ).toEqual('BTCUSD'); expect( - await yahooFinanceService.convertFromYahooFinanceSymbol('EURUSD=X') + await yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol( + 'EURUSD=X' + ) ).toEqual('EURUSD'); }); it('convertToYahooFinanceSymbol', async () => { expect( - await yahooFinanceService.convertToYahooFinanceSymbol('BTCUSD') + await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( + 'BTCUSD' + ) ).toEqual('BTC-USD'); expect( - await yahooFinanceService.convertToYahooFinanceSymbol('DOGEUSD') + await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( + 'DOGEUSD' + ) ).toEqual('DOGE-USD'); expect( - await yahooFinanceService.convertToYahooFinanceSymbol('USDCHF') + await yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( + 'USDCHF' + ) ).toEqual('USDCHF=X'); }); }); diff --git a/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts new file mode 100644 index 00000000..32bcd42c --- /dev/null +++ b/apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts @@ -0,0 +1,325 @@ +import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; +import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; +import { UNKNOWN_KEY } from '@ghostfolio/common/config'; +import { isCurrency } from '@ghostfolio/common/helper'; +import { Injectable, Logger } from '@nestjs/common'; +import { + AssetClass, + AssetSubClass, + DataSource, + SymbolProfile +} from '@prisma/client'; +import { countries } from 'countries-list'; +import yahooFinance from 'yahoo-finance2'; +import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; + +@Injectable() +export class YahooFinanceDataEnhancerService implements DataEnhancerInterface { + private baseCurrency: string; + + public constructor( + private readonly configurationService: ConfigurationService, + private readonly cryptocurrencyService: CryptocurrencyService + ) { + this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); + } + + public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) { + let symbol = aYahooFinanceSymbol.replace( + new RegExp(`-${this.baseCurrency}$`), + this.baseCurrency + ); + + if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) { + symbol = `${this.baseCurrency}${symbol}`; + } + + return symbol.replace('=X', ''); + } + + /** + * Converts a symbol to a Yahoo Finance symbol + * + * Currency: USDCHF -> USDCHF=X + * Cryptocurrency: BTCUSD -> BTC-USD + * DOGEUSD -> DOGE-USD + */ + public convertToYahooFinanceSymbol(aSymbol: string) { + if ( + aSymbol.includes(this.baseCurrency) && + aSymbol.length > this.baseCurrency.length + ) { + if ( + isCurrency( + aSymbol.substring(0, aSymbol.length - this.baseCurrency.length) + ) + ) { + return `${aSymbol}=X`; + } else if ( + this.cryptocurrencyService.isCryptocurrency( + aSymbol.replace( + new RegExp(`-${this.baseCurrency}$`), + this.baseCurrency + ) + ) + ) { + // Add a dash before the last three characters + // BTCUSD -> BTC-USD + // DOGEUSD -> DOGE-USD + // SOL1USD -> SOL1-USD + return aSymbol.replace( + new RegExp(`-?${this.baseCurrency}$`), + `-${this.baseCurrency}` + ); + } + } + + return aSymbol; + } + + public async enhance({ + response, + symbol + }: { + response: Partial; + symbol: string; + }): Promise> { + if (response.dataSource !== 'YAHOO' && !response.isin) { + return response; + } + + try { + let yahooSymbol: string; + + if (response.dataSource === 'YAHOO') { + yahooSymbol = symbol; + } else { + const { quotes } = await yahooFinance.search(response.isin); + yahooSymbol = quotes[0].symbol; + } + + const { countries, sectors, url } = await this.getAssetProfile( + yahooSymbol + ); + + if (countries) { + response.countries = countries; + } + + if (sectors) { + response.sectors = sectors; + } + + if (url) { + response.url = url; + } + } catch (error) { + Logger.error(error, 'YahooFinanceDataEnhancerService'); + } + + return response; + } + + public formatName({ + longName, + quoteType, + shortName, + symbol + }: { + longName: Price['longName']; + quoteType: Price['quoteType']; + shortName: Price['shortName']; + symbol: Price['symbol']; + }) { + let name = longName; + + if (name) { + name = name.replace('Amundi Index Solutions - ', ''); + name = name.replace('iShares ETF (CH) - ', ''); + name = name.replace('iShares III Public Limited Company - ', ''); + name = name.replace('iShares V PLC - ', ''); + name = name.replace('iShares VI Public Limited Company - ', ''); + name = name.replace('iShares VII PLC - ', ''); + name = name.replace('Multi Units Luxembourg - ', ''); + name = name.replace('VanEck ETFs N.V. - ', ''); + name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', ''); + name = name.replace('Vanguard Funds Public Limited Company - ', ''); + name = name.replace('Vanguard Index Funds - ', ''); + name = name.replace('Xtrackers (IE) Plc - ', ''); + } + + if (quoteType === 'FUTURE') { + // "Gold Jun 22" -> "Gold" + name = shortName?.slice(0, -7); + } + + return name || shortName || symbol; + } + + public async getAssetProfile( + aSymbol: string + ): Promise> { + const response: Partial = {}; + + try { + const symbol = this.convertToYahooFinanceSymbol(aSymbol); + const assetProfile = await yahooFinance.quoteSummary(symbol, { + modules: ['price', 'summaryProfile', 'topHoldings'] + }); + + const { assetClass, assetSubClass } = this.parseAssetClass({ + quoteType: assetProfile.price.quoteType, + shortName: assetProfile.price.shortName + }); + + response.assetClass = assetClass; + response.assetSubClass = assetSubClass; + response.currency = assetProfile.price.currency; + response.dataSource = this.getName(); + response.name = this.formatName({ + longName: assetProfile.price.longName, + quoteType: assetProfile.price.quoteType, + shortName: assetProfile.price.shortName, + symbol: assetProfile.price.symbol + }); + response.symbol = aSymbol; + + if (assetSubClass === AssetSubClass.MUTUALFUND) { + response.sectors = []; + + for (const sectorWeighting of assetProfile.topHoldings + ?.sectorWeightings ?? []) { + for (const [sector, weight] of Object.entries(sectorWeighting)) { + response.sectors.push({ weight, name: this.parseSector(sector) }); + } + } + } else if ( + assetSubClass === AssetSubClass.STOCK && + assetProfile.summaryProfile?.country + ) { + // Add country if asset is stock and country available + + try { + const [code] = Object.entries(countries).find(([, country]) => { + return country.name === assetProfile.summaryProfile?.country; + }); + + if (code) { + response.countries = [{ code, weight: 1 }]; + } + } catch {} + + if (assetProfile.summaryProfile?.sector) { + response.sectors = [ + { name: assetProfile.summaryProfile?.sector, weight: 1 } + ]; + } + } + + const url = assetProfile.summaryProfile?.website; + if (url) { + response.url = url; + } + } catch (error) { + Logger.error(error, 'YahooFinanceService'); + } + + return response; + } + + public getName() { + return DataSource.YAHOO; + } + + public parseAssetClass({ + quoteType, + shortName + }: { + quoteType: string; + shortName: string; + }): { + assetClass: AssetClass; + assetSubClass: AssetSubClass; + } { + let assetClass: AssetClass; + let assetSubClass: AssetSubClass; + + switch (quoteType?.toLowerCase()) { + case 'cryptocurrency': + assetClass = AssetClass.CASH; + assetSubClass = AssetSubClass.CRYPTOCURRENCY; + break; + case 'equity': + assetClass = AssetClass.EQUITY; + assetSubClass = AssetSubClass.STOCK; + break; + case 'etf': + assetClass = AssetClass.EQUITY; + assetSubClass = AssetSubClass.ETF; + break; + case 'future': + assetClass = AssetClass.COMMODITY; + assetSubClass = AssetSubClass.COMMODITY; + + if ( + shortName?.toLowerCase()?.startsWith('gold') || + shortName?.toLowerCase()?.startsWith('palladium') || + shortName?.toLowerCase()?.startsWith('platinum') || + shortName?.toLowerCase()?.startsWith('silver') + ) { + assetSubClass = AssetSubClass.PRECIOUS_METAL; + } + + break; + case 'mutualfund': + assetClass = AssetClass.EQUITY; + assetSubClass = AssetSubClass.MUTUALFUND; + break; + } + + return { assetClass, assetSubClass }; + } + + private parseSector(aString: string): string { + let sector = UNKNOWN_KEY; + + switch (aString) { + case 'basic_materials': + sector = 'Basic Materials'; + break; + case 'communication_services': + sector = 'Communication Services'; + break; + case 'consumer_cyclical': + sector = 'Consumer Cyclical'; + break; + case 'consumer_defensive': + sector = 'Consumer Staples'; + break; + case 'energy': + sector = 'Energy'; + break; + case 'financial_services': + sector = 'Financial Services'; + break; + case 'healthcare': + sector = 'Healthcare'; + break; + case 'industrials': + sector = 'Industrials'; + break; + case 'realestate': + sector = 'Real Estate'; + break; + case 'technology': + sector = 'Technology'; + break; + case 'utilities': + sector = 'Utilities'; + break; + } + + return sector; + } +} 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 d36a12aa..e0ff98dd 100644 --- a/apps/api/src/services/data-provider/data-provider.module.ts +++ b/apps/api/src/services/data-provider/data-provider.module.ts @@ -11,12 +11,15 @@ import { PrismaModule } from '@ghostfolio/api/services/prisma.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile.module'; import { Module } from '@nestjs/common'; +import { DataEnhancerModule } from './data-enhancer/data-enhancer.module'; +import { YahooFinanceDataEnhancerService } from './data-enhancer/yahoo-finance/yahoo-finance.service'; import { DataProviderService } from './data-provider.service'; @Module({ imports: [ ConfigurationModule, CryptocurrencyModule, + DataEnhancerModule, PrismaModule, SymbolProfileModule ], @@ -57,7 +60,8 @@ import { DataProviderService } from './data-provider.service'; rapidApiService, yahooFinanceService ] - } + }, + YahooFinanceDataEnhancerService ], exports: [DataProviderService, YahooFinanceService] }) diff --git a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts index 73f16872..b0ef5735 100644 --- a/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts +++ b/apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts @@ -43,7 +43,8 @@ export class EodHistoricalDataService implements DataProviderInterface { currency: searchResult?.currency, dataSource: this.getName(), isin: searchResult?.isin, - name: searchResult?.name + name: searchResult?.name, + symbol: aSymbol }; } diff --git a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts index 65de9d28..f315a891 100644 --- a/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts +++ b/apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts @@ -30,7 +30,8 @@ export class GoogleSheetsService implements DataProviderInterface { aSymbol: string ): Promise> { return { - dataSource: this.getName() + dataSource: this.getName(), + symbol: aSymbol }; } diff --git a/apps/api/src/services/data-provider/manual/manual.service.ts b/apps/api/src/services/data-provider/manual/manual.service.ts index 324fa7aa..da58fefb 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -34,7 +34,8 @@ export class ManualService implements DataProviderInterface { aSymbol: string ): Promise> { return { - dataSource: this.getName() + dataSource: this.getName(), + symbol: aSymbol }; } diff --git a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts index f5119e0b..b0f185ef 100644 --- a/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts @@ -27,7 +27,8 @@ export class RapidApiService implements DataProviderInterface { aSymbol: string ): Promise> { return { - dataSource: this.getName() + dataSource: this.getName(), + symbol: aSymbol }; } diff --git a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts index 6a8349f8..863ee587 100644 --- a/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts +++ b/apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts @@ -1,26 +1,19 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; +import { YahooFinanceDataEnhancerService } from '@ghostfolio/api/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service'; import { DataProviderInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-provider.interface'; import { IDataProviderHistoricalResponse, IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; -import { UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; +import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { - AssetClass, - AssetSubClass, - DataSource, - SymbolProfile -} from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import Big from 'big.js'; -import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; import yahooFinance from 'yahoo-finance2'; -import type { Price } from 'yahoo-finance2/dist/esm/src/modules/quoteSummary-iface'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -28,7 +21,8 @@ export class YahooFinanceService implements DataProviderInterface { public constructor( private readonly configurationService: ConfigurationService, - private readonly cryptocurrencyService: CryptocurrencyService + private readonly cryptocurrencyService: CryptocurrencyService, + private readonly yahooFinanceDataEnhancerService: YahooFinanceDataEnhancerService ) { this.baseCurrency = this.configurationService.get('BASE_CURRENCY'); } @@ -37,128 +31,20 @@ export class YahooFinanceService implements DataProviderInterface { return true; } - public convertFromYahooFinanceSymbol(aYahooFinanceSymbol: string) { - let symbol = aYahooFinanceSymbol.replace( - new RegExp(`-${this.baseCurrency}$`), - this.baseCurrency - ); - - if (symbol.includes('=X') && !symbol.includes(this.baseCurrency)) { - symbol = `${this.baseCurrency}${symbol}`; - } - - return symbol.replace('=X', ''); - } - - /** - * Converts a symbol to a Yahoo Finance symbol - * - * Currency: USDCHF -> USDCHF=X - * Cryptocurrency: BTCUSD -> BTC-USD - * DOGEUSD -> DOGE-USD - */ - public convertToYahooFinanceSymbol(aSymbol: string) { - if ( - aSymbol.includes(this.baseCurrency) && - aSymbol.length > this.baseCurrency.length - ) { - if ( - isCurrency( - aSymbol.substring(0, aSymbol.length - this.baseCurrency.length) - ) - ) { - return `${aSymbol}=X`; - } else if ( - this.cryptocurrencyService.isCryptocurrency( - aSymbol.replace( - new RegExp(`-${this.baseCurrency}$`), - this.baseCurrency - ) - ) - ) { - // Add a dash before the last three characters - // BTCUSD -> BTC-USD - // DOGEUSD -> DOGE-USD - // SOL1USD -> SOL1-USD - return aSymbol.replace( - new RegExp(`-?${this.baseCurrency}$`), - `-${this.baseCurrency}` - ); - } - } - - return aSymbol; - } - public async getAssetProfile( aSymbol: string ): Promise> { - const response: Partial = {}; + const { assetClass, assetSubClass, currency, name } = + await this.yahooFinanceDataEnhancerService.getAssetProfile(aSymbol); - try { - const symbol = this.convertToYahooFinanceSymbol(aSymbol); - const assetProfile = await yahooFinance.quoteSummary(symbol, { - modules: ['price', 'summaryProfile', 'topHoldings'] - }); - - const { assetClass, assetSubClass } = this.parseAssetClass({ - quoteType: assetProfile.price.quoteType, - shortName: assetProfile.price.shortName - }); - - response.assetClass = assetClass; - response.assetSubClass = assetSubClass; - response.currency = assetProfile.price.currency; - response.dataSource = this.getName(); - response.name = this.formatName({ - longName: assetProfile.price.longName, - quoteType: assetProfile.price.quoteType, - shortName: assetProfile.price.shortName, - symbol: assetProfile.price.symbol - }); - response.symbol = aSymbol; - - if (assetSubClass === AssetSubClass.MUTUALFUND) { - response.sectors = []; - - for (const sectorWeighting of assetProfile.topHoldings - ?.sectorWeightings ?? []) { - for (const [sector, weight] of Object.entries(sectorWeighting)) { - response.sectors.push({ weight, name: this.parseSector(sector) }); - } - } - } else if ( - assetSubClass === AssetSubClass.STOCK && - assetProfile.summaryProfile?.country - ) { - // Add country if asset is stock and country available - - try { - const [code] = Object.entries(countries).find(([, country]) => { - return country.name === assetProfile.summaryProfile?.country; - }); - - if (code) { - response.countries = [{ code, weight: 1 }]; - } - } catch {} - - if (assetProfile.summaryProfile?.sector) { - response.sectors = [ - { name: assetProfile.summaryProfile?.sector, weight: 1 } - ]; - } - } - - const url = assetProfile.summaryProfile?.website; - if (url) { - response.url = url; - } - } catch (error) { - Logger.error(error, 'YahooFinanceService'); - } - - return response; + return { + assetClass, + assetSubClass, + currency, + name, + dataSource: this.getName(), + symbol: aSymbol + }; } public async getDividends({ @@ -178,7 +64,9 @@ export class YahooFinanceService implements DataProviderInterface { try { const historicalResult = await yahooFinance.historical( - this.convertToYahooFinanceSymbol(symbol), + this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( + symbol + ), { events: 'dividends', interval: granularity === 'month' ? '1mo' : '1d', @@ -228,7 +116,9 @@ export class YahooFinanceService implements DataProviderInterface { try { const historicalResult = await yahooFinance.historical( - this.convertToYahooFinanceSymbol(aSymbol), + this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol( + aSymbol + ), { interval: '1d', period1: format(from, DATE_FORMAT), @@ -278,7 +168,7 @@ export class YahooFinanceService implements DataProviderInterface { return {}; } const yahooFinanceSymbols = aSymbols.map((symbol) => - this.convertToYahooFinanceSymbol(symbol) + this.yahooFinanceDataEnhancerService.convertToYahooFinanceSymbol(symbol) ); try { @@ -288,7 +178,10 @@ export class YahooFinanceService implements DataProviderInterface { for (const quote of quotes) { // Convert symbols back - const symbol = this.convertFromYahooFinanceSymbol(quote.symbol); + const symbol = + this.yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol( + quote.symbol + ); response[symbol] = { currency: quote.currency, @@ -405,14 +298,16 @@ export class YahooFinanceService implements DataProviderInterface { return currentQuote.symbol === marketDataItem.symbol; }); - const symbol = this.convertFromYahooFinanceSymbol( - marketDataItem.symbol - ); + const symbol = + this.yahooFinanceDataEnhancerService.convertFromYahooFinanceSymbol( + marketDataItem.symbol + ); - const { assetClass, assetSubClass } = this.parseAssetClass({ - quoteType: quote.quoteType, - shortName: quote.shortname - }); + const { assetClass, assetSubClass } = + this.yahooFinanceDataEnhancerService.parseAssetClass({ + quoteType: quote.quoteType, + shortName: quote.shortname + }); items.push({ assetClass, @@ -420,7 +315,7 @@ export class YahooFinanceService implements DataProviderInterface { symbol, currency: marketDataItem.currency, dataSource: this.getName(), - name: this.formatName({ + name: this.yahooFinanceDataEnhancerService.formatName({ longName: quote.longname, quoteType: quote.quoteType, shortName: quote.shortname, @@ -435,42 +330,6 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } - private formatName({ - longName, - quoteType, - shortName, - symbol - }: { - longName: Price['longName']; - quoteType: Price['quoteType']; - shortName: Price['shortName']; - symbol: Price['symbol']; - }) { - let name = longName; - - if (name) { - name = name.replace('Amundi Index Solutions - ', ''); - name = name.replace('iShares ETF (CH) - ', ''); - name = name.replace('iShares III Public Limited Company - ', ''); - name = name.replace('iShares V PLC - ', ''); - name = name.replace('iShares VI Public Limited Company - ', ''); - name = name.replace('iShares VII PLC - ', ''); - name = name.replace('Multi Units Luxembourg - ', ''); - name = name.replace('VanEck ETFs N.V. - ', ''); - name = name.replace('Vaneck Vectors Ucits Etfs Plc - ', ''); - name = name.replace('Vanguard Funds Public Limited Company - ', ''); - name = name.replace('Vanguard Index Funds - ', ''); - name = name.replace('Xtrackers (IE) Plc - ', ''); - } - - if (quoteType === 'FUTURE') { - // "Gold Jun 22" -> "Gold" - name = shortName?.slice(0, -6); - } - - return name || shortName || symbol; - } - private getConvertedValue({ symbol, value @@ -491,95 +350,4 @@ export class YahooFinanceService implements DataProviderInterface { return value; } - - private parseAssetClass({ - quoteType, - shortName - }: { - quoteType: string; - shortName: string; - }): { - assetClass: AssetClass; - assetSubClass: AssetSubClass; - } { - let assetClass: AssetClass; - let assetSubClass: AssetSubClass; - - switch (quoteType?.toLowerCase()) { - case 'cryptocurrency': - assetClass = AssetClass.CASH; - assetSubClass = AssetSubClass.CRYPTOCURRENCY; - break; - case 'equity': - assetClass = AssetClass.EQUITY; - assetSubClass = AssetSubClass.STOCK; - break; - case 'etf': - assetClass = AssetClass.EQUITY; - assetSubClass = AssetSubClass.ETF; - break; - case 'future': - assetClass = AssetClass.COMMODITY; - assetSubClass = AssetSubClass.COMMODITY; - - if ( - shortName?.toLowerCase()?.startsWith('gold') || - shortName?.toLowerCase()?.startsWith('palladium') || - shortName?.toLowerCase()?.startsWith('platinum') || - shortName?.toLowerCase()?.startsWith('silver') - ) { - assetSubClass = AssetSubClass.PRECIOUS_METAL; - } - - break; - case 'mutualfund': - assetClass = AssetClass.EQUITY; - assetSubClass = AssetSubClass.MUTUALFUND; - break; - } - - return { assetClass, assetSubClass }; - } - - private parseSector(aString: string): string { - let sector = UNKNOWN_KEY; - - switch (aString) { - case 'basic_materials': - sector = 'Basic Materials'; - break; - case 'communication_services': - sector = 'Communication Services'; - break; - case 'consumer_cyclical': - sector = 'Consumer Cyclical'; - break; - case 'consumer_defensive': - sector = 'Consumer Staples'; - break; - case 'energy': - sector = 'Energy'; - break; - case 'financial_services': - sector = 'Financial Services'; - break; - case 'healthcare': - sector = 'Healthcare'; - break; - case 'industrials': - sector = 'Industrials'; - break; - case 'realestate': - sector = 'Real Estate'; - break; - case 'technology': - sector = 'Technology'; - break; - case 'utilities': - sector = 'Utilities'; - break; - } - - return sector; - } }