From a0b3d1f71a0a26e9e1eadc895948324e56e56d9a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 7 Oct 2026 08:51:40 +0200 Subject: [PATCH] Bugfix/start date of calendar year date ranges in portfolio performance (#8077) * Fix start date of calendar year date ranges in portfolio performance * Update changelog --- CHANGELOG.md | 1 + .../benchmarks/benchmarks.service.ts | 4 ++-- .../calculator/portfolio-calculator.ts | 7 ++++++- ...ator-btcusd-buy-and-sell-partially.spec.ts | 21 +++++++++++++++---- 4 files changed, 26 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603da63dff..7512b27d80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fixed the visibility of the asset class, asset sub class, fee and quantity fields of a valuable in the create activity dialog - Fixed the missing mapping for Aland Islands in the country weightings of the _Financial Modeling Prep_ service - Fixed the net performance percentage of date ranges in the portfolio performance calculation by weighting the average investment by the number of days between the chart dates +- Fixed the start date of calendar year date ranges in the portfolio performance calculation ## 3.80.2 - 2026-10-06 diff --git a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts index 908db59b7f..d807b1bb6b 100644 --- a/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts +++ b/apps/api/src/app/endpoints/benchmarks/benchmarks.service.ts @@ -13,7 +13,7 @@ import { import { DateRange } from '@ghostfolio/common/types'; import { Injectable, Logger } from '@nestjs/common'; -import { format, isSameDay } from 'date-fns'; +import { format, isSameDay, min } from 'date-fns'; import { isNumber } from 'lodash-es'; @Injectable() @@ -96,8 +96,8 @@ export class BenchmarksService { const exchangeRates = await this.exchangeRateDataService.getExchangeRatesByCurrency({ - startDate, currencies: [currentSymbolItem.currency], + startDate: min([baselineDate, startDate]), targetCurrency: userCurrency }); diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 1154f897d1..7a8bbcc741 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -64,6 +64,7 @@ import { format, isAfter, isBefore, + isEqual, isFuture, isPast, isWithinInterval, @@ -1417,10 +1418,14 @@ export abstract class PortfolioCalculator { let netPerformanceAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number; + const startDate = isEqual(start, endOfDay(start)) + ? resetHours(start) + : start; + for (const historicalDataItem of historicalData) { const date = resetHours(parseDate(historicalDataItem.date)); - if (!isBefore(date, start) && !isAfter(date, end)) { + if (!isBefore(date, startDate) && !isAfter(date, end)) { // Take the values at the start date from the first day of the date // range if (historicalDataItemsOfDateRange.length === 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 0110f68f27..0be9af1c00 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 @@ -13,6 +13,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate- import { ExchangeRateDataServiceMock } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service.mock'; 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 { parseDate } from '@ghostfolio/common/helper'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; @@ -168,6 +169,7 @@ describe('PortfolioCalculator', () => { }, netPerformanceWithCurrencyEffectMap: { '1d': new Big('-556.443324'), + '2017': new Big('27081.23736'), max: new Big('26516.208701400000064086'), ytd: new Big('-556.443324') }, @@ -262,8 +264,8 @@ describe('PortfolioCalculator', () => { '2017': { date: '2017-12-31', netPerformance: 26957.033439, - netPerformanceInPercentage: 44.109822737327406, - netPerformanceInPercentageWithCurrencyEffect: 43.14859823223876, + netPerformanceInPercentage: 44.10965416175681, + netPerformanceInPercentageWithCurrencyEffect: 43.148434302822835, netPerformanceWithCurrencyEffect: 27081.23736, totalInvestmentValueWithCurrencyEffect: 318.54266729999995, valueWithCurrencyEffect: 13854.86868 @@ -287,6 +289,17 @@ describe('PortfolioCalculator', () => { valueWithCurrencyEffect: 13298.425356 } }); + + const { endDate, startDate } = getIntervalFromDateRange({ + dateRange: '2017' + }); + + const { chart } = await portfolioCalculator.getPerformance({ + end: endDate, + start: startDate + }); + + expect(chart[0].date).toBe('2016-12-31'); }); it.only('with BTCUSD buy and sell partially and fewer chart items', async () => { @@ -333,12 +346,12 @@ describe('PortfolioCalculator', () => { // Other weights give another rounding of the floating point numbers expect( performanceByDateRange['2017'].netPerformanceInPercentage - ).toBeCloseTo(44.109822737327406, 10); + ).toBeCloseTo(44.10965416175681, 10); expect( performanceByDateRange['2017'] .netPerformanceInPercentageWithCurrencyEffect - ).toBeCloseTo(43.14859823223876, 10); + ).toBeCloseTo(43.148434302822835, 10); expect(performanceByDateRange.max.netPerformanceInPercentage).toBeCloseTo( 42.43911719562077,