From 5a1fb495218b7253cd0a12d6a5f728c474656d5c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:07:21 +0200 Subject: [PATCH] Fix portfolio calculation for holdings with same symbol --- .../calculator/portfolio-calculator.ts | 2 +- ...tor-msft-buy-from-two-data-sources.spec.ts | 9 +++--- .../portfolio/current-rate.service.mock.ts | 32 +++++++++++++++---- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index cfc4de7c7..3a0b063bb 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -1109,7 +1109,7 @@ export abstract class PortfolioCalculator { const items = lastTransactionPoint?.items ?? []; const newItems = items.filter((item) => { - return item.dataSource !== dataSource || item.symbol !== symbol; + return getAssetProfileIdentifier(item) !== assetProfileIdentifier; }); newItems.push(currentTransactionPointItem); diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-from-two-data-sources.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-from-two-data-sources.spec.ts index fd548731e..0aa3a4be3 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-from-two-data-sources.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-msft-buy-from-two-data-sources.spec.ts @@ -142,16 +142,17 @@ describe('PortfolioCalculator', () => { const portfolioSnapshot = await portfolioCalculator.computeSnapshot(); // The holdings must not be aggregated, because they belong to two - // different asset profiles + // different asset profiles. Each holding must be valuated with the + // market price of its own data source. expect(portfolioSnapshot.positions).toEqual([ expect.objectContaining({ activitiesCount: 1, dataSource: 'EOD_HISTORICAL_DATA', investment: new Big('679.02'), - marketPrice: 331.83, + marketPrice: 332.47, quantity: new Big('2'), symbol: 'MSFT', - valueInBaseCurrency: new Big('663.66') + valueInBaseCurrency: new Big('664.94') }), expect.objectContaining({ activitiesCount: 1, @@ -165,7 +166,7 @@ describe('PortfolioCalculator', () => { ]); expect(portfolioSnapshot.currentValueInBaseCurrency).toEqual( - new Big('995.49') + new Big('996.77') ); expect(portfolioSnapshot.totalInvestment).toEqual(new Big('1018.53')); diff --git a/apps/api/src/app/portfolio/current-rate.service.mock.ts b/apps/api/src/app/portfolio/current-rate.service.mock.ts index 8e027f971..7fcacad61 100644 --- a/apps/api/src/app/portfolio/current-rate.service.mock.ts +++ b/apps/api/src/app/portfolio/current-rate.service.mock.ts @@ -1,5 +1,6 @@ import { parseDate, resetHours } from '@ghostfolio/common/helper'; +import { DataSource } from '@prisma/client'; import { addDays, eachDayOfInterval, @@ -12,7 +13,15 @@ import { GetValueObject } from './interfaces/get-value-object.interface'; import { GetValuesObject } from './interfaces/get-values-object.interface'; import { GetValuesParams } from './interfaces/get-values-params.interface'; -function mockGetValue(symbol: string, date: Date) { +function mockGetValue({ + dataSource, + date, + symbol +}: { + dataSource: DataSource; + date: Date; + symbol: string; +}) { switch (symbol) { case '55196015-1365-4560-aa60-8751ae6d18f8': if (isSameDay(parseDate('2022-01-31'), date)) { @@ -83,7 +92,12 @@ function mockGetValue(symbol: string, date: Date) { } else if (isSameDay(parseDate('2023-07-09'), date)) { return { marketPrice: 337.22 }; } else if (isSameDay(parseDate('2023-07-10'), date)) { - return { marketPrice: 331.83 }; + // Deviating market prices per data source to verify that the market + // price is resolved by the asset profile identifier + return { + marketPrice: + dataSource === DataSource.EOD_HISTORICAL_DATA ? 332.47 : 331.83 + }; } return { marketPrice: 0 }; @@ -117,8 +131,11 @@ export const CurrentRateServiceMock = { values.push({ date, dataSource: dataGatheringItem.dataSource, - marketPrice: mockGetValue(dataGatheringItem.symbol, date) - .marketPrice, + marketPrice: mockGetValue({ + date, + dataSource: dataGatheringItem.dataSource, + symbol: dataGatheringItem.symbol + }).marketPrice, symbol: dataGatheringItem.symbol }); } @@ -132,8 +149,11 @@ export const CurrentRateServiceMock = { values.push({ date, dataSource: dataGatheringItem.dataSource, - marketPrice: mockGetValue(dataGatheringItem.symbol, date) - .marketPrice, + marketPrice: mockGetValue({ + date, + dataSource: dataGatheringItem.dataSource, + symbol: dataGatheringItem.symbol + }).marketPrice, symbol: dataGatheringItem.symbol }); }