diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a921744f..59ec71ff2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fixed an issue where the dividends, the interest and the liabilities of asset profiles without market data have been valued at zero in the portfolio calculation +- Fixed an issue where an error has been reported for asset profiles without market data which do not hold any units - Fixed an issue with removing a linked account from a buy, sell or dividend activity ## 3.27.0 - 2026-07-15 diff --git a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts index e5798818f..4e413c0c5 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator-fee.spec.ts @@ -113,7 +113,7 @@ describe('PortfolioCalculator', () => { expect(portfolioSnapshot).toMatchObject({ currentValueInBaseCurrency: new Big('0'), errors: [], - hasErrors: true, + hasErrors: false, positions: [], totalFeesWithCurrencyEffect: new Big('49'), totalInterestWithCurrencyEffect: new Big('0'), 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 f79e5bc79..747fca7a5 100644 --- a/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.ts @@ -236,6 +236,36 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { }; } + // The dividends, the interest and the liabilities are derived from the + // activities only. Accumulate them upfront so that they survive the bail + // out for symbols without a market price below. + for (const order of orders) { + const exchangeRateAtOrderDate = exchangeRates[order.date]; + + if (order.type === 'DIVIDEND') { + const dividend = order.quantity.mul(order.unitPrice); + + totalDividend = totalDividend.plus(dividend); + totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( + dividend.mul(exchangeRateAtOrderDate ?? 1) + ); + } else if (order.type === 'INTEREST') { + const interest = order.quantity.mul(order.unitPrice); + + totalInterest = totalInterest.plus(interest); + totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( + interest.mul(exchangeRateAtOrderDate ?? 1) + ); + } else if (order.type === 'LIABILITY') { + const liabilities = order.quantity.mul(order.unitPrice); + + totalLiabilities = totalLiabilities.plus(liabilities); + totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( + liabilities.mul(exchangeRateAtOrderDate ?? 1) + ); + } + } + const dateOfFirstTransaction = new Date(orders[0].date); const endDateString = format(end, DATE_FORMAT); @@ -263,7 +293,20 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { !unitPriceAtEndDate || (!unitPriceAtStartDate && isBefore(dateOfFirstTransaction, start)) ) { + // A missing market price can only affect the units which are held. The + // dividends, the interest and the liabilities do not hold any units and + // are therefore not in error. + const hasActivitiesWithUnits = orders.some(({ type }) => { + return ['BUY', 'SELL'].includes(type); + }); + return { + totalDividend, + totalDividendInBaseCurrency, + totalInterest, + totalInterestInBaseCurrency, + totalLiabilities, + totalLiabilitiesInBaseCurrency, currentValues: {}, currentValuesWithCurrencyEffect: {}, feesWithCurrencyEffect: new Big(0), @@ -271,7 +314,7 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { grossPerformancePercentage: new Big(0), grossPerformancePercentageWithCurrencyEffect: new Big(0), grossPerformanceWithCurrencyEffect: new Big(0), - hasErrors: true, + hasErrors: hasActivitiesWithUnits, initialValue: new Big(0), initialValueWithCurrencyEffect: new Big(0), investmentValuesAccumulated: {}, @@ -288,14 +331,8 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { timeWeightedInvestmentValuesWithCurrencyEffect: {}, timeWeightedInvestmentWithCurrencyEffect: new Big(0), totalAccountBalanceInBaseCurrency: new Big(0), - totalDividend: new Big(0), - totalDividendInBaseCurrency: new Big(0), - totalInterest: new Big(0), - totalInterestInBaseCurrency: new Big(0), totalInvestment: new Big(0), - totalInvestmentWithCurrencyEffect: new Big(0), - totalLiabilities: new Big(0), - totalLiabilitiesInBaseCurrency: new Big(0) + totalInvestmentWithCurrencyEffect: new Big(0) }; } @@ -415,29 +452,6 @@ export class RoaiPortfolioCalculator extends PortfolioCalculator { const exchangeRateAtOrderDate = exchangeRates[order.date]; - if (order.type === 'DIVIDEND') { - const dividend = order.quantity.mul(order.unitPrice); - - totalDividend = totalDividend.plus(dividend); - totalDividendInBaseCurrency = totalDividendInBaseCurrency.plus( - dividend.mul(exchangeRateAtOrderDate ?? 1) - ); - } else if (order.type === 'INTEREST') { - const interest = order.quantity.mul(order.unitPrice); - - totalInterest = totalInterest.plus(interest); - totalInterestInBaseCurrency = totalInterestInBaseCurrency.plus( - interest.mul(exchangeRateAtOrderDate ?? 1) - ); - } else if (order.type === 'LIABILITY') { - const liabilities = order.quantity.mul(order.unitPrice); - - totalLiabilities = totalLiabilities.plus(liabilities); - totalLiabilitiesInBaseCurrency = totalLiabilitiesInBaseCurrency.plus( - liabilities.mul(exchangeRateAtOrderDate ?? 1) - ); - } - if (order.itemType === 'start') { // Take the unit price of the order as the market price if there are no // orders of this symbol before the start date