diff --git a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts index 72e3301fa7..e9f24ccd9b 100644 --- a/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/mwr/portfolio-calculator.ts @@ -1,24 +1,20 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; -import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; -import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; -import { - AssetProfileIdentifier, - HistoricalDataItem -} from '@ghostfolio/common/interfaces'; +import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type'; +import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; export class MwrPortfolioCalculator extends PortfolioCalculator { - protected calculateNetPerformancePercentages({}: { - accumulatedValuesByDate: { [date: string]: AccumulatedValues }; - }): { [date: string]: NetPerformancePercentages } { + protected calculateNetPerformancePercentages(): { + [date: string]: NetPerformancePercentages; + } { throw new Error('Method not implemented.'); } - protected calculateNetPerformancePercentagesForDateRange({}: { - historicalDataItems: HistoricalDataItem[]; - }): { [date: string]: NetPerformancePercentages } { + protected calculateNetPerformancePercentagesForDateRange(): { + [date: string]: NetPerformancePercentages; + } { throw new Error('Method not implemented.'); } diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index c55c68ced7..debfa7d23f 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -4,13 +4,13 @@ import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accu import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; import { HoldingValuationItem } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation-item.interface'; import { HoldingValuation } from '@ghostfolio/api/app/portfolio/interfaces/holding-valuation.interface'; -import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface'; import { PortfolioCalculatorActivity } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity.interface'; import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; import { TransactionPointSymbol } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point-symbol.interface'; import { TransactionPoint } from '@ghostfolio/api/app/portfolio/interfaces/transaction-point.interface'; +import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { getFactor } from '@ghostfolio/api/helper/portfolio.helper'; import { LogPerformance } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; @@ -43,7 +43,10 @@ import { InvestmentItem, ResponseError } from '@ghostfolio/common/interfaces'; -import { PortfolioSnapshot } from '@ghostfolio/common/models'; +import { + PortfolioSnapshot, + PortfolioSnapshotHolding +} from '@ghostfolio/common/models'; import { GroupBy } from '@ghostfolio/common/types'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; @@ -69,7 +72,7 @@ import { startOfYear, subDays } from 'date-fns'; -import { groupBy, isNumber, sortBy, uniqBy } from 'lodash'; +import { groupBy, sortBy, uniqBy } from 'lodash'; export abstract class PortfolioCalculator { protected static readonly ENABLE_LOGGING = false; @@ -854,8 +857,14 @@ export abstract class PortfolioCalculator { public async getDividendInBaseCurrency() { await this.snapshotPromise; + return this.getDividendInBaseCurrencyOfHoldings(this.snapshot.positions); + } + + protected getDividendInBaseCurrencyOfHoldings( + holdings: PortfolioSnapshotHolding[] + ) { return getSum( - this.snapshot.positions.map(({ dividendInBaseCurrency }) => { + holdings.map(({ dividendInBaseCurrency }) => { return dividendInBaseCurrency; }) ); @@ -1350,7 +1359,9 @@ export abstract class PortfolioCalculator { const date = resetHours(parseDate(historicalDataItem.date)); if (!isBefore(date, start) && !isAfter(date, end)) { - if (!isNumber(netPerformanceAtStartDate)) { + // Take the values at the start date from the first day of the date + // range + if (historicalDataItemsOfDateRange.length === 0) { netPerformanceAtStartDate = historicalDataItem.netPerformance; netPerformanceWithCurrencyEffectAtStartDate = 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 fc7dc1dccd..cd4f474ec0 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -1,9 +1,9 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; -import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; import { PortfolioCalculatorActivityItem } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-activity-item.interface'; import { PortfolioCalculatorHolding } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-calculator-holding.interface'; +import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type'; import { getAnnualizedPerformancePercent, getIntervalFromDateRange @@ -29,7 +29,7 @@ import { isBefore, isThisYear } from 'date-fns'; -import { isNumber, sum } from 'lodash'; +import { sum } from 'lodash'; export class RoaiPortfolioCalculator extends PortfolioCalculator { protected calculateNetPerformancePercentages({ @@ -82,8 +82,10 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { [date: string]: NetPerformancePercentages; } = {}; - for (const historicalDataItem of historicalDataItems) { - if (!isNumber(grossPerformanceAtStartDate)) { + for (const [index, historicalDataItem] of historicalDataItems.entries()) { + // Take the values at the start date from the first day of the date + // range + if (index === 0) { grossPerformanceAtStartDate = historicalDataItem.value - historicalDataItem.totalInvestment; @@ -142,7 +144,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { let hasErrors = false; let totalAverageInvestment = new Big(0); let totalAverageInvestmentWithCurrencyEffect = new Big(0); - let totalDividendInBaseCurrency = new Big(0); let totalFeesWithCurrencyEffect = new Big(0); const totalInterestWithCurrencyEffect = new Big(0); let totalInvestment = new Big(0); @@ -161,12 +162,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { continue; } - if (currentPosition.dividendInBaseCurrency) { - totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( - currentPosition.dividendInBaseCurrency - ); - } - if (currentPosition.feeInBaseCurrency) { totalFeesWithCurrencyEffect = totalFeesWithCurrencyEffect.plus( currentPosition.feeInBaseCurrency @@ -215,21 +210,42 @@ export class RoaiPortfolioCalculator 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); + + // A holding without a market price, and a holding which is excluded from + // the performance, gives a dividend but no average investment. Such a + // holding makes the dividend yield too high. Therefore the dividend yield + // stays 0 in this case. + const hasDividendWithoutAverageInvestment = positions.some( + ({ averageInvestment, dividendInBaseCurrency, includeInPerformance }) => { + return ( + !dividendInBaseCurrency.eq(0) && + (!includeInPerformance || averageInvestment.eq(0)) + ); + } + ); + const dividendYieldPercent = getAnnualizedPerformancePercent({ daysInMarket, - netPerformancePercentage: totalAverageInvestment.eq(0) - ? new Big(0) - : totalDividendInBaseCurrency.div(totalAverageInvestment) + netPerformancePercentage: + hasDividendWithoutAverageInvestment || totalAverageInvestment.eq(0) + ? new Big(0) + : totalDividendInBaseCurrency.div(totalAverageInvestment) }); const dividendYieldPercentWithCurrencyEffect = getAnnualizedPerformancePercent({ daysInMarket, - netPerformancePercentage: totalAverageInvestmentWithCurrencyEffect.eq(0) - ? new Big(0) - : totalDividendInBaseCurrency.div( - totalAverageInvestmentWithCurrencyEffect - ) + netPerformancePercentage: + hasDividendWithoutAverageInvestment || + totalAverageInvestmentWithCurrencyEffect.eq(0) + ? new Big(0) + : totalDividendInBaseCurrency.div( + totalAverageInvestmentWithCurrencyEffect + ) }); return { 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 c891643587..5829585ed7 100644 --- a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts @@ -1,24 +1,20 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; -import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; -import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; -import { - AssetProfileIdentifier, - HistoricalDataItem -} from '@ghostfolio/common/interfaces'; +import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type'; +import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; export class RoiPortfolioCalculator extends PortfolioCalculator { - protected calculateNetPerformancePercentages({}: { - accumulatedValuesByDate: { [date: string]: AccumulatedValues }; - }): { [date: string]: NetPerformancePercentages } { + protected calculateNetPerformancePercentages(): { + [date: string]: NetPerformancePercentages; + } { throw new Error('Method not implemented.'); } - protected calculateNetPerformancePercentagesForDateRange({}: { - historicalDataItems: HistoricalDataItem[]; - }): { [date: string]: NetPerformancePercentages } { + protected calculateNetPerformancePercentagesForDateRange(): { + [date: string]: NetPerformancePercentages; + } { throw new Error('Method not implemented.'); } diff --git a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts index a413a7accb..c3877a347c 100644 --- a/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/twr/portfolio-calculator.ts @@ -1,24 +1,20 @@ import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; -import { AccumulatedValues } from '@ghostfolio/api/app/portfolio/interfaces/accumulated-values.interface'; import { HoldingPerformance } from '@ghostfolio/api/app/portfolio/interfaces/holding-performance.interface'; -import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/interfaces/net-performance-percentages.interface'; -import { - AssetProfileIdentifier, - HistoricalDataItem -} from '@ghostfolio/common/interfaces'; +import { NetPerformancePercentages } from '@ghostfolio/api/app/portfolio/types/net-performance-percentages.type'; +import { AssetProfileIdentifier } from '@ghostfolio/common/interfaces'; import { PortfolioSnapshot } from '@ghostfolio/common/models'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; export class TwrPortfolioCalculator extends PortfolioCalculator { - protected calculateNetPerformancePercentages({}: { - accumulatedValuesByDate: { [date: string]: AccumulatedValues }; - }): { [date: string]: NetPerformancePercentages } { + protected calculateNetPerformancePercentages(): { + [date: string]: NetPerformancePercentages; + } { throw new Error('Method not implemented.'); } - protected calculateNetPerformancePercentagesForDateRange({}: { - historicalDataItems: HistoricalDataItem[]; - }): { [date: string]: NetPerformancePercentages } { + protected calculateNetPerformancePercentagesForDateRange(): { + [date: string]: NetPerformancePercentages; + } { throw new Error('Method not implemented.'); } diff --git a/apps/api/src/app/portfolio/interfaces/net-performance-percentages.interface.ts b/apps/api/src/app/portfolio/interfaces/net-performance-percentages.interface.ts deleted file mode 100644 index 460ae2e447..0000000000 --- a/apps/api/src/app/portfolio/interfaces/net-performance-percentages.interface.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; - -export interface NetPerformancePercentages extends Pick< - HistoricalDataItem, - 'netPerformanceInPercentage' | 'netPerformanceInPercentageWithCurrencyEffect' -> {} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index c89982ba92..a9a1a95faf 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -984,12 +984,15 @@ export class PortfolioService { const { activitiesCount, + averageInvestment, + averageInvestmentWithCurrencyEffect, averagePrice, currency, dateOfFirstActivity, dividendInBaseCurrency, - dividendYieldPercent, - dividendYieldPercentWithCurrencyEffect, + dividendYieldPercent: dividendYieldPercentOfSnapshot, + dividendYieldPercentWithCurrencyEffect: + dividendYieldPercentWithCurrencyEffectOfSnapshot, feeInBaseCurrency, grossPerformance, grossPerformancePercentage, @@ -1005,6 +1008,34 @@ export class PortfolioService { tags } = holding; + // TODO: Remove the block below with the next release, when each cached + // portfolio snapshot contains the dividend yield. Then take + // dividendYieldPercent and dividendYieldPercentWithCurrencyEffect + // directly from the holding and remove averageInvestment and + // averageInvestmentWithCurrencyEffect from the properties above + const daysInMarket = differenceInDays( + new Date(), + parseDate(dateOfFirstActivity) + ); + + const dividendYieldPercent = + dividendYieldPercentOfSnapshot ?? + getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercentage: averageInvestment.eq(0) + ? new Big(0) + : dividendInBaseCurrency.div(averageInvestment) + }); + + const dividendYieldPercentWithCurrencyEffect = + dividendYieldPercentWithCurrencyEffectOfSnapshot ?? + getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercentage: averageInvestmentWithCurrencyEffect.eq(0) + ? new Big(0) + : dividendInBaseCurrency.div(averageInvestmentWithCurrencyEffect) + }); + const activitiesOfHolding = activities.filter((activity) => { return ( activity.assetProfile.dataSource === dataSource && @@ -1117,11 +1148,9 @@ export class PortfolioService { .getDataProvider(dataSource) .getDataProviderInfo(), dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), - // TODO: Remove the fallback with the next release, when the cached - // portfolio snapshots have been recalculated - dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0, + dividendYieldPercent: dividendYieldPercent.toNumber(), dividendYieldPercentWithCurrencyEffect: - dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0, + dividendYieldPercentWithCurrencyEffect.toNumber(), feeInBaseCurrency: feeInBaseCurrency.toNumber(), grossPerformance: grossPerformance?.toNumber(), grossPerformancePercent: grossPerformancePercentage?.toNumber(), @@ -2202,8 +2231,8 @@ export class PortfolioService { return ['BUY', 'SELL'].includes(type); }).length, dividendInBaseCurrency: dividendInBaseCurrency.toNumber(), - // TODO: Remove the fallback with the next release, when the cached - // portfolio snapshots have been recalculated + // TODO: Remove the fallback to 0 with the next release, when each + // cached portfolio snapshot contains the dividend yield dividendYieldPercent: dividendYieldPercent?.toNumber() ?? 0, dividendYieldPercentWithCurrencyEffect: dividendYieldPercentWithCurrencyEffect?.toNumber() ?? 0, diff --git a/apps/api/src/app/portfolio/types/net-performance-percentages.type.ts b/apps/api/src/app/portfolio/types/net-performance-percentages.type.ts new file mode 100644 index 0000000000..288f44631d --- /dev/null +++ b/apps/api/src/app/portfolio/types/net-performance-percentages.type.ts @@ -0,0 +1,9 @@ +import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; + +export type NetPerformancePercentages = Required< + Pick< + HistoricalDataItem, + | 'netPerformanceInPercentage' + | 'netPerformanceInPercentageWithCurrencyEffect' + > +>;