From def15e544a7bf73d9f46d8f13e2d25d311d0c605 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:53:12 +0200 Subject: [PATCH] Bugfix/liabilities without market data (#7345) * Fix liabilities without market data * Update changelog --- CHANGELOG.md | 7 ++ .../roai/portfolio-calculator-fee.spec.ts | 2 +- .../calculator/roai/portfolio-calculator.ts | 76 +++++++++++-------- 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76727fefd..894e914a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ 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). +## Unreleased + +### 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 + ## 3.27.0 - 2026-07-15 ### Changed 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