From c88592ec53d85fd741739c47419537b2747e9842 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Tue, 6 Oct 2026 16:00:01 +0200 Subject: [PATCH] Include dividend of holdings without average investment in dividend percentage --- .../calculator/portfolio-calculator.ts | 14 ++----- ...-calculator-googl-fee-and-dividend.spec.ts | 12 +++--- .../calculator/roai/portfolio-calculator.ts | 38 ++----------------- 3 files changed, 13 insertions(+), 51 deletions(-) diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index a03e318c93..9b94545fd8 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -223,19 +223,15 @@ export abstract class PortfolioCalculator { ): PortfolioSnapshot; protected abstract calculatePerformancePercentages({ - accumulatedValuesByDate, - holdings + accumulatedValuesByDate }: { accumulatedValuesByDate: { [date: string]: AccumulatedValues }; - holdings: PortfolioSnapshotHolding[]; }): { [date: string]: PerformancePercentages }; protected abstract calculatePerformancePercentagesForDateRange({ - historicalDataItems, - holdings + historicalDataItems }: { historicalDataItems: HistoricalDataItem[]; - holdings: PortfolioSnapshotHolding[]; }): { [date: string]: PerformancePercentages }; @LogPerformance @@ -680,8 +676,7 @@ export abstract class PortfolioCalculator { }); const performancePercentagesByDate = this.calculatePerformancePercentages({ - accumulatedValuesByDate, - holdings: positionsIncludedInHoldings + accumulatedValuesByDate }); const historicalData: HistoricalDataItem[] = Object.entries( @@ -1443,8 +1438,7 @@ export abstract class PortfolioCalculator { const performancePercentagesByDate = this.calculatePerformancePercentagesForDateRange({ - historicalDataItems: historicalDataItemsOfDateRange, - holdings: this.snapshot.positions + historicalDataItems: historicalDataItemsOfDateRange }); const chart = historicalDataItemsOfDateRange.map((historicalDataItem) => { diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts index bdf5105ad5..18929a680c 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-googl-fee-and-dividend.spec.ts @@ -234,8 +234,8 @@ describe('PortfolioCalculator', () => { unitPriceInAssetProfileCurrency: 0.62 }, { - // The holding has a dividend, but no average investment. Thus the - // dividend percentage stays 0, like the dividend yield + // The holding has a dividend, but no average investment. Its dividend + // is part of the net performance, and thus of the dividend percentage ...activityDummyData, assetProfile: { ...assetProfileDummyData, @@ -274,7 +274,7 @@ describe('PortfolioCalculator', () => { expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({ dividendInBaseCurrency: 5.62, - dividendInPercentageWithCurrencyEffect: 0, + dividendInPercentageWithCurrencyEffect: 0.01882242615044544, netPerformance: 19.87, netPerformanceInPercentage: 0.06654832875611226 }); @@ -282,7 +282,7 @@ describe('PortfolioCalculator', () => { expect(performanceByDateRange).toMatchObject({ max: { dividendInBaseCurrency: 5.62, - dividendInPercentageWithCurrencyEffect: 0, + dividendInPercentageWithCurrencyEffect: 0.01882242615044535, netPerformance: 19.87, netPerformanceInPercentage: 0.06654832875611194 } @@ -365,7 +365,7 @@ describe('PortfolioCalculator', () => { expect(portfolioSnapshot.historicalData.at(-1)).toMatchObject({ dividendInBaseCurrency: 0.62, - dividendInPercentageWithCurrencyEffect: 0, + dividendInPercentageWithCurrencyEffect: 0.0020764954116149776, netPerformance: 14.87, netPerformanceInPercentage: 0.0498023980172818 }); @@ -373,7 +373,7 @@ describe('PortfolioCalculator', () => { expect(performanceByDateRange).toMatchObject({ max: { dividendInBaseCurrency: 0.62, - dividendInPercentageWithCurrencyEffect: 0, + dividendInPercentageWithCurrencyEffect: 0.002076495411614967, netPerformance: 14.87, netPerformanceInPercentage: 0.049802398017281556 } diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts index c0261d4e22..33b9ec68d9 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -17,10 +17,7 @@ import { AssetProfileIdentifier, HistoricalDataItem } from '@ghostfolio/common/interfaces'; -import { - PortfolioSnapshot, - PortfolioSnapshotHolding -} from '@ghostfolio/common/models'; +import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { DateRange } from '@ghostfolio/common/types'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; @@ -168,15 +165,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } protected calculatePerformancePercentages({ - accumulatedValuesByDate, - holdings + accumulatedValuesByDate }: { accumulatedValuesByDate: { [date: string]: AccumulatedValues }; - holdings: PortfolioSnapshotHolding[]; }): { [date: string]: PerformancePercentages } { - const hasDividendWithoutAverageInvestment = - this.hasDividendWithoutAverageInvestment(holdings); - const performancePercentagesByDate: { [date: string]: PerformancePercentages; } = {}; @@ -193,7 +185,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { ] of Object.entries(accumulatedValuesByDate)) { performancePercentagesByDate[date] = { dividendInPercentageWithCurrencyEffect: - hasDividendWithoutAverageInvestment || totalAverageInvestmentValueWithCurrencyEffect.eq(0) ? 0 : totalDividendValueWithCurrencyEffect @@ -217,15 +208,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { } protected calculatePerformancePercentagesForDateRange({ - historicalDataItems, - holdings + historicalDataItems }: { historicalDataItems: HistoricalDataItem[]; - holdings: PortfolioSnapshotHolding[]; }): { [date: string]: PerformancePercentages } { - const hasDividendWithoutAverageInvestment = - this.hasDividendWithoutAverageInvestment(holdings); - const averageInvestmentValues: number[] = []; const averageInvestmentValuesWithCurrencyEffect: number[] = []; let grossPerformanceAtStartDate: number; @@ -276,7 +262,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { performancePercentagesByDate[historicalDataItem.date] = { dividendInPercentageWithCurrencyEffect: - !hasDividendWithoutAverageInvestment && averageInvestmentValueWithCurrencyEffect > 0 ? historicalDataItem.dividendInBaseCurrency / averageInvestmentValueWithCurrencyEffect @@ -770,21 +755,4 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { protected getPerformanceCalculationType() { return PerformanceCalculationType.ROAI; } - - private hasDividendWithoutAverageInvestment( - holdings: PortfolioSnapshotHolding[] - ) { - // A holding with a dividend only (without a buy activity), and a holding - // without a market price, gives a dividend but no average investment. - // Such a holding makes the dividend percentage too high. Therefore the - // dividend percentage stays 0 in this case, like the dividend yield. - return holdings.some( - ({ averageInvestmentWithCurrencyEffect, dividendInBaseCurrency }) => { - return ( - !dividendInBaseCurrency.eq(0) && - averageInvestmentWithCurrencyEffect.eq(0) - ); - } - ); - } }