diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b268a78c..987a872351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,14 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## 3.81.0 - 2026-10-07 +## Unreleased ### Added - Added the _Return on Investment_ (ROI) performance calculation type (experimental) +## 3.81.0 - 2026-10-07 + ### Changed - Moved the performance calculation including dividends (total return) from experimental to general availability diff --git a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts index 8400c0167f..11583490e9 100644 --- a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roi/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'; @@ -116,13 +113,21 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { ? differenceInDays(new Date(), dateOfFirstActivity) : 0; - // Take the dividend from the same source as the portfolio summary, so - // that the response shows one dividend only const totalDividendInBaseCurrency = this.getDividendInBaseCurrencyOfHoldings(positions); - const hasDividendWithoutInvestedCapital = - this.hasDividendWithoutInvestedCapital(positions); + // A holding without a market price, and a holding which is excluded from + // the performance, gives a dividend but no invested capital. Such a + // holding makes the dividend yield too high. Therefore the dividend yield + // stays 0 in this case. + const hasDividendWithoutInvestedCapital = positions.some( + ({ averageInvestment, dividendInBaseCurrency, includeInPerformance }) => { + return ( + !dividendInBaseCurrency.eq(0) && + (!includeInPerformance || averageInvestment.eq(0)) + ); + } + ); const dividendYieldPercent = getAnnualizedPerformancePercent({ daysInMarket, @@ -166,15 +171,10 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { } protected calculatePerformancePercentages({ - accumulatedValuesByDate, - holdings + accumulatedValuesByDate }: { accumulatedValuesByDate: { [date: string]: AccumulatedValues }; - holdings: PortfolioSnapshotHolding[]; }): { [date: string]: PerformancePercentages } { - const hasDividendWithoutInvestedCapital = - this.hasDividendWithoutInvestedCapital(holdings); - const performancePercentagesByDate: { [date: string]: PerformancePercentages; } = {}; @@ -192,7 +192,6 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { ] of Object.entries(accumulatedValuesByDate)) { performancePercentagesByDate[date] = { dividendInPercentageWithCurrencyEffect: - hasDividendWithoutInvestedCapital || totalInvestedCapitalWithCurrencyEffect.eq(0) ? 0 : totalDividendValueWithCurrencyEffect @@ -214,15 +213,10 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { } protected calculatePerformancePercentagesForDateRange({ - historicalDataItems, - holdings + historicalDataItems }: { historicalDataItems: HistoricalDataItem[]; - holdings: PortfolioSnapshotHolding[]; }): { [date: string]: PerformancePercentages } { - const hasDividendWithoutInvestedCapital = - this.hasDividendWithoutInvestedCapital(holdings); - let investedCapital = 0; let investedCapitalWithCurrencyEffect = 0; @@ -257,7 +251,6 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { performancePercentagesByDate[historicalDataItem.date] = { dividendInPercentageWithCurrencyEffect: - !hasDividendWithoutInvestedCapital && investedCapitalWithCurrencyEffect > 0 ? historicalDataItem.dividendInBaseCurrency / investedCapitalWithCurrencyEffect @@ -376,6 +369,7 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { const { currentValues, currentValuesWithCurrencyEffect, + dividendValuesWithCurrencyEffect, initialValue, investmentValuesAccumulated, investmentValuesAccumulatedWithCurrencyEffect, @@ -557,6 +551,7 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { return { currentValues, currentValuesWithCurrencyEffect, + dividendValuesWithCurrencyEffect, dividendYieldPercent, dividendYieldPercentWithCurrencyEffect, grossPerformancePercentage, @@ -591,21 +586,4 @@ export class RoiPortfolioCalculator extends PortfolioCalculator { protected getPerformanceCalculationType() { return PerformanceCalculationType.ROI; } - - private hasDividendWithoutInvestedCapital( - holdings: PortfolioSnapshotHolding[] - ) { - // A holding with a dividend only (without a buy activity), and a holding - // without a market price, gives a dividend but no invested capital. 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) - ); - } - ); - } }