From e24eb4a4b476b0717191fb6b37917e165af9f515 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Tue, 3 Mar 2026 08:15:35 +0100 Subject: [PATCH 1/3] feat(api): support group by year in portfolio performance endpoint --- .../calculator/portfolio-calculator.ts | 41 +++++++++++++++++++ .../src/app/portfolio/portfolio.controller.ts | 2 + .../src/app/portfolio/portfolio.service.ts | 19 ++++++++- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 553cb8c90..467449090 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -51,6 +51,7 @@ import { format, isAfter, isBefore, + isSameYear, isWithinInterval, min, startOfDay, @@ -784,6 +785,46 @@ export abstract class PortfolioCalculator { return { chart }; } + public async getPerformanceByGroup({ + endDate, + groupBy, + startDate + }: { + endDate: Date; + groupBy: GroupBy; + startDate: Date; + }) { + const interval = { start: startDate, end: endDate }; + const chart: HistoricalDataItem[] = []; + + if (groupBy === 'year') { + for (const year of eachYearOfInterval(interval)) { + const yearStartDate = startOfYear(year); + const yearEndDate = endOfYear(year); + const yearIntervalStartDate = isSameYear(startDate, yearStartDate) + ? startDate + : yearStartDate; + const yearIntervalEndDate = isSameYear(endDate, yearEndDate) + ? endDate + : yearEndDate; + + const { chart: yearChart } = await this.getPerformance({ + end: yearIntervalEndDate, + start: yearIntervalStartDate + }); + + const yearPerformanceItem = { + ...(yearChart.at(-1) ?? ({} as HistoricalDataItem)), + date: format(yearStartDate, DATE_FORMAT) + }; + + chart.push(yearPerformanceItem); + } + } + + return { chart }; + } + public async getSnapshot() { await this.snapshotPromise; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 73d4320d6..f9758d0b9 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -510,6 +510,7 @@ export class PortfolioController { @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, @Query('dataSource') filterByDataSource?: string, + @Query('groupBy') groupBy?: Extract, @Query('range') dateRange: DateRange = 'max', @Query('symbol') filterBySymbol?: string, @Query('tags') filterByTags?: string, @@ -528,6 +529,7 @@ export class PortfolioController { const performanceInformation = await this.portfolioService.getPerformance({ dateRange, filters, + groupBy, impersonationId, withExcludedAccounts, userId: this.request.user.id diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 5dab27939..d59875575 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -991,11 +991,13 @@ export class PortfolioService { public async getPerformance({ dateRange = 'max', filters, + groupBy, impersonationId, userId }: { dateRange?: DateRange; filters?: Filter[]; + groupBy?: Extract; impersonationId: string; userId: string; withExcludedAccounts?: boolean; @@ -1049,11 +1051,24 @@ export class PortfolioService { const { endDate, startDate } = getIntervalFromDateRange(dateRange); - const { chart } = await portfolioCalculator.getPerformance({ + const { chart: intervalChart } = await portfolioCalculator.getPerformance({ end: endDate, start: startDate }); + let chart = intervalChart; + + if (groupBy) { + const { chart: groupedChart } = + await portfolioCalculator.getPerformanceByGroup({ + startDate, + endDate, + groupBy + }); + + chart = groupedChart; + } + const { netPerformance, netPerformanceInPercentage, @@ -1063,7 +1078,7 @@ export class PortfolioService { totalInvestment, totalInvestmentValueWithCurrencyEffect, valueWithCurrencyEffect - } = chart?.at(-1) ?? { + } = intervalChart?.at(-1) ?? { netPerformance: 0, netPerformanceInPercentage: 0, netPerformanceInPercentageWithCurrencyEffect: 0, From 81a79ab4820c5ba8f0dfcd4b7ce65ecc2f80b321 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Mon, 16 Mar 2026 04:13:31 +0100 Subject: [PATCH 2/3] unit tests extended --- .../calculator/portfolio-calculator.ts | 131 +++++++----------- ...tfolio-calculator-baln-buy-and-buy.spec.ts | 17 +++ ...aln-buy-and-sell-in-two-activities.spec.ts | 17 +++ ...folio-calculator-baln-buy-and-sell.spec.ts | 17 +++ .../portfolio-calculator-baln-buy.spec.ts | 17 +++ .../roai/portfolio-calculator-btceur.spec.ts | 24 ++++ ...ator-btcusd-buy-and-sell-partially.spec.ts | 41 ++++++ .../roai/portfolio-calculator-btcusd.spec.ts | 24 ++++ .../portfolio-calculator-googl-buy.spec.ts | 17 +++ ...jnug-buy-and-sell-and-buy-and-sell.spec.ts | 17 +++ .../portfolio-calculator-no-orders.spec.ts | 9 ++ ...ulator-novn-buy-and-sell-partially.spec.ts | 17 +++ .../src/app/portfolio/portfolio.service.ts | 12 +- 13 files changed, 277 insertions(+), 83 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 467449090..81095b511 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -18,12 +18,7 @@ import { PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_HIGH, PORTFOLIO_SNAPSHOT_COMPUTATION_QUEUE_PRIORITY_LOW } from '@ghostfolio/common/config'; -import { - DATE_FORMAT, - getSum, - parseDate, - resetHours -} from '@ghostfolio/common/helper'; +import { DATE_FORMAT, getSum, parseDate } from '@ghostfolio/common/helper'; import { Activity, AssetProfileIdentifier, @@ -51,14 +46,13 @@ import { format, isAfter, isBefore, - isSameYear, isWithinInterval, min, startOfDay, startOfYear, subDays } from 'date-fns'; -import { isNumber, sortBy, sum, uniqBy } from 'lodash'; +import { groupBy as ldGroupBy, isNumber, sortBy, sum, uniqBy } from 'lodash'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; @@ -717,105 +711,86 @@ export abstract class PortfolioCalculator { return this.snapshot.totalLiabilitiesWithCurrencyEffect; } - public async getPerformance({ end, start }) { - await this.snapshotPromise; - - const { historicalData } = this.snapshot; - + public async getPerformance({ data }: { data: HistoricalDataItem[] }) { const chart: HistoricalDataItem[] = []; let netPerformanceAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number; const totalInvestmentValuesWithCurrencyEffect: number[] = []; - for (const historicalDataItem of historicalData) { - const date = resetHours(parseDate(historicalDataItem.date)); + for (const historicalDataItem of data) { + if (!isNumber(netPerformanceAtStartDate)) { + netPerformanceAtStartDate = historicalDataItem.netPerformance; - if (!isBefore(date, start) && !isAfter(date, end)) { - if (!isNumber(netPerformanceAtStartDate)) { - netPerformanceAtStartDate = historicalDataItem.netPerformance; + netPerformanceWithCurrencyEffectAtStartDate = + historicalDataItem.netPerformanceWithCurrencyEffect; + } - netPerformanceWithCurrencyEffectAtStartDate = - historicalDataItem.netPerformanceWithCurrencyEffect; - } + const netPerformanceSinceStartDate = + historicalDataItem.netPerformance - netPerformanceAtStartDate; - const netPerformanceSinceStartDate = - historicalDataItem.netPerformance - netPerformanceAtStartDate; + const netPerformanceWithCurrencyEffectSinceStartDate = + historicalDataItem.netPerformanceWithCurrencyEffect - + netPerformanceWithCurrencyEffectAtStartDate; - const netPerformanceWithCurrencyEffectSinceStartDate = - historicalDataItem.netPerformanceWithCurrencyEffect - - netPerformanceWithCurrencyEffectAtStartDate; + if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) { + totalInvestmentValuesWithCurrencyEffect.push( + historicalDataItem.totalInvestmentValueWithCurrencyEffect + ); + } - if (historicalDataItem.totalInvestmentValueWithCurrencyEffect > 0) { - totalInvestmentValuesWithCurrencyEffect.push( - historicalDataItem.totalInvestmentValueWithCurrencyEffect - ); - } + const timeWeightedInvestmentValue = + totalInvestmentValuesWithCurrencyEffect.length > 0 + ? sum(totalInvestmentValuesWithCurrencyEffect) / + totalInvestmentValuesWithCurrencyEffect.length + : 0; - const timeWeightedInvestmentValue = - totalInvestmentValuesWithCurrencyEffect.length > 0 - ? sum(totalInvestmentValuesWithCurrencyEffect) / - totalInvestmentValuesWithCurrencyEffect.length - : 0; - - chart.push({ - ...historicalDataItem, - netPerformance: - historicalDataItem.netPerformance - netPerformanceAtStartDate, - netPerformanceWithCurrencyEffect: - netPerformanceWithCurrencyEffectSinceStartDate, - netPerformanceInPercentage: - timeWeightedInvestmentValue === 0 - ? 0 - : netPerformanceSinceStartDate / timeWeightedInvestmentValue, - netPerformanceInPercentageWithCurrencyEffect: - timeWeightedInvestmentValue === 0 - ? 0 - : netPerformanceWithCurrencyEffectSinceStartDate / - timeWeightedInvestmentValue - // TODO: Add net worth - // netWorth: totalCurrentValueWithCurrencyEffect - // .plus(totalAccountBalanceWithCurrencyEffect) - // .toNumber() - // netWorth: 0 - }); - } + chart.push({ + ...historicalDataItem, + netPerformance: + historicalDataItem.netPerformance - netPerformanceAtStartDate, + netPerformanceWithCurrencyEffect: + netPerformanceWithCurrencyEffectSinceStartDate, + netPerformanceInPercentage: + timeWeightedInvestmentValue === 0 + ? 0 + : netPerformanceSinceStartDate / timeWeightedInvestmentValue, + netPerformanceInPercentageWithCurrencyEffect: + timeWeightedInvestmentValue === 0 + ? 0 + : netPerformanceWithCurrencyEffectSinceStartDate / + timeWeightedInvestmentValue + // TODO: Add net worth + // netWorth: totalCurrentValueWithCurrencyEffect + // .plus(totalAccountBalanceWithCurrencyEffect) + // .toNumber() + // netWorth: 0 + }); } return { chart }; } public async getPerformanceByGroup({ - endDate, - groupBy, - startDate + data, + groupBy }: { - endDate: Date; - groupBy: GroupBy; - startDate: Date; + data: HistoricalDataItem[]; + groupBy: Extract; }) { - const interval = { start: startDate, end: endDate }; const chart: HistoricalDataItem[] = []; if (groupBy === 'year') { - for (const year of eachYearOfInterval(interval)) { - const yearStartDate = startOfYear(year); - const yearEndDate = endOfYear(year); - const yearIntervalStartDate = isSameYear(startDate, yearStartDate) - ? startDate - : yearStartDate; - const yearIntervalEndDate = isSameYear(endDate, yearEndDate) - ? endDate - : yearEndDate; + const dataByYear = ldGroupBy(data, (item) => item.date.slice(0, 4)); + for (const year of Object.keys(dataByYear)) { const { chart: yearChart } = await this.getPerformance({ - end: yearIntervalEndDate, - start: yearIntervalStartDate + data: Object.values(dataByYear[year]) }); const yearPerformanceItem = { ...(yearChart.at(-1) ?? ({} as HistoricalDataItem)), - date: format(yearStartDate, DATE_FORMAT) + date: format(startOfYear(year), DATE_FORMAT) }; chart.push(yearPerformanceItem); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts index 9a93d0419..71eda96f5 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts @@ -138,6 +138,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('595.6'), errors: [], @@ -212,6 +219,16 @@ describe('PortfolioCalculator', () => { expect(investmentsByYear).toEqual([ { date: '2021-01-01', investment: 559 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2021-01-01', + netPerformance: 33.4, + netPerformanceInPercentage: 0.06986689805847808, + netPerformanceInPercentageWithCurrencyEffect: 0.06986689805847808, + netPerformanceWithCurrencyEffect: 33.4 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index c876d0db1..299aa639f 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -154,6 +154,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), errors: [], @@ -226,6 +233,16 @@ describe('PortfolioCalculator', () => { expect(investmentsByYear).toEqual([ { date: '2021-01-01', investment: 0 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2021-01-01', + netPerformance: -15.8, + netPerformanceInPercentage: -0.05528341497550735, + netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550735, + netPerformanceWithCurrencyEffect: -15.8 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index ae921d6d9..c42bcf05e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -138,6 +138,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), errors: [], @@ -210,6 +217,16 @@ describe('PortfolioCalculator', () => { expect(investmentsByYear).toEqual([ { date: '2021-01-01', investment: 0 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2021-01-01', + netPerformance: -15.8, + netPerformanceInPercentage: -0.05528341497550735, + netPerformanceInPercentageWithCurrencyEffect: -0.05528341497550735, + netPerformanceWithCurrencyEffect: -15.8 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index 6207f1417..5d5f2acc5 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -128,6 +128,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('297.8'), errors: [], @@ -209,6 +216,16 @@ describe('PortfolioCalculator', () => { expect(investmentsByYear).toEqual([ { date: '2021-01-01', investment: 273.2 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2021-01-01', + netPerformance: 23.05, + netPerformanceInPercentage: 0.08437042459736459, + netPerformanceInPercentageWithCurrencyEffect: 0.08437042459736459, + netPerformanceWithCurrencyEffect: 23.05 + }) + ]); }); it.only('with BALN.SW buy (with unit price lower than closing price)', async () => { diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index 055356325..64a65b75c 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -137,6 +137,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot.historicalData[0]).toEqual({ date: '2021-12-11', investmentValueWithCurrencyEffect: 0, @@ -255,6 +262,23 @@ describe('PortfolioCalculator', () => { { date: '2021-01-01', investment: 44558.42 }, { date: '2022-01-01', investment: 0 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2021-01-01', + netPerformance: -1463.18, + netPerformanceInPercentage: -0.03283734028271199, + netPerformanceInPercentageWithCurrencyEffect: -0.03283734028271199, + netPerformanceWithCurrencyEffect: -1463.18 + }), + expect.objectContaining({ + date: '2022-01-01', + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index 11765fc49..bcbb60c57 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -151,6 +151,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('13298.425356'), errors: [], @@ -266,6 +273,40 @@ describe('PortfolioCalculator', () => { { date: '2017-01-01', investment: -318.54266729999995 }, { date: '2018-01-01', investment: 0 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2014-01-01', + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0 + }), + expect.objectContaining({ + date: '2015-01-01', + netPerformance: 25984.861407, + netPerformanceInPercentage: 40.787097105782344, + netPerformanceInPercentageWithCurrencyEffect: 41.893291864515064, + netPerformanceWithCurrencyEffect: 26689.601865 + }), + expect.objectContaining({ + date: '2016-01-01', + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0 + }), + expect.objectContaining({ + date: '2017-01-01', + netPerformance: 972.1720319999986, + netPerformanceInPercentage: 1.5306926710921496, + netPerformanceInPercentageWithCurrencyEffect: 0.6224618479468005, + netPerformanceWithCurrencyEffect: 395.3373599999977 + }), + expect.objectContaining({ + date: '2018-01-01' + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index 64882061f..024f24e0e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -137,6 +137,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot.historicalData[0]).toEqual({ date: '2021-12-11', investmentValueWithCurrencyEffect: 0, @@ -255,6 +262,23 @@ describe('PortfolioCalculator', () => { { date: '2021-01-01', investment: 44558.42 }, { date: '2022-01-01', investment: 0 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2021-01-01', + netPerformance: -1463.18, + netPerformanceInPercentage: -0.03283734028271199, + netPerformanceInPercentageWithCurrencyEffect: -0.03283734028271199, + netPerformanceWithCurrencyEffect: -1463.18 + }), + expect.objectContaining({ + date: '2022-01-01', + netPerformance: 0, + netPerformanceInPercentage: 0, + netPerformanceInPercentageWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts index 122a9aaed..961d59d60 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts @@ -134,6 +134,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('103.10483'), errors: [], @@ -228,6 +235,16 @@ describe('PortfolioCalculator', () => { expect(investmentsByYear).toEqual([ { date: '2023-01-01', investment: 82.329056 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2023-01-01', + netPerformance: 23.312582, + netPerformanceInPercentage: 0.2831634799747973, + netPerformanceInPercentageWithCurrencyEffect: 0.2411296201428566, + netPerformanceWithCurrencyEffect: 19.851974 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts index d5b22e864..725b26339 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts @@ -134,6 +134,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), errors: [], @@ -185,6 +192,16 @@ describe('PortfolioCalculator', () => { expect(investmentsByYear).toEqual([ { date: '2025-01-01', investment: 0 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2025-01-01', + netPerformance: 39.95, + netPerformanceInPercentage: 0.020208978362720148, + netPerformanceInPercentageWithCurrencyEffect: 0.020208978362720148, + netPerformanceWithCurrencyEffect: 39.95 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts index 6c47af7ca..42730cb6e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts @@ -98,6 +98,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big(0), hasErrors: false, @@ -115,6 +122,8 @@ describe('PortfolioCalculator', () => { expect(investmentsByMonth).toEqual([]); expect(investmentsByYear).toEqual([]); + + expect(performanceByYear.chart).toEqual([]); }); }); }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index 3034e3a1f..ac2f7184a 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -134,6 +134,13 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); + const performanceByYear = await portfolioCalculator.getPerformanceByGroup( + { + data: portfolioSnapshot.historicalData, + groupBy: 'year' + } + ); + expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('87.8'), errors: [], @@ -208,6 +215,16 @@ describe('PortfolioCalculator', () => { expect(investmentsByYear).toEqual([ { date: '2022-01-01', investment: 75.8 } ]); + + expect(performanceByYear.chart).toEqual([ + expect.objectContaining({ + date: '2022-01-01', + netPerformance: 17.68, + netPerformanceInPercentage: 0.12348284960422161, + netPerformanceInPercentageWithCurrencyEffect: 0.12348284960422161, + netPerformanceWithCurrencyEffect: 17.68 + }) + ]); }); }); }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index d59875575..5b22fa187 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1006,6 +1006,8 @@ export class PortfolioService { const user = await this.userService.user({ id: userId }); const userCurrency = this.getUserCurrency(user); + const { endDate, startDate } = getIntervalFromDateRange(dateRange); + const [accountBalanceItems, { activities }] = await Promise.all([ this.accountBalanceService.getAccountBalanceItems({ filters, @@ -1049,11 +1051,12 @@ export class PortfolioService { const { errors, hasErrors, historicalData } = await portfolioCalculator.getSnapshot(); - const { endDate, startDate } = getIntervalFromDateRange(dateRange); + const items = historicalData.filter(({ date }) => { + return !isBefore(date, startDate) && !isAfter(date, endDate); + }); const { chart: intervalChart } = await portfolioCalculator.getPerformance({ - end: endDate, - start: startDate + data: items }); let chart = intervalChart; @@ -1061,8 +1064,7 @@ export class PortfolioService { if (groupBy) { const { chart: groupedChart } = await portfolioCalculator.getPerformanceByGroup({ - startDate, - endDate, + data: items, groupBy }); From 0c4ca8f3ac538c8004a84133e0c3e6f286372054 Mon Sep 17 00:00:00 2001 From: Attila Cseh Date: Mon, 16 Mar 2026 04:31:46 +0100 Subject: [PATCH 3/3] portfolioCalculator performance calculation redundant async removed --- .../app/portfolio/calculator/portfolio-calculator.ts | 6 +++--- .../roai/portfolio-calculator-baln-buy-and-buy.spec.ts | 10 ++++------ ...culator-baln-buy-and-sell-in-two-activities.spec.ts | 10 ++++------ .../portfolio-calculator-baln-buy-and-sell.spec.ts | 10 ++++------ .../roai/portfolio-calculator-baln-buy.spec.ts | 10 ++++------ .../roai/portfolio-calculator-btceur.spec.ts | 10 ++++------ ...io-calculator-btcusd-buy-and-sell-partially.spec.ts | 10 ++++------ .../roai/portfolio-calculator-btcusd.spec.ts | 10 ++++------ .../roai/portfolio-calculator-googl-buy.spec.ts | 10 ++++------ ...lculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts | 10 ++++------ .../roai/portfolio-calculator-no-orders.spec.ts | 10 ++++------ ...olio-calculator-novn-buy-and-sell-partially.spec.ts | 10 ++++------ apps/api/src/app/portfolio/portfolio.service.ts | 9 +++++---- 13 files changed, 52 insertions(+), 73 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 81095b511..bf5ac055d 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -711,7 +711,7 @@ export abstract class PortfolioCalculator { return this.snapshot.totalLiabilitiesWithCurrencyEffect; } - public async getPerformance({ data }: { data: HistoricalDataItem[] }) { + public getPerformance({ data }: { data: HistoricalDataItem[] }) { const chart: HistoricalDataItem[] = []; let netPerformanceAtStartDate: number; @@ -771,7 +771,7 @@ export abstract class PortfolioCalculator { return { chart }; } - public async getPerformanceByGroup({ + public getPerformanceByGroup({ data, groupBy }: { @@ -784,7 +784,7 @@ export abstract class PortfolioCalculator { const dataByYear = ldGroupBy(data, (item) => item.date.slice(0, 4)); for (const year of Object.keys(dataByYear)) { - const { chart: yearChart } = await this.getPerformance({ + const { chart: yearChart } = this.getPerformance({ data: Object.values(dataByYear[year]) }); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts index 71eda96f5..7f6bd3623 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-buy.spec.ts @@ -138,12 +138,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('595.6'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts index 299aa639f..7eea1b609 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell-in-two-activities.spec.ts @@ -154,12 +154,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts index c42bcf05e..d952ed709 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy-and-sell.spec.ts @@ -138,12 +138,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts index 5d5f2acc5..f0d4ae491 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-baln-buy.spec.ts @@ -128,12 +128,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('297.8'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts index 64a65b75c..c6ac9e3e3 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btceur.spec.ts @@ -137,12 +137,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot.historicalData[0]).toEqual({ date: '2021-12-11', diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts index bcbb60c57..a61e0a62d 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd-buy-and-sell-partially.spec.ts @@ -151,12 +151,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('13298.425356'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts index 024f24e0e..406792ba7 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-btcusd.spec.ts @@ -137,12 +137,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot.historicalData[0]).toEqual({ date: '2021-12-11', diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts index 961d59d60..8374e4a8e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-buy.spec.ts @@ -134,12 +134,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('103.10483'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts index 725b26339..9d49f5c9e 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-jnug-buy-and-sell-and-buy-and-sell.spec.ts @@ -134,12 +134,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts index 42730cb6e..2dc3bd56c 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-no-orders.spec.ts @@ -98,12 +98,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big(0), diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts index ac2f7184a..8e20b03e6 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-novn-buy-and-sell-partially.spec.ts @@ -134,12 +134,10 @@ describe('PortfolioCalculator', () => { groupBy: 'year' }); - const performanceByYear = await portfolioCalculator.getPerformanceByGroup( - { - data: portfolioSnapshot.historicalData, - groupBy: 'year' - } - ); + const performanceByYear = portfolioCalculator.getPerformanceByGroup({ + data: portfolioSnapshot.historicalData, + groupBy: 'year' + }); expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('87.8'), diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 5b22fa187..103d95a7a 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1055,18 +1055,19 @@ export class PortfolioService { return !isBefore(date, startDate) && !isAfter(date, endDate); }); - const { chart: intervalChart } = await portfolioCalculator.getPerformance({ + const { chart: intervalChart } = portfolioCalculator.getPerformance({ data: items }); let chart = intervalChart; if (groupBy) { - const { chart: groupedChart } = - await portfolioCalculator.getPerformanceByGroup({ + const { chart: groupedChart } = portfolioCalculator.getPerformanceByGroup( + { data: items, groupBy - }); + } + ); chart = groupedChart; }