diff --git a/CHANGELOG.md b/CHANGELOG.md index b6563cdc1..9c9658e29 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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 the allocation in the accounts tab of the holding detail dialog caused by floating-point rounding + ## 3.47.0 - 2026-08-10 ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.service.spec.ts b/apps/api/src/app/portfolio/portfolio.service.spec.ts index eed3a27cb..c5e0a8332 100644 --- a/apps/api/src/app/portfolio/portfolio.service.spec.ts +++ b/apps/api/src/app/portfolio/portfolio.service.spec.ts @@ -510,5 +510,43 @@ describe('PortfolioService', () => { expect(accounts[UNKNOWN_KEY]).toBeUndefined(); expect(platforms[UNKNOWN_KEY]).toBeUndefined(); }); + + it('should not accumulate rounding errors of activities cancelling each other out', async () => { + const { accounts, platforms } = await getValueOfAccountsAndPlatforms({ + activities: [ + { + account, + accountId: account.id, + assetProfile: { symbol: 'AAPL' }, + quantity: 0.1, + type: 'BUY' + }, + { + account, + accountId: account.id, + assetProfile: { symbol: 'AAPL' }, + quantity: 0.2, + type: 'BUY' + }, + { + account, + accountId: account.id, + assetProfile: { symbol: 'AAPL' }, + quantity: 0.3, + type: 'SELL' + } + ], + filters: [], + portfolioItemsNow: { + AAPL: { marketPriceInBaseCurrency: 1234.5678 } + }, + userCurrency: 'USD', + userId: userDummyData.id + }); + + // 100 (balance) + 0 (activities) + expect(accounts[account.id].valueInBaseCurrency).toBe(100); + expect(platforms[account.platformId].valueInBaseCurrency).toBe(100); + }); }); }); diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 704de93f2..409046b1e 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -2262,38 +2262,54 @@ export class PortfolioService { } } - for (const { account, assetProfile, quantity, type } of ordersByAccount) { - const currentValueOfSymbolInBaseCurrency = - getFactor(type) * - quantity * - (portfolioItemsNow[assetProfile.symbol]?.marketPriceInBaseCurrency ?? - 0); - - if (accounts[account?.id || UNKNOWN_KEY]?.valueInBaseCurrency) { - accounts[account?.id || UNKNOWN_KEY].valueInBaseCurrency += - currentValueOfSymbolInBaseCurrency; - } else { - accounts[account?.id || UNKNOWN_KEY] = { - balance: 0, - currency: account?.currency, - name: account?.name, - valueInBaseCurrency: currentValueOfSymbolInBaseCurrency - }; - } + if (ordersByAccount.length === 0) { + continue; + } - if ( - platforms[account?.platformId || UNKNOWN_KEY]?.valueInBaseCurrency - ) { - platforms[account?.platformId || UNKNOWN_KEY].valueInBaseCurrency += - currentValueOfSymbolInBaseCurrency; - } else { - platforms[account?.platformId || UNKNOWN_KEY] = { - balance: 0, - currency: account?.currency, - name: account?.platform?.name, - valueInBaseCurrency: currentValueOfSymbolInBaseCurrency - }; - } + let valueOfAccountInBaseCurrency = new Big(0); + + for (const { assetProfile, quantity, type } of ordersByAccount) { + valueOfAccountInBaseCurrency = valueOfAccountInBaseCurrency.plus( + new Big(quantity) + .mul(getFactor(type)) + .mul( + portfolioItemsNow[assetProfile.symbol] + ?.marketPriceInBaseCurrency ?? 0 + ) + ); + } + + const currentAccountId = account?.id || UNKNOWN_KEY; + const currentPlatformId = account?.platformId || UNKNOWN_KEY; + + if (accounts[currentAccountId]) { + accounts[currentAccountId].valueInBaseCurrency = new Big( + accounts[currentAccountId].valueInBaseCurrency + ) + .plus(valueOfAccountInBaseCurrency) + .toNumber(); + } else { + accounts[currentAccountId] = { + balance: 0, + currency: account?.currency, + name: account?.name, + valueInBaseCurrency: valueOfAccountInBaseCurrency.toNumber() + }; + } + + if (platforms[currentPlatformId]) { + platforms[currentPlatformId].valueInBaseCurrency = new Big( + platforms[currentPlatformId].valueInBaseCurrency + ) + .plus(valueOfAccountInBaseCurrency) + .toNumber(); + } else { + platforms[currentPlatformId] = { + balance: 0, + currency: account?.currency, + name: account?.platform?.name, + valueInBaseCurrency: valueOfAccountInBaseCurrency.toNumber() + }; } }