diff --git a/apps/api/src/app/portfolio/current-rate.service.ts b/apps/api/src/app/portfolio/current-rate.service.ts index fac041837..0549596ce 100644 --- a/apps/api/src/app/portfolio/current-rate.service.ts +++ b/apps/api/src/app/portfolio/current-rate.service.ts @@ -40,7 +40,7 @@ export class CurrentRateService { const today = resetHours(new Date()); promises.push( this.dataProviderService - .get(dataGatheringItems) + .getQuotes(dataGatheringItems) .then((dataResultProvider) => { const result = []; for (const dataGatheringItem of dataGatheringItems) { diff --git a/apps/api/src/app/portfolio/portfolio.service-new.ts b/apps/api/src/app/portfolio/portfolio.service-new.ts index adeea5c91..99e9496fc 100644 --- a/apps/api/src/app/portfolio/portfolio.service-new.ts +++ b/apps/api/src/app/portfolio/portfolio.service-new.ts @@ -327,7 +327,7 @@ export class PortfolioServiceNew { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -358,7 +358,6 @@ export class PortfolioServiceNew { countries: symbolProfile.countries, currency: item.currency, dataSource: symbolProfile.dataSource, - exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: item.grossPerformancePercentage?.toNumber() ?? 0, @@ -578,7 +577,7 @@ export class PortfolioServiceNew { ) }; } else { - const currentData = await this.dataProviderService.get([ + const currentData = await this.dataProviderService.getQuotes([ { dataSource: DataSource.YAHOO, symbol: aSymbol } ]); const marketPrice = currentData[aSymbol]?.marketPrice; @@ -679,7 +678,7 @@ export class PortfolioServiceNew { const symbols = positions.map((position) => position.symbol); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItem), + this.dataProviderService.getQuotes(dataGatheringItem), this.symbolProfileService.getSymbolProfiles(symbols) ]); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 0a164708c..ca0c25b03 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -315,7 +315,7 @@ export class PortfolioService { ); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItems), + this.dataProviderService.getQuotes(dataGatheringItems), this.symbolProfileService.getSymbolProfiles(symbols) ]); @@ -346,7 +346,6 @@ export class PortfolioService { countries: symbolProfile.countries, currency: item.currency, dataSource: symbolProfile.dataSource, - exchange: dataProviderResponse.exchange, grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: item.grossPerformancePercentage?.toNumber() ?? 0, @@ -552,9 +551,10 @@ export class PortfolioService { SymbolProfile, transactionCount, averagePrice: averagePrice.toNumber(), - grossPerformancePercent: position.grossPerformancePercentage.toNumber(), + grossPerformancePercent: + position.grossPerformancePercentage?.toNumber(), historicalData: historicalDataArray, - netPerformancePercent: position.netPerformancePercentage.toNumber(), + netPerformancePercent: position.netPerformancePercentage?.toNumber(), quantity: quantity.toNumber(), value: this.exchangeRateDataService.toCurrency( quantity.mul(marketPrice).toNumber(), @@ -563,7 +563,7 @@ export class PortfolioService { ) }; } else { - const currentData = await this.dataProviderService.get([ + const currentData = await this.dataProviderService.getQuotes([ { dataSource: DataSource.YAHOO, symbol: aSymbol } ]); const marketPrice = currentData[aSymbol]?.marketPrice; @@ -660,7 +660,7 @@ export class PortfolioService { const symbols = positions.map((position) => position.symbol); const [dataProviderResponses, symbolProfiles] = await Promise.all([ - this.dataProviderService.get(dataGatheringItem), + this.dataProviderService.getQuotes(dataGatheringItem), this.symbolProfileService.getSymbolProfiles(symbols) ]); diff --git a/apps/api/src/app/symbol/symbol.service.ts b/apps/api/src/app/symbol/symbol.service.ts index 37b1c5864..8d73617c6 100644 --- a/apps/api/src/app/symbol/symbol.service.ts +++ b/apps/api/src/app/symbol/symbol.service.ts @@ -27,8 +27,10 @@ export class SymbolService { dataGatheringItem: IDataGatheringItem; includeHistoricalData?: number; }): Promise { - const response = await this.dataProviderService.get([dataGatheringItem]); - const { currency, marketPrice } = response[dataGatheringItem.symbol] ?? {}; + const quotes = await this.dataProviderService.getQuotes([ + dataGatheringItem + ]); + const { currency, marketPrice } = quotes[dataGatheringItem.symbol] ?? {}; if (dataGatheringItem.dataSource && marketPrice) { let historicalData: HistoricalDataItem[] = []; diff --git a/apps/api/src/services/data-gathering.service.ts b/apps/api/src/services/data-gathering.service.ts index 81c9c884d..62bb6d190 100644 --- a/apps/api/src/services/data-gathering.service.ts +++ b/apps/api/src/services/data-gathering.service.ts @@ -226,22 +226,24 @@ export class DataGatheringService { dataGatheringItems = await this.getSymbolsProfileData(); } - const currentData = await this.dataProviderService.get(dataGatheringItems); + const assetProfiles = await this.dataProviderService.getAssetProfiles( + dataGatheringItems + ); const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( dataGatheringItems.map(({ symbol }) => { return symbol; }) ); - for (const [symbol, response] of Object.entries(currentData)) { + for (const [symbol, assetProfile] of Object.entries(assetProfiles)) { const symbolMapping = symbolProfiles.find((symbolProfile) => { return symbolProfile.symbol === symbol; })?.symbolMapping; for (const dataEnhancer of this.dataEnhancers) { try { - currentData[symbol] = await dataEnhancer.enhance({ - response, + assetProfiles[symbol] = await dataEnhancer.enhance({ + response: assetProfile, symbol: symbolMapping?.[dataEnhancer.getName()] ?? symbol }); } catch (error) { @@ -257,7 +259,7 @@ export class DataGatheringService { dataSource, name, sectors - } = currentData[symbol]; + } = assetProfiles[symbol]; try { await this.prismaService.symbolProfile.upsert({ 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 b0a1cc667..c1c0583d9 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 @@ -1,15 +1,15 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse +} from '@ghostfolio/api/services/interfaces/interfaces'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import { isAfter, isBefore, parse } from 'date-fns'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse -} from '../../interfaces/interfaces'; import { DataProviderInterface } from '../interfaces/data-provider.interface'; import { IAlphaVantageHistoricalResponse } from './interfaces/interfaces'; @@ -29,10 +29,12 @@ export class AlphaVantageService implements DataProviderInterface { return !!this.configurationService.get('ALPHA_VANTAGE_API_KEY'); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( @@ -84,6 +86,12 @@ export class AlphaVantageService implements DataProviderInterface { return DataSource.ALPHA_VANTAGE; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const result = await this.alphaVantage.data.search(aQuery); 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 a469e57a5..e6d90edca 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,5 +1,8 @@ import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { Country } from '@ghostfolio/common/interfaces/country.interface'; +import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; +import { SymbolProfile } from '@prisma/client'; import bent from 'bent'; const getJSON = bent('json'); @@ -21,9 +24,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { response, symbol }: { - response: IDataProviderResponse; + response: Partial; symbol: string; - }): Promise { + }): Promise> { if ( !(response.assetClass === 'EQUITY' && response.assetSubClass === 'ETF') ) { @@ -40,7 +43,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { ); }); - if (!response.countries || response.countries.length === 0) { + if ( + !response.countries || + (response.countries as unknown as Country[]).length === 0 + ) { response.countries = []; for (const [name, value] of Object.entries(holdings.countries)) { let countryCode: string; @@ -65,7 +71,10 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface { } } - if (!response.sectors || response.sectors.length === 0) { + if ( + !response.sectors || + (response.sectors as unknown as Sector[]).length === 0 + ) { response.sectors = []; for (const [name, value] of Object.entries(holdings.sectors)) { response.sectors.push({ diff --git a/apps/api/src/services/data-provider/data-provider.service.ts b/apps/api/src/services/data-provider/data-provider.service.ts index 4281c3cb8..71cc293d4 100644 --- a/apps/api/src/services/data-provider/data-provider.service.ts +++ b/apps/api/src/services/data-provider/data-provider.service.ts @@ -10,7 +10,7 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Inject, Injectable, Logger } from '@nestjs/common'; -import { DataSource, MarketData } from '@prisma/client'; +import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { format, isValid } from 'date-fns'; import { groupBy, isEmpty } from 'lodash'; @@ -23,42 +23,6 @@ export class DataProviderService { private readonly prismaService: PrismaService ) {} - public async get(items: IDataGatheringItem[]): Promise<{ - [symbol: string]: IDataProviderResponse; - }> { - const response: { - [symbol: string]: IDataProviderResponse; - } = {}; - - const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); - - const promises = []; - - for (const [dataSource, dataGatheringItems] of Object.entries( - itemsGroupedByDataSource - )) { - const symbols = dataGatheringItems.map((dataGatheringItem) => { - return dataGatheringItem.symbol; - }); - - const promise = Promise.resolve( - this.getDataProvider(DataSource[dataSource]).get(symbols) - ); - - promises.push( - promise.then((result) => { - for (const [symbol, dataProviderResponse] of Object.entries(result)) { - response[symbol] = dataProviderResponse; - } - }) - ); - } - - await Promise.all(promises); - - return response; - } - public async getHistorical( aItems: IDataGatheringItem[], aGranularity: Granularity = 'month', @@ -158,6 +122,82 @@ export class DataProviderService { return result; } + public getPrimaryDataSource(): DataSource { + return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')]; + } + + public async getAssetProfiles(items: IDataGatheringItem[]): Promise<{ + [symbol: string]: Partial; + }> { + const response: { + [symbol: string]: Partial; + } = {}; + + const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); + + const promises = []; + + for (const [dataSource, dataGatheringItems] of Object.entries( + itemsGroupedByDataSource + )) { + const symbols = dataGatheringItems.map((dataGatheringItem) => { + return dataGatheringItem.symbol; + }); + + for (const symbol of symbols) { + const promise = Promise.resolve( + this.getDataProvider(DataSource[dataSource]).getAssetProfile(symbol) + ); + + promises.push( + promise.then((symbolProfile) => { + response[symbol] = symbolProfile; + }) + ); + } + } + + await Promise.all(promises); + + return response; + } + + public async getQuotes(items: IDataGatheringItem[]): Promise<{ + [symbol: string]: IDataProviderResponse; + }> { + const response: { + [symbol: string]: IDataProviderResponse; + } = {}; + + const itemsGroupedByDataSource = groupBy(items, (item) => item.dataSource); + + const promises = []; + + for (const [dataSource, dataGatheringItems] of Object.entries( + itemsGroupedByDataSource + )) { + const symbols = dataGatheringItems.map((dataGatheringItem) => { + return dataGatheringItem.symbol; + }); + + const promise = Promise.resolve( + this.getDataProvider(DataSource[dataSource]).getQuotes(symbols) + ); + + promises.push( + promise.then((result) => { + for (const [symbol, dataProviderResponse] of Object.entries(result)) { + response[symbol] = dataProviderResponse; + } + }) + ); + } + + await Promise.all(promises); + + return response; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const promises: Promise<{ items: LookupItem[] }>[] = []; let lookupItems: LookupItem[] = []; @@ -184,10 +224,6 @@ export class DataProviderService { }; } - public getPrimaryDataSource(): DataSource { - return DataSource[this.configurationService.get('DATA_SOURCE_PRIMARY')]; - } - private getDataProvider(providerName: DataSource) { for (const dataProviderInterface of this.dataProviderInterfaces) { if (dataProviderInterface.getName() === providerName) { diff --git a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts index a75dae7fb..dbc7dc97c 100644 --- a/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts +++ b/apps/api/src/services/data-provider/ghostfolio-scraper-api/ghostfolio-scraper-api.service.ts @@ -14,7 +14,7 @@ import { } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import * as cheerio from 'cheerio'; import { format } from 'date-fns'; @@ -32,41 +32,12 @@ export class GhostfolioScraperApiService implements DataProviderInterface { return isGhostfolioScraperApiSymbol(symbol); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const [symbol] = aSymbols; - const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( - [symbol] - ); - - const { marketPrice } = await this.prismaService.marketData.findFirst({ - orderBy: { - date: 'desc' - }, - where: { - symbol - } - }); - - return { - [symbol]: { - marketPrice, - currency: symbolProfile?.currency, - dataSource: this.getName(), - marketState: MarketState.delayed - } - }; - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( @@ -112,6 +83,43 @@ export class GhostfolioScraperApiService implements DataProviderInterface { return DataSource.GHOSTFOLIO; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const [symbol] = aSymbols; + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( + [symbol] + ); + + const { marketPrice } = await this.prismaService.marketData.findFirst({ + orderBy: { + date: 'desc' + }, + where: { + symbol + } + }); + + return { + [symbol]: { + marketPrice, + currency: symbolProfile?.currency, + dataSource: this.getName(), + marketState: MarketState.delayed + } + }; + } catch (error) { + Logger.error(error); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items = await this.prismaService.symbolProfile.findMany({ select: { 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 079b023a3..3bc427fd3 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 @@ -11,7 +11,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import { format } from 'date-fns'; import { GoogleSpreadsheet } from 'google-spreadsheet'; @@ -27,49 +27,12 @@ export class GoogleSheetsService implements DataProviderInterface { return true; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const response: { [symbol: string]: IDataProviderResponse } = {}; - - const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( - aSymbols - ); - - const sheet = await this.getSheet({ - sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), - symbol: 'Overview' - }); - - const rows = await sheet.getRows(); - - for (const row of rows) { - const marketPrice = parseFloat(row['marketPrice']); - const symbol = row['symbol']; - - if (aSymbols.includes(symbol)) { - response[symbol] = { - marketPrice, - currency: symbolProfiles.find((symbolProfile) => { - return symbolProfile.symbol === symbol; - })?.currency, - dataSource: this.getName(), - marketState: MarketState.delayed - }; - } - } - - return response; - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( @@ -119,6 +82,51 @@ export class GoogleSheetsService implements DataProviderInterface { return DataSource.GOOGLE_SHEETS; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const response: { [symbol: string]: IDataProviderResponse } = {}; + + const symbolProfiles = await this.symbolProfileService.getSymbolProfiles( + aSymbols + ); + + const sheet = await this.getSheet({ + sheetId: this.configurationService.get('GOOGLE_SHEETS_ID'), + symbol: 'Overview' + }); + + const rows = await sheet.getRows(); + + for (const row of rows) { + const marketPrice = parseFloat(row['marketPrice']); + const symbol = row['symbol']; + + if (aSymbols.includes(symbol)) { + response[symbol] = { + marketPrice, + currency: symbolProfiles.find((symbolProfile) => { + return symbolProfile.symbol === symbol; + })?.currency, + dataSource: this.getName(), + marketState: MarketState.delayed + }; + } + } + + return response; + } catch (error) { + Logger.error(error); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items = await this.prismaService.symbolProfile.findMany({ select: { diff --git a/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts b/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts index 26585b320..4e5ce8cba 100644 --- a/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts @@ -1,13 +1,13 @@ -import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; +import { SymbolProfile } from '@prisma/client'; export interface DataEnhancerInterface { enhance({ response, symbol }: { - response: IDataProviderResponse; + response: Partial; symbol: string; - }): Promise; + }): Promise>; getName(): string; } diff --git a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts index cdcdb3bd4..16cf44603 100644 --- a/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts +++ b/apps/api/src/services/data-provider/interfaces/data-provider.interface.ts @@ -4,12 +4,12 @@ import { IDataProviderResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { Granularity } from '@ghostfolio/common/types'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; export interface DataProviderInterface { canHandle(symbol: string): boolean; - get(aSymbols: string[]): Promise<{ [symbol: string]: IDataProviderResponse }>; + getAssetProfile(aSymbol: string): Promise>; getHistorical( aSymbol: string, @@ -22,5 +22,9 @@ export interface DataProviderInterface { getName(): DataSource; + getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }>; + search(aQuery: string): Promise<{ items: LookupItem[] }>; } 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 b1407c932..edcdd2cde 100644 --- a/apps/api/src/services/data-provider/manual/manual.service.ts +++ b/apps/api/src/services/data-provider/manual/manual.service.ts @@ -6,7 +6,7 @@ import { } from '@ghostfolio/api/services/interfaces/interfaces'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; @Injectable() export class ManualService implements DataProviderInterface { @@ -16,10 +16,12 @@ export class ManualService implements DataProviderInterface { return false; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( @@ -37,6 +39,12 @@ export class ManualService implements DataProviderInterface { return DataSource.MANUAL; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } diff --git a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts index 366ef6c97..47f7eba40 100644 --- a/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts +++ b/apps/api/src/services/data-provider/rakuten-rapid-api/rakuten-rapid-api.service.ts @@ -1,19 +1,19 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { ConfigurationService } from '@ghostfolio/api/services/configuration.service'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { ghostfolioFearAndGreedIndexSymbol } from '@ghostfolio/common/config'; import { DATE_FORMAT, getToday, getYesterday } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { DataSource } from '@prisma/client'; +import { DataSource, SymbolProfile } from '@prisma/client'; import * as bent from 'bent'; import { format, subMonths, subWeeks, subYears } from 'date-fns'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; import { DataProviderInterface } from '../interfaces/data-provider.interface'; @Injectable() @@ -29,34 +29,12 @@ export class RakutenRapidApiService implements DataProviderInterface { return !!this.configurationService.get('RAKUTEN_RAPID_API_KEY'); } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - - try { - const symbol = aSymbols[0]; - - if (symbol === ghostfolioFearAndGreedIndexSymbol) { - const fgi = await this.getFearAndGreedIndex(); - - return { - [ghostfolioFearAndGreedIndexSymbol]: { - currency: undefined, - dataSource: this.getName(), - marketPrice: fgi.now.value, - marketState: MarketState.open, - name: RakutenRapidApiService.FEAR_AND_GREED_INDEX_NAME - } - }; - } - } catch (error) { - Logger.error(error); - } - - return {}; + public async getAssetProfile( + aSymbol: string + ): Promise> { + return { + dataSource: this.getName() + }; } public async getHistorical( @@ -125,6 +103,35 @@ export class RakutenRapidApiService implements DataProviderInterface { return DataSource.RAKUTEN; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + + try { + const symbol = aSymbols[0]; + + if (symbol === ghostfolioFearAndGreedIndexSymbol) { + const fgi = await this.getFearAndGreedIndex(); + + return { + [ghostfolioFearAndGreedIndexSymbol]: { + currency: undefined, + dataSource: this.getName(), + marketPrice: fgi.now.value, + marketState: MarketState.open + } + }; + } + } catch (error) { + Logger.error(error); + } + + return {}; + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { return { items: [] }; } diff --git a/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts b/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts deleted file mode 100644 index 8f917f462..000000000 --- a/apps/api/src/services/data-provider/yahoo-finance/interfaces/interfaces.ts +++ /dev/null @@ -1,21 +0,0 @@ -export interface IYahooFinanceQuoteResponse { - price: IYahooFinancePrice; - summaryProfile: IYahooFinanceSummaryProfile; -} - -export interface IYahooFinancePrice { - currency: string; - exchangeName: string; - longName: string; - marketState: string; - quoteType: string; - regularMarketPrice: number; - shortName: string; -} - -export interface IYahooFinanceSummaryProfile { - country?: string; - industry?: string; - sector?: string; - website?: string; -} 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 1492c84b0..984d5953d 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,27 +1,27 @@ import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; -import { UNKNOWN_KEY, baseCurrency } from '@ghostfolio/common/config'; +import { + IDataProviderHistoricalResponse, + IDataProviderResponse, + MarketState +} from '@ghostfolio/api/services/interfaces/interfaces'; +import { baseCurrency } from '@ghostfolio/common/config'; import { DATE_FORMAT, isCurrency } from '@ghostfolio/common/helper'; import { Granularity } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { + AssetClass, + AssetSubClass, + DataSource, + SymbolProfile +} from '@prisma/client'; import * as bent from 'bent'; import Big from 'big.js'; import { countries } from 'countries-list'; import { addDays, format, isSameDay } from 'date-fns'; -import * as yahooFinance from 'yahoo-finance'; import yahooFinance2 from 'yahoo-finance2'; -import { - IDataProviderHistoricalResponse, - IDataProviderResponse, - MarketState -} from '../../interfaces/interfaces'; import { DataProviderInterface } from '../interfaces/data-provider.interface'; -import { - IYahooFinancePrice, - IYahooFinanceQuoteResponse -} from './interfaces/interfaces'; @Injectable() export class YahooFinanceService implements DataProviderInterface { @@ -73,92 +73,60 @@ export class YahooFinanceService implements DataProviderInterface { return aSymbol; } - public async get( - aSymbols: string[] - ): Promise<{ [symbol: string]: IDataProviderResponse }> { - if (aSymbols.length <= 0) { - return {}; - } - const yahooFinanceSymbols = aSymbols.map((symbol) => - this.convertToYahooFinanceSymbol(symbol) - ); + public async getAssetProfile( + aSymbol: string + ): Promise> { + const response: Partial = {}; try { - const response: { [symbol: string]: IDataProviderResponse } = {}; - - const data: { - [symbol: string]: IYahooFinanceQuoteResponse; - } = await yahooFinance.quote({ - modules: ['price', 'summaryProfile'], - symbols: yahooFinanceSymbols + const symbol = this.convertToYahooFinanceSymbol(aSymbol); + const assetProfile = await yahooFinance2.quoteSummary(symbol, { + modules: ['price', 'summaryProfile'] }); - for (const [yahooFinanceSymbol, value] of Object.entries(data)) { - // Convert symbols back - const symbol = this.convertFromYahooFinanceSymbol(yahooFinanceSymbol); - - const { assetClass, assetSubClass } = this.parseAssetClass(value.price); + const { assetClass, assetSubClass } = this.parseAssetClass( + assetProfile.price + ); - response[symbol] = { - assetClass, - assetSubClass, - currency: value.price?.currency, - dataSource: this.getName(), - exchange: this.parseExchange(value.price?.exchangeName), - marketState: - value.price?.marketState === 'REGULAR' || - this.cryptocurrencyService.isCryptocurrency(symbol) - ? MarketState.open - : MarketState.closed, - marketPrice: value.price?.regularMarketPrice || 0, - name: value.price?.longName || value.price?.shortName || symbol - }; + response.assetClass = assetClass; + response.assetSubClass = assetSubClass; + response.currency = assetProfile.price.currency; + response.dataSource = this.getName(); + response.name = + assetProfile.price.longName || assetProfile.price.shortName || symbol; + response.symbol = aSymbol; + + if ( + assetSubClass === AssetSubClass.STOCK && + assetProfile.summaryProfile?.country + ) { + // Add country if asset is stock and country available - if (value.price?.currency === 'GBp') { - // Convert GBp (pence) to GBP - response[symbol].currency = 'GBP'; - response[symbol].marketPrice = new Big( - value.price?.regularMarketPrice ?? 0 - ) - .div(100) - .toNumber(); - } + try { + const [code] = Object.entries(countries).find(([, country]) => { + return country.name === assetProfile.summaryProfile?.country; + }); - // Add country if stock and available - if ( - assetSubClass === AssetSubClass.STOCK && - value.summaryProfile?.country - ) { - try { - const [code] = Object.entries(countries).find(([, country]) => { - return country.name === value.summaryProfile?.country; - }); - - if (code) { - response[symbol].countries = [{ code, weight: 1 }]; - } - } catch {} - - if (value.summaryProfile?.sector) { - response[symbol].sectors = [ - { name: value.summaryProfile?.sector, weight: 1 } - ]; + if (code) { + response.countries = [{ code, weight: 1 }]; } - } + } catch {} - // Add url if available - const url = value.summaryProfile?.website; - if (url) { - response[symbol].url = url; + if (assetProfile.summaryProfile?.sector) { + response.sectors = [ + { name: assetProfile.summaryProfile?.sector, weight: 1 } + ]; } } - return response; - } catch (error) { - Logger.error(error); + // TODO: Add url if available + /*const url = assetProfile.summaryProfile?.website; + if (url) { + response.url = url; + }*/ + } catch {} - return {}; - } + return response; } public async getHistorical( @@ -215,6 +183,53 @@ export class YahooFinanceService implements DataProviderInterface { return DataSource.YAHOO; } + public async getQuotes( + aSymbols: string[] + ): Promise<{ [symbol: string]: IDataProviderResponse }> { + if (aSymbols.length <= 0) { + return {}; + } + const yahooFinanceSymbols = aSymbols.map((symbol) => + this.convertToYahooFinanceSymbol(symbol) + ); + + try { + const response: { [symbol: string]: IDataProviderResponse } = {}; + + const quotes = await yahooFinance2.quote(yahooFinanceSymbols, {}, {}); + + for (const quote of quotes) { + // Convert symbols back + const symbol = this.convertFromYahooFinanceSymbol(quote.symbol); + + response[symbol] = { + currency: quote.currency, + dataSource: this.getName(), + marketState: + quote.marketState === 'REGULAR' || + this.cryptocurrencyService.isCryptocurrency(symbol) + ? MarketState.open + : MarketState.closed, + marketPrice: quote.regularMarketPrice || 0 + }; + + if (quote.currency === 'GBp') { + // Convert GBp (pence) to GBP + response[symbol].currency = 'GBP'; + response[symbol].marketPrice = new Big(quote.regularMarketPrice ?? 0) + .div(100) + .toNumber(); + } + } + + return response; + } catch (error) { + Logger.error(error); + + return {}; + } + } + public async search(aQuery: string): Promise<{ items: LookupItem[] }> { const items: LookupItem[] = []; @@ -230,7 +245,7 @@ export class YahooFinanceService implements DataProviderInterface { const searchResult = await get(); - const symbols: string[] = searchResult.quotes + const quotes = searchResult.quotes .filter((quote) => { // filter out undefined symbols return quote.symbol; @@ -253,19 +268,24 @@ export class YahooFinanceService implements DataProviderInterface { } return true; - }) - .map(({ symbol }) => { - return symbol; }); - const marketData = await this.get(symbols); + const marketData = await this.getQuotes( + quotes.map(({ symbol }) => { + return symbol; + }) + ); for (const [symbol, value] of Object.entries(marketData)) { + const quote = quotes.find((currentQuote: any) => { + return currentQuote.symbol === symbol; + }); + items.push({ symbol, currency: value.currency, dataSource: this.getName(), - name: value.name + name: quote?.longname || quote?.shortname || symbol }); } } catch (error) { @@ -275,7 +295,7 @@ export class YahooFinanceService implements DataProviderInterface { return { items }; } - private parseAssetClass(aPrice: IYahooFinancePrice): { + private parseAssetClass(aPrice: any): { assetClass: AssetClass; assetSubClass: AssetSubClass; } { @@ -299,12 +319,4 @@ export class YahooFinanceService implements DataProviderInterface { return { assetClass, assetSubClass }; } - - private parseExchange(aString: string): string { - if (aString?.toLowerCase() === 'ccc') { - return UNKNOWN_KEY; - } - - return aString; - } } diff --git a/apps/api/src/services/exchange-rate-data.service.ts b/apps/api/src/services/exchange-rate-data.service.ts index f77f7ef79..628d149ba 100644 --- a/apps/api/src/services/exchange-rate-data.service.ts +++ b/apps/api/src/services/exchange-rate-data.service.ts @@ -61,7 +61,7 @@ export class ExchangeRateDataService { if (Object.keys(result).length !== this.currencyPairs.length) { // Load currencies directly from data provider as a fallback // if historical data is not fully available - const historicalData = await this.dataProviderService.get( + const historicalData = await this.dataProviderService.getQuotes( this.currencyPairs.map(({ dataSource, symbol }) => { return { dataSource, symbol }; }) diff --git a/apps/api/src/services/interfaces/interfaces.ts b/apps/api/src/services/interfaces/interfaces.ts index c7d3a08f7..50fd6009f 100644 --- a/apps/api/src/services/interfaces/interfaces.ts +++ b/apps/api/src/services/interfaces/interfaces.ts @@ -33,19 +33,10 @@ export interface IDataProviderHistoricalResponse { } export interface IDataProviderResponse { - assetClass?: AssetClass; - assetSubClass?: AssetSubClass; - countries?: { code: string; weight: number }[]; currency: string; dataSource: DataSource; - exchange?: string; - marketChange?: number; - marketChangePercent?: number; marketPrice: number; marketState: MarketState; - name?: string; - sectors?: { name: string; weight: number }[]; - url?: string; } export interface IDataGatheringItem { diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index 8d254bb98..d846ecd43 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -39,11 +39,6 @@
{{ position?.name }}
{{ position?.symbol | gfSymbol }} - ({{ position.exchange }})