From e5a992fc8073c224dadf9dc36a51a3814000adce Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:10:40 +0200 Subject: [PATCH] Add groupBy=year to portfolio performance endpoint --- .../app/portfolio/portfolio.service.spec.ts | 51 +++++++++++++++++-- .../src/app/portfolio/portfolio.service.ts | 6 ++- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index 42042eb9e9..c6bf33a11b 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -16,7 +16,7 @@ import { TAG_ID_EXCLUDE_FROM_ANALYSIS, UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { parseDate } from '@ghostfolio/common/helper'; +import { parseDate, resetHours } from '@ghostfolio/common/helper'; import { Activity, AssetProfileIdentifier, @@ -946,8 +946,7 @@ describe('PortfolioService', () => { }); // A year runs from the end of 31 December of the previous year, like - // the date range of the year. The first year is clipped to the date - // range, and the current year ends today. + // the date range of the year, and the current year ends today expect(getPerformanceOfCalculator).toHaveBeenCalledTimes(4); expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(2, { end: endOfDay(parseDate('2023-12-31')), @@ -1004,6 +1003,52 @@ describe('PortfolioService', () => { expect(chart).toEqual([{ date: '2024-01-01', netPerformance: 200 }]); expect(performance.netPerformance).toBe(200); }); + + it('should clip the first calendar year to the date range when grouped by year', async () => { + getPerformanceOfCalculator + .mockResolvedValueOnce({ + chart: [ + { date: '2024-06-15', netPerformance: 0 }, + { date: '2024-12-31', netPerformance: 100 }, + { date: '2025-06-15', netPerformance: 300 } + ] + }) + .mockResolvedValueOnce({ + chart: [ + { date: '2024-06-15', netPerformance: 0 }, + { date: '2024-12-31', netPerformance: 100 } + ] + }) + .mockResolvedValueOnce({ + chart: [ + { date: '2024-12-31', netPerformance: 0 }, + { date: '2025-06-15', netPerformance: 200 } + ] + }); + + const { chart, performance } = await portfolioService.getPerformance({ + dateRange: '1y', + groupBy: 'year', + userId: userDummyData.id + }); + + // The first year starts at the start date of the date range, not at the + // end of 31 December of the previous year + expect(getPerformanceOfCalculator).toHaveBeenCalledTimes(3); + expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(2, { + end: endOfDay(parseDate('2024-12-31')), + start: resetHours(parseDate('2024-06-15')) + }); + expect(getPerformanceOfCalculator).toHaveBeenNthCalledWith(3, { + end: endOfDay(parseDate('2025-06-15')), + start: endOfDay(parseDate('2024-12-31')) + }); + expect(chart).toEqual([ + { date: '2024-01-01', netPerformance: 100 }, + { date: '2025-01-01', netPerformance: 200 } + ]); + expect(performance.netPerformance).toBe(300); + }); }); describe('getSummary', () => { diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 69d7d4638b..63f47df5b0 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -2097,10 +2097,12 @@ export class PortfolioService { for (const year of years) { const { endDate: endDateOfYear, startDate: startDateOfYear } = - getIntervalFromDateRange({ dateRange: year, startDate }); + getIntervalFromDateRange({ startDate, dateRange: year }); + + const end = min([endDate, endDateOfYear]); const { chart: chartOfYear } = await portfolioCalculator.getPerformance({ - end: min([endDate, endDateOfYear]), + end, start: startDateOfYear });