diff --git a/CHANGELOG.md b/CHANGELOG.md index bc0ef2df9..50e770f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Refactored the get holding functionality in the portfolio service + ## 2.216.0 - 2025-11-10 ### Changed diff --git a/apps/api/src/app/import/import.service.ts b/apps/api/src/app/import/import.service.ts index 2ec28365e..669432db5 100644 --- a/apps/api/src/app/import/import.service.ts +++ b/apps/api/src/app/import/import.service.ts @@ -58,13 +58,18 @@ export class ImportService { userId }: AssetProfileIdentifier & { userId: string }): Promise { try { - const { activities, firstBuyDate, historicalData } = - await this.portfolioService.getHolding({ - dataSource, - symbol, - userId, - impersonationId: undefined - }); + const holding = await this.portfolioService.getHolding({ + dataSource, + symbol, + userId, + impersonationId: undefined + }); + + if (!holding) { + return []; + } + + const { activities, firstBuyDate, historicalData } = holding; const [[assetProfile], dividends] = await Promise.all([ this.symbolProfileService.getSymbolProfiles([ diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index b74b779f6..1ae6190e1 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -88,7 +88,6 @@ import { parseISO, set } from 'date-fns'; -import { isEmpty } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; @@ -776,35 +775,7 @@ export class PortfolioService { }); if (activities.length === 0) { - return { - activities: [], - activitiesCount: 0, - averagePrice: undefined, - dataProviderInfo: undefined, - dividendInBaseCurrency: undefined, - dividendYieldPercent: undefined, - dividendYieldPercentWithCurrencyEffect: undefined, - feeInBaseCurrency: undefined, - firstBuyDate: undefined, - grossPerformance: undefined, - grossPerformancePercent: undefined, - grossPerformancePercentWithCurrencyEffect: undefined, - grossPerformanceWithCurrencyEffect: undefined, - historicalData: [], - investmentInBaseCurrencyWithCurrencyEffect: undefined, - marketPrice: undefined, - marketPriceMax: undefined, - marketPriceMin: undefined, - netPerformance: undefined, - netPerformancePercent: undefined, - netPerformancePercentWithCurrencyEffect: undefined, - netPerformanceWithCurrencyEffect: undefined, - performances: undefined, - quantity: undefined, - SymbolProfile: undefined, - tags: [], - value: undefined - }; + return undefined; } const [SymbolProfile] = await this.symbolProfileService.getSymbolProfiles([ @@ -818,7 +789,6 @@ export class PortfolioService { currency: userCurrency }); - const portfolioStart = portfolioCalculator.getStartDate(); const transactionPoints = portfolioCalculator.getTransactionPoints(); const { positions } = await portfolioCalculator.getSnapshot(); @@ -827,225 +797,108 @@ export class PortfolioService { return position.dataSource === dataSource && position.symbol === symbol; }); - if (holding) { - const { - averagePrice, - currency, - dividendInBaseCurrency, - fee, - firstBuyDate, - grossPerformance, - grossPerformancePercentage, - grossPerformancePercentageWithCurrencyEffect, - grossPerformanceWithCurrencyEffect, - investmentWithCurrencyEffect, - marketPrice, - netPerformance, - netPerformancePercentage, - netPerformancePercentageWithCurrencyEffectMap, - netPerformanceWithCurrencyEffectMap, - quantity, - tags, - timeWeightedInvestment, - timeWeightedInvestmentWithCurrencyEffect, - transactionCount - } = holding; - - const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { - return ( - SymbolProfile.dataSource === dataSource && - SymbolProfile.symbol === symbol - ); - }); - - const dividendYieldPercent = getAnnualizedPerformancePercent({ - daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), - netPerformancePercentage: timeWeightedInvestment.eq(0) - ? new Big(0) - : dividendInBaseCurrency.div(timeWeightedInvestment) - }); - - const dividendYieldPercentWithCurrencyEffect = - getAnnualizedPerformancePercent({ - daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), - netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq( - 0 - ) - ? new Big(0) - : dividendInBaseCurrency.div( - timeWeightedInvestmentWithCurrencyEffect - ) - }); + if (!holding) { + return undefined; + } - const historicalData = await this.dataProviderService.getHistorical( - [{ dataSource, symbol }], - 'day', - parseISO(firstBuyDate), - new Date() - ); + const { + averagePrice, + currency, + dividendInBaseCurrency, + fee, + firstBuyDate, + grossPerformance, + grossPerformancePercentage, + grossPerformancePercentageWithCurrencyEffect, + grossPerformanceWithCurrencyEffect, + investmentWithCurrencyEffect, + marketPrice, + netPerformance, + netPerformancePercentage, + netPerformancePercentageWithCurrencyEffectMap, + netPerformanceWithCurrencyEffectMap, + quantity, + tags, + timeWeightedInvestment, + timeWeightedInvestmentWithCurrencyEffect, + transactionCount + } = holding; - const historicalDataArray: HistoricalDataItem[] = []; - let marketPriceMax = Math.max( - activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - marketPrice - ); - let marketPriceMaxDate = - marketPrice > activitiesOfHolding[0].unitPriceInAssetProfileCurrency - ? new Date() - : activitiesOfHolding[0].date; - let marketPriceMin = Math.min( - activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - marketPrice + const activitiesOfHolding = activities.filter(({ SymbolProfile }) => { + return ( + SymbolProfile.dataSource === dataSource && + SymbolProfile.symbol === symbol ); + }); - if (historicalData[symbol]) { - let j = -1; - for (const [date, { marketPrice }] of Object.entries( - historicalData[symbol] - )) { - while ( - j + 1 < transactionPoints.length && - !isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date)) - ) { - j++; - } - - let currentAveragePrice = 0; - let currentQuantity = 0; + const dividendYieldPercent = getAnnualizedPerformancePercent({ + daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), + netPerformancePercentage: timeWeightedInvestment.eq(0) + ? new Big(0) + : dividendInBaseCurrency.div(timeWeightedInvestment) + }); - const currentSymbol = transactionPoints[j]?.items.find( - (transactionPointSymbol) => { - return transactionPointSymbol.symbol === symbol; - } - ); + const dividendYieldPercentWithCurrencyEffect = + getAnnualizedPerformancePercent({ + daysInMarket: differenceInDays(new Date(), parseDate(firstBuyDate)), + netPerformancePercentage: timeWeightedInvestmentWithCurrencyEffect.eq(0) + ? new Big(0) + : dividendInBaseCurrency.div(timeWeightedInvestmentWithCurrencyEffect) + }); - if (currentSymbol) { - currentAveragePrice = currentSymbol.averagePrice.toNumber(); - currentQuantity = currentSymbol.quantity.toNumber(); - } + const historicalData = await this.dataProviderService.getHistorical( + [{ dataSource, symbol }], + 'day', + parseISO(firstBuyDate), + new Date() + ); - historicalDataArray.push({ - date, - averagePrice: currentAveragePrice, - marketPrice: - historicalDataArray.length > 0 - ? marketPrice - : currentAveragePrice, - quantity: currentQuantity - }); + const historicalDataArray: HistoricalDataItem[] = []; + let marketPriceMax = Math.max( + activitiesOfHolding[0].unitPriceInAssetProfileCurrency, + marketPrice + ); + let marketPriceMaxDate = + marketPrice > activitiesOfHolding[0].unitPriceInAssetProfileCurrency + ? new Date() + : activitiesOfHolding[0].date; + let marketPriceMin = Math.min( + activitiesOfHolding[0].unitPriceInAssetProfileCurrency, + marketPrice + ); - if (marketPrice > marketPriceMax) { - marketPriceMax = marketPrice; - marketPriceMaxDate = parseISO(date); - } - marketPriceMin = Math.min( - marketPrice ?? Number.MAX_SAFE_INTEGER, - marketPriceMin - ); + if (historicalData[symbol]) { + let j = -1; + for (const [date, { marketPrice }] of Object.entries( + historicalData[symbol] + )) { + while ( + j + 1 < transactionPoints.length && + !isAfter(parseDate(transactionPoints[j + 1].date), parseDate(date)) + ) { + j++; } - } else { - // Add historical entry for buy date, if no historical data available - historicalDataArray.push({ - averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - date: firstBuyDate, - marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, - quantity: activitiesOfHolding[0].quantity - }); - } - const performancePercent = - this.benchmarkService.calculateChangeInPercentage( - marketPriceMax, - marketPrice - ); + let currentAveragePrice = 0; + let currentQuantity = 0; - return { - firstBuyDate, - marketPrice, - marketPriceMax, - marketPriceMin, - SymbolProfile, - tags, - activities: activitiesOfHolding, - activitiesCount: transactionCount, - averagePrice: averagePrice.toNumber(), - dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], - dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), - dividendYieldPercent: dividendYieldPercent.toNumber(), - dividendYieldPercentWithCurrencyEffect: - dividendYieldPercentWithCurrencyEffect.toNumber(), - feeInBaseCurrency: this.exchangeRateDataService.toCurrency( - fee.toNumber(), - SymbolProfile.currency, - userCurrency - ), - grossPerformance: grossPerformance?.toNumber(), - grossPerformancePercent: grossPerformancePercentage?.toNumber(), - grossPerformancePercentWithCurrencyEffect: - grossPerformancePercentageWithCurrencyEffect?.toNumber(), - grossPerformanceWithCurrencyEffect: - grossPerformanceWithCurrencyEffect?.toNumber(), - historicalData: historicalDataArray, - investmentInBaseCurrencyWithCurrencyEffect: - investmentWithCurrencyEffect?.toNumber(), - netPerformance: netPerformance?.toNumber(), - netPerformancePercent: netPerformancePercentage?.toNumber(), - netPerformancePercentWithCurrencyEffect: - netPerformancePercentageWithCurrencyEffectMap?.['max']?.toNumber(), - netPerformanceWithCurrencyEffect: - netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(), - performances: { - allTimeHigh: { - performancePercent, - date: marketPriceMaxDate + const currentSymbol = transactionPoints[j]?.items.find( + (transactionPointSymbol) => { + return transactionPointSymbol.symbol === symbol; } - }, - quantity: quantity.toNumber(), - value: this.exchangeRateDataService.toCurrency( - quantity.mul(marketPrice ?? 0).toNumber(), - currency, - userCurrency - ) - }; - } else { - const currentData = await this.dataProviderService.getQuotes({ - user, - items: [{ symbol, dataSource: DataSource.YAHOO }] - }); - const marketPrice = currentData[symbol]?.marketPrice; - - let historicalData = await this.dataProviderService.getHistorical( - [{ symbol, dataSource: DataSource.YAHOO }], - 'day', - portfolioStart, - new Date() - ); + ); - if (isEmpty(historicalData)) { - try { - historicalData = await this.dataProviderService.getHistoricalRaw({ - assetProfileIdentifiers: [{ symbol, dataSource: DataSource.YAHOO }], - from: portfolioStart, - to: new Date() - }); - } catch { - historicalData = { - [symbol]: {} - }; + if (currentSymbol) { + currentAveragePrice = currentSymbol.averagePrice.toNumber(); + currentQuantity = currentSymbol.quantity.toNumber(); } - } - - const historicalDataArray: HistoricalDataItem[] = []; - let marketPriceMax = marketPrice; - let marketPriceMaxDate = new Date(); - let marketPriceMin = marketPrice; - for (const [date, { marketPrice }] of Object.entries( - historicalData[symbol] - )) { historicalDataArray.push({ date, - value: marketPrice + averagePrice: currentAveragePrice, + marketPrice: + historicalDataArray.length > 0 ? marketPrice : currentAveragePrice, + quantity: currentQuantity }); if (marketPrice > marketPriceMax) { @@ -1057,48 +910,70 @@ export class PortfolioService { marketPriceMin ); } + } else { + // Add historical entry for buy date, if no historical data available + historicalDataArray.push({ + averagePrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, + date: firstBuyDate, + marketPrice: activitiesOfHolding[0].unitPriceInAssetProfileCurrency, + quantity: activitiesOfHolding[0].quantity + }); + } - const performancePercent = - this.benchmarkService.calculateChangeInPercentage( - marketPriceMax, - marketPrice - ); - - return { - marketPrice, + const performancePercent = + this.benchmarkService.calculateChangeInPercentage( marketPriceMax, - marketPriceMin, - SymbolProfile, - activities: [], - activitiesCount: 0, - averagePrice: 0, - dataProviderInfo: undefined, - dividendInBaseCurrency: 0, - dividendYieldPercent: 0, - dividendYieldPercentWithCurrencyEffect: 0, - feeInBaseCurrency: 0, - firstBuyDate: undefined, - grossPerformance: undefined, - grossPerformancePercent: undefined, - grossPerformancePercentWithCurrencyEffect: undefined, - grossPerformanceWithCurrencyEffect: undefined, - historicalData: historicalDataArray, - investmentInBaseCurrencyWithCurrencyEffect: 0, - netPerformance: undefined, - netPerformancePercent: undefined, - netPerformancePercentWithCurrencyEffect: undefined, - netPerformanceWithCurrencyEffect: undefined, - performances: { - allTimeHigh: { - performancePercent, - date: marketPriceMaxDate - } - }, - quantity: 0, - tags: [], - value: 0 - }; - } + marketPrice + ); + + return { + firstBuyDate, + marketPrice, + marketPriceMax, + marketPriceMin, + SymbolProfile, + tags, + activities: activitiesOfHolding, + activitiesCount: transactionCount, + averagePrice: averagePrice.toNumber(), + dataProviderInfo: portfolioCalculator.getDataProviderInfos()?.[0], + dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), + dividendYieldPercent: dividendYieldPercent.toNumber(), + dividendYieldPercentWithCurrencyEffect: + dividendYieldPercentWithCurrencyEffect.toNumber(), + feeInBaseCurrency: this.exchangeRateDataService.toCurrency( + fee.toNumber(), + SymbolProfile.currency, + userCurrency + ), + grossPerformance: grossPerformance?.toNumber(), + grossPerformancePercent: grossPerformancePercentage?.toNumber(), + grossPerformancePercentWithCurrencyEffect: + grossPerformancePercentageWithCurrencyEffect?.toNumber(), + grossPerformanceWithCurrencyEffect: + grossPerformanceWithCurrencyEffect?.toNumber(), + historicalData: historicalDataArray, + investmentInBaseCurrencyWithCurrencyEffect: + investmentWithCurrencyEffect?.toNumber(), + netPerformance: netPerformance?.toNumber(), + netPerformancePercent: netPerformancePercentage?.toNumber(), + netPerformancePercentWithCurrencyEffect: + netPerformancePercentageWithCurrencyEffectMap?.['max']?.toNumber(), + netPerformanceWithCurrencyEffect: + netPerformanceWithCurrencyEffectMap?.['max']?.toNumber(), + performances: { + allTimeHigh: { + performancePercent, + date: marketPriceMaxDate + } + }, + quantity: quantity.toNumber(), + value: this.exchangeRateDataService.toCurrency( + quantity.mul(marketPrice ?? 0).toNumber(), + currency, + userCurrency + ) + }; } public async getPerformance({