From 8287c1c7ca162058495be50f4336d238bab45227 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 9 Oct 2026 17:10:22 +0200 Subject: [PATCH] Bugfix/end date of calendar year date ranges in portfolio performance (#8095) * Fix end date of calendar year date ranges * Update changelog --- CHANGELOG.md | 1 + .../calculator/portfolio-calculator.ts | 13 +++--- .../roai/portfolio-calculator.spec.ts | 44 +++++++++++++++++++ 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d87e1535b..0a105dd492 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the missing list numbers in the top and bottom holdings on the analysis page in Safari - Fixed the missing first data point in the portfolio evolution chart on the analysis page +- Fixed the end date of calendar year date ranges in the portfolio performance calculation for instances in time zones with a negative UTC offset - Fixed the missing mapping for Korea in the country weightings of the _Financial Modeling Prep_ service ## 3.81.0 - 2026-10-07 diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 7a8bbcc741..ca440c6d2a 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1407,7 +1407,7 @@ export abstract class PortfolioCalculator { return this.snapshot.totalLiabilitiesWithCurrencyEffect; } - public async getPerformance({ end, start }) { + public async getPerformance({ end, start }: { end: Date; start: Date }) { await this.snapshotPromise; const { historicalData } = this.snapshot; @@ -1418,14 +1418,13 @@ export abstract class PortfolioCalculator { let netPerformanceAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number; - const startDate = isEqual(start, endOfDay(start)) - ? resetHours(start) - : start; + const endDate = this.toChartDate(end); + const startDate = this.toChartDate(start); for (const historicalDataItem of historicalData) { const date = resetHours(parseDate(historicalDataItem.date)); - if (!isBefore(date, startDate) && !isAfter(date, end)) { + if (!isBefore(date, startDate) && !isAfter(date, endDate)) { // Take the values at the start date from the first day of the date // range if (historicalDataItemsOfDateRange.length === 0) { @@ -1915,4 +1914,8 @@ export abstract class PortfolioCalculator { await this.initialize(attempt + 1); } } + + private toChartDate(aDate: Date) { + return isEqual(aDate, endOfDay(aDate)) ? resetHours(aDate) : aDate; + } } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts index 5f8d9cdc44..43ff160a78 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts @@ -1,3 +1,7 @@ +/** + * @jest-environment /jest-environment-tz.js + * @jest-environment-options {"timeZone": "America/New_York"} + */ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; @@ -9,6 +13,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; +import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; @@ -183,4 +188,43 @@ describe('PortfolioCalculator', () => { ).toBeCloseTo(0.23, 10); }); }); + + describe('get performance', () => { + it('with a calendar year date range', async () => { + const snapshot = await portfolioCalculator.getSnapshot(); + + snapshot.historicalData = [ + getHistoricalDataItem({ + date: '2016-12-31', + investment: 100, + netPerformance: 0 + }), + getHistoricalDataItem({ + date: '2017-12-31', + investment: 100, + netPerformance: 10 + }), + getHistoricalDataItem({ + date: '2018-01-01', + investment: 100, + netPerformance: 20 + }) + ]; + + const { endDate, startDate } = getIntervalFromDateRange({ + dateRange: '2017' + }); + + const { chart } = await portfolioCalculator.getPerformance({ + end: endDate, + start: startDate + }); + + expect( + chart.map(({ date }) => { + return date; + }) + ).toEqual(['2016-12-31', '2017-12-31']); + }); + }); });