From 00d47e539af33139b11d095d2eb3eab6f03694a3 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 8 Jun 2026 20:35:24 +0200 Subject: [PATCH] Improve unknown bucket grouping --- .../src/app/portfolio/portfolio.service.ts | 98 ++++---- .../allocations/allocations-page.component.ts | 214 ++++++++++-------- 2 files changed, 166 insertions(+), 146 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 4feb0f77a..24d760888 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1453,31 +1453,29 @@ export class PortfolioService { for (const [, position] of Object.entries(holdings)) { const value = position.valueInBaseCurrency; - if (position.assetProfile.assetClass !== AssetClass.LIQUIDITY) { - if (position.assetProfile.countries.length > 0) { - markets.developedMarkets.valueInBaseCurrency += - position.markets.developedMarkets * value; - markets.emergingMarkets.valueInBaseCurrency += - position.markets.emergingMarkets * value; - markets.otherMarkets.valueInBaseCurrency += - position.markets.otherMarkets * value; - - marketsAdvanced.asiaPacific.valueInBaseCurrency += - position.marketsAdvanced.asiaPacific * value; - marketsAdvanced.emergingMarkets.valueInBaseCurrency += - position.marketsAdvanced.emergingMarkets * value; - marketsAdvanced.europe.valueInBaseCurrency += - position.marketsAdvanced.europe * value; - marketsAdvanced.japan.valueInBaseCurrency += - position.marketsAdvanced.japan * value; - marketsAdvanced.northAmerica.valueInBaseCurrency += - position.marketsAdvanced.northAmerica * value; - marketsAdvanced.otherMarkets.valueInBaseCurrency += - position.marketsAdvanced.otherMarkets * value; - } else { - markets[UNKNOWN_KEY].valueInBaseCurrency += value; - marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency += value; - } + if (position.assetProfile.countries.length > 0) { + markets.developedMarkets.valueInBaseCurrency += + position.markets.developedMarkets * value; + markets.emergingMarkets.valueInBaseCurrency += + position.markets.emergingMarkets * value; + markets.otherMarkets.valueInBaseCurrency += + position.markets.otherMarkets * value; + + marketsAdvanced.asiaPacific.valueInBaseCurrency += + position.marketsAdvanced.asiaPacific * value; + marketsAdvanced.emergingMarkets.valueInBaseCurrency += + position.marketsAdvanced.emergingMarkets * value; + marketsAdvanced.europe.valueInBaseCurrency += + position.marketsAdvanced.europe * value; + marketsAdvanced.japan.valueInBaseCurrency += + position.marketsAdvanced.japan * value; + marketsAdvanced.northAmerica.valueInBaseCurrency += + position.marketsAdvanced.northAmerica * value; + marketsAdvanced.otherMarkets.valueInBaseCurrency += + position.marketsAdvanced.otherMarkets * value; + } else { + markets[UNKNOWN_KEY].valueInBaseCurrency += value; + marketsAdvanced[UNKNOWN_KEY].valueInBaseCurrency += value; } } @@ -2163,40 +2161,44 @@ export class PortfolioService { return withExcludedAccounts || account.isExcluded === false; }); - for (const account of currentAccounts) { + // Iterate over the accounts plus a null entry to group activities without + // an account into the unknown bucket + for (const account of [...currentAccounts, null]) { const ordersByAccount = activities.filter(({ accountId }) => { - return accountId === account.id; + return account ? accountId === account.id : !accountId; }); - accounts[account.id] = { - balance: account.balance, - currency: account.currency, - name: account.name, - valueInBaseCurrency: this.exchangeRateDataService.toCurrency( - account.balance, - account.currency, - userCurrency - ) - }; - - if (platforms[account.platformId || UNKNOWN_KEY]?.valueInBaseCurrency) { - platforms[account.platformId || UNKNOWN_KEY].valueInBaseCurrency += - this.exchangeRateDataService.toCurrency( - account.balance, - account.currency, - userCurrency - ); - } else { - platforms[account.platformId || UNKNOWN_KEY] = { + if (account) { + accounts[account.id] = { balance: account.balance, currency: account.currency, - name: account.platform?.name, + name: account.name, valueInBaseCurrency: this.exchangeRateDataService.toCurrency( account.balance, account.currency, userCurrency ) }; + + if (platforms[account.platformId || UNKNOWN_KEY]?.valueInBaseCurrency) { + platforms[account.platformId || UNKNOWN_KEY].valueInBaseCurrency += + this.exchangeRateDataService.toCurrency( + account.balance, + account.currency, + userCurrency + ); + } else { + platforms[account.platformId || UNKNOWN_KEY] = { + balance: account.balance, + currency: account.currency, + name: account.platform?.name, + valueInBaseCurrency: this.exchangeRateDataService.toCurrency( + account.balance, + account.currency, + userCurrency + ) + }; + } } for (const { diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index d0eb3788b..d53977ae8 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -201,6 +201,26 @@ export class GfAllocationsPageComponent implements OnInit { } } + private extractCurrency({ + assetClass, + assetSubClass, + currency + }: { + assetClass: PortfolioPosition['assetProfile']['assetClass']; + assetSubClass: PortfolioPosition['assetProfile']['assetSubClass']; + currency?: PortfolioPosition['assetProfile']['currency']; + }) { + if ( + assetClass === AssetClass.COMMODITY || + assetSubClass === AssetSubClass.CRYPTOCURRENCY + ) { + // Commodities and cryptocurrencies have no meaningful currency exposure + return UNKNOWN_KEY; + } + + return currency; + } + private extractEtfProvider({ assetSubClass, name @@ -339,7 +359,7 @@ export class GfAllocationsPageComponent implements OnInit { position.assetProfile.assetSubClass || (UNKNOWN_KEY as AssetSubClass), assetSubClassLabel: position.assetProfile.assetSubClassLabel || UNKNOWN_KEY, - currency: position.assetProfile.currency, + currency: this.extractCurrency(position.assetProfile), etfProvider: this.extractEtfProvider({ assetSubClass: position.assetProfile.assetSubClass, name: position.assetProfile.name @@ -348,119 +368,117 @@ export class GfAllocationsPageComponent implements OnInit { name: position.assetProfile.name }; - if (position.assetProfile.assetClass !== AssetClass.LIQUIDITY) { - // Prepare analysis data by continents, countries, holdings and sectors except for liquidity - - if (position.assetProfile.countries.length > 0) { - for (const country of position.assetProfile.countries) { - const { code, continent, weight } = country; - - if (this.continents[continent]?.value) { - this.continents[continent].value += + // Prepare analysis data by continents, countries, holdings and sectors + + if (position.assetProfile.countries.length > 0) { + for (const country of position.assetProfile.countries) { + const { code, continent, weight } = country; + + if (this.continents[continent]?.value) { + this.continents[continent].value += + weight * + (isNumber(position.valueInBaseCurrency) + ? position.valueInBaseCurrency + : position.valueInPercentage); + } else { + this.continents[continent] = { + name: translate(continent), + value: weight * (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - } else { - this.continents[continent] = { - name: translate(continent), - value: - weight * - (isNumber(position.valueInBaseCurrency) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage) - }; - } - - if (this.countries[code]?.value) { - this.countries[code].value += + ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency + : this.portfolioDetails.holdings[symbol].valueInPercentage) + }; + } + + if (this.countries[code]?.value) { + this.countries[code].value += + weight * + (isNumber(position.valueInBaseCurrency) + ? position.valueInBaseCurrency + : position.valueInPercentage); + } else { + this.countries[code] = { + name: getCountryName({ + code, + locale: this.user?.settings?.locale + }), + value: weight * (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - } else { - this.countries[code] = { - name: getCountryName({ - code, - locale: this.user?.settings?.locale - }), - value: - weight * - (isNumber(position.valueInBaseCurrency) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage) - }; - } + ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency + : this.portfolioDetails.holdings[symbol].valueInPercentage) + }; } - } else { - this.continents[UNKNOWN_KEY].value += isNumber( - position.valueInBaseCurrency - ) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage; - - this.countries[UNKNOWN_KEY].value += isNumber( - position.valueInBaseCurrency - ) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage; } + } else { + this.continents[UNKNOWN_KEY].value += isNumber( + position.valueInBaseCurrency + ) + ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency + : this.portfolioDetails.holdings[symbol].valueInPercentage; + + this.countries[UNKNOWN_KEY].value += isNumber( + position.valueInBaseCurrency + ) + ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency + : this.portfolioDetails.holdings[symbol].valueInPercentage; + } - if (position.assetProfile.holdings.length > 0) { - for (const { - allocationInPercentage, - name, - valueInBaseCurrency - } of position.assetProfile.holdings) { - const normalizedAssetName = this.normalizeAssetName(name); - - if (this.topHoldingsMap[normalizedAssetName]?.value) { - this.topHoldingsMap[normalizedAssetName].value += isNumber( - valueInBaseCurrency - ) + if (position.assetProfile.holdings.length > 0) { + for (const { + allocationInPercentage, + name, + valueInBaseCurrency + } of position.assetProfile.holdings) { + const normalizedAssetName = this.normalizeAssetName(name); + + if (this.topHoldingsMap[normalizedAssetName]?.value) { + this.topHoldingsMap[normalizedAssetName].value += isNumber( + valueInBaseCurrency + ) + ? valueInBaseCurrency + : allocationInPercentage * + this.portfolioDetails.holdings[symbol].valueInPercentage; + } else { + this.topHoldingsMap[normalizedAssetName] = { + name, + value: isNumber(valueInBaseCurrency) ? valueInBaseCurrency : allocationInPercentage * - this.portfolioDetails.holdings[symbol].valueInPercentage; - } else { - this.topHoldingsMap[normalizedAssetName] = { - name, - value: isNumber(valueInBaseCurrency) - ? valueInBaseCurrency - : allocationInPercentage * - this.portfolioDetails.holdings[symbol].valueInPercentage - }; - } + this.portfolioDetails.holdings[symbol].valueInPercentage + }; } } + } - if (position.assetProfile.sectors.length > 0) { - for (const sector of position.assetProfile.sectors) { - const { name, weight } = sector; - - if (this.sectors[name]?.value) { - this.sectors[name].value += + if (position.assetProfile.sectors.length > 0) { + for (const sector of position.assetProfile.sectors) { + const { name, weight } = sector; + + if (this.sectors[name]?.value) { + this.sectors[name].value += + weight * + (isNumber(position.valueInBaseCurrency) + ? position.valueInBaseCurrency + : position.valueInPercentage); + } else { + this.sectors[name] = { + name: translate(name), + value: weight * (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage); - } else { - this.sectors[name] = { - name: translate(name), - value: - weight * - (isNumber(position.valueInBaseCurrency) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage) - }; - } + ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency + : this.portfolioDetails.holdings[symbol].valueInPercentage) + }; } - } else { - this.sectors[UNKNOWN_KEY].value += isNumber( - position.valueInBaseCurrency - ) - ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency - : this.portfolioDetails.holdings[symbol].valueInPercentage; } + } else { + this.sectors[UNKNOWN_KEY].value += isNumber( + position.valueInBaseCurrency + ) + ? this.portfolioDetails.holdings[symbol].valueInBaseCurrency + : this.portfolioDetails.holdings[symbol].valueInPercentage; } if (this.holdings[symbol].assetSubClass === 'ETF') {