import { ActivitiesService } from '@ghostfolio/api/app/activities/activities.service'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { getAssetProfileIdentifier, resetHours } from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces'; import { utc } from '@date-fns/utc'; import { Injectable } from '@nestjs/common'; import { Type as ActivityType } from '@prisma/client'; import { isBefore, isSameDay } from 'date-fns'; import { isEmpty, uniqBy } from 'lodash-es'; import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValuesObject } from './interfaces/get-values-object.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface'; @Injectable() export class CurrentRateService { private static readonly MARKET_DATA_PAGE_SIZE = 50000; public constructor( private readonly activitiesService: ActivitiesService, private readonly dataProviderService: DataProviderService, private readonly marketDataService: MarketDataService ) {} @LogPerformance public async getValues({ dataGatheringItems, dateQuery, subscriptionType }: GetValuesParams): Promise { const dataProviderInfos: DataProviderInfo[] = []; const today = resetHours(new Date()); const includesToday = (!dateQuery.lt || isBefore(new Date(), dateQuery.lt)) && (!dateQuery.gte || isBefore(dateQuery.gte, new Date())) && (!dateQuery.in || this.containsToday({ today, dates: dateQuery.in })); const quoteErrors: ResponseError['errors'] = []; const values: GetValueObject[] = []; if (includesToday) { const quotes = await this.dataProviderService.getQuotes({ subscriptionType, items: dataGatheringItems }); for (const { dataSource, symbol } of dataGatheringItems) { const quote = quotes[getAssetProfileIdentifier({ dataSource, symbol })]; if (quote?.dataProviderInfo) { dataProviderInfos.push(quote.dataProviderInfo); } if (quote?.marketPrice) { values.push({ dataSource, symbol, date: today, marketPrice: quote.marketPrice }); } else { quoteErrors.push({ dataSource, symbol }); } } } const assetProfileIdentifiers: AssetProfileIdentifier[] = dataGatheringItems.map(({ dataSource, symbol }) => { return { dataSource, symbol }; }); const marketDataCount = await this.marketDataService.getRangeCount({ assetProfileIdentifiers, dateQuery }); for ( let i = 0; i < marketDataCount; i += CurrentRateService.MARKET_DATA_PAGE_SIZE ) { // Use page size to limit the number of records fetched at once const data = await this.marketDataService.getRange({ assetProfileIdentifiers, dateQuery, skip: i, take: CurrentRateService.MARKET_DATA_PAGE_SIZE }); values.push( ...data.map(({ dataSource, date, marketPrice, symbol }) => ({ dataSource, date, marketPrice, symbol })) ); } const response: GetValuesObject = { dataProviderInfos, errors: quoteErrors.map(({ dataSource, symbol }) => { return { dataSource, symbol }; }), values: uniqBy(values, ({ dataSource, date, symbol }) => { return `${date}-${getAssetProfileIdentifier({ dataSource, symbol })}`; }) }; if (!isEmpty(quoteErrors)) { for (const { dataSource, symbol } of quoteErrors) { try { // If missing quote, fallback to the latest available historical market price let value: GetValueObject = response.values.find((currentValue) => { return ( currentValue.dataSource === dataSource && currentValue.symbol === symbol && isSameDay(currentValue.date, today, { in: utc }) ); }); if (!value) { const latestMarketData = await this.marketDataService.getLatest({ dataSource, symbol }); let marketPrice = latestMarketData?.marketPrice; if (!marketPrice) { // Fallback to unit price of latest buy or sell activity const latestActivity = await this.activitiesService.getLatestActivity({ dataSource, symbol, types: [ActivityType.BUY, ActivityType.SELL] }); marketPrice = latestActivity?.unitPrice ?? 0; } value = { dataSource, marketPrice, symbol, date: today }; response.values.push(value); } const [latestValue] = response.values .filter((currentValue) => { return ( currentValue.dataSource === dataSource && currentValue.marketPrice && currentValue.symbol === symbol ); }) .sort((a, b) => { if (a.date < b.date) { return 1; } if (a.date > b.date) { return -1; } return 0; }); value.marketPrice = latestValue.marketPrice; } catch {} } } return response; } private containsToday({ dates, today }: { dates: Date[]; today: Date; }): boolean { return dates.some((date) => { return isSameDay(date, today, { in: utc }); }); } }