From 4cf74b5e604985220635423bb94becbb2718f595 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 Mar 2026 07:21:18 +0100 Subject: [PATCH 1/3] Use asset profile data --- apps/api/src/app/endpoints/ai/ai.service.ts | 85 +++++++++---------- .../app/endpoints/public/public.controller.ts | 12 +-- .../src/app/portfolio/portfolio.controller.ts | 37 ++++---- .../src/app/portfolio/portfolio.service.ts | 35 +------- apps/api/src/models/rule.ts | 10 ++- .../rules/asset-class-cluster-risk/equity.ts | 3 +- .../asset-class-cluster-risk/fixed-income.ts | 3 +- .../base-currency-current-investment.ts | 2 +- .../current-investment.ts | 2 +- ...ate-or-update-activity-dialog.component.ts | 41 ++++----- .../import-activities-dialog.component.ts | 5 +- .../import-activities-dialog.html | 12 +-- .../allocations/allocations-page.component.ts | 65 +++++++------- .../portfolio/analysis/analysis-page.html | 16 ++-- .../app/pages/public/public-page.component.ts | 26 ++++-- libs/common/src/lib/helper.ts | 14 --- .../portfolio-position.interface.ts | 45 +--------- .../public-portfolio-response.interface.ts | 22 ----- .../src/lib/assistant/assistant.component.ts | 42 ++++----- .../holdings-table.component.html | 18 ++-- .../holdings-table.component.ts | 4 +- .../portfolio-filter-form.component.html | 12 ++- .../portfolio-filter-form.component.ts | 5 +- .../top-holdings/top-holdings.component.html | 2 +- .../top-holdings/top-holdings.component.ts | 4 +- .../treemap-chart/treemap-chart.component.ts | 14 +-- 26 files changed, 218 insertions(+), 318 deletions(-) diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts index d07768d69..57d5bdd90 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.ts @@ -89,53 +89,44 @@ export class AiService { .sort((a, b) => { return b.allocationInPercentage - a.allocationInPercentage; }) - .map( - ({ - allocationInPercentage, - assetClass, - assetSubClass, - currency, - name: label, - symbol - }) => { - return AiService.HOLDINGS_TABLE_COLUMN_DEFINITIONS.reduce( - (row, { key, name }) => { - switch (key) { - case 'ALLOCATION_PERCENTAGE': - row[name] = `${(allocationInPercentage * 100).toFixed(3)}%`; - break; - - case 'ASSET_CLASS': - row[name] = assetClass ?? ''; - break; - - case 'ASSET_SUB_CLASS': - row[name] = assetSubClass ?? ''; - break; - - case 'CURRENCY': - row[name] = currency; - break; - - case 'NAME': - row[name] = label; - break; - - case 'SYMBOL': - row[name] = symbol; - break; - - default: - row[name] = ''; - break; - } - - return row; - }, - {} as Record - ); - } - ); + .map(({ allocationInPercentage, assetProfile }) => { + return AiService.HOLDINGS_TABLE_COLUMN_DEFINITIONS.reduce( + (row, { key, name }) => { + switch (key) { + case 'ALLOCATION_PERCENTAGE': + row[name] = `${(allocationInPercentage * 100).toFixed(3)}%`; + break; + + case 'ASSET_CLASS': + row[name] = assetProfile?.assetClass ?? ''; + break; + + case 'ASSET_SUB_CLASS': + row[name] = assetProfile?.assetSubClass ?? ''; + break; + + case 'CURRENCY': + row[name] = assetProfile?.currency ?? ''; + break; + + case 'NAME': + row[name] = assetProfile?.name ?? ''; + break; + + case 'SYMBOL': + row[name] = assetProfile?.symbol ?? ''; + break; + + default: + row[name] = ''; + break; + } + + return row; + }, + {} as Record + ); + }); // Dynamic import to load ESM module from CommonJS context // eslint-disable-next-line @typescript-eslint/no-implied-eval diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index b97640cab..f2dc0ef05 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -150,11 +150,11 @@ export class PublicController { }; const totalValue = getSum( - Object.values(holdings).map(({ currency, marketPrice, quantity }) => { + Object.values(holdings).map(({ assetProfile, marketPrice, quantity }) => { return new Big( this.exchangeRateDataService.toCurrency( quantity * marketPrice, - currency, + assetProfile.currency, this.request.user?.settings?.settings.baseCurrency ?? DEFAULT_CURRENCY ) @@ -166,19 +166,11 @@ export class PublicController { publicPortfolioResponse.holdings[symbol] = { allocationInPercentage: portfolioPosition.valueInBaseCurrency / totalValue, - assetClass: hasDetails ? portfolioPosition.assetClass : undefined, assetProfile: hasDetails ? portfolioPosition.assetProfile : undefined, - countries: hasDetails ? portfolioPosition.countries : [], - currency: hasDetails ? portfolioPosition.currency : undefined, - dataSource: portfolioPosition.dataSource, dateOfFirstActivity: portfolioPosition.dateOfFirstActivity, markets: hasDetails ? portfolioPosition.markets : undefined, - name: portfolioPosition.name, netPerformancePercentWithCurrencyEffect: portfolioPosition.netPerformancePercentWithCurrencyEffect, - sectors: hasDetails ? portfolioPosition.sectors : [], - symbol: portfolioPosition.symbol, - url: portfolioPosition.url, valueInPercentage: portfolioPosition.valueInBaseCurrency / totalValue }; } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 73d4320d6..5ca30dd96 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -141,10 +141,10 @@ export class PortfolioController { .reduce((a, b) => a + b, 0); const totalValue = Object.values(holdings) - .filter(({ assetClass, assetSubClass }) => { + .filter(({ assetProfile }) => { return ( - assetClass !== AssetClass.LIQUIDITY && - assetSubClass !== AssetSubClass.CASH + assetProfile.assetClass !== AssetClass.LIQUIDITY && + assetProfile.assetSubClass !== AssetSubClass.CASH ); }) .map(({ valueInBaseCurrency }) => { @@ -212,24 +212,29 @@ export class PortfolioController { } for (const [symbol, portfolioPosition] of Object.entries(holdings)) { + const assetProfile = portfolioPosition.assetProfile; + holdings[symbol] = { ...portfolioPosition, - assetClass: - hasDetails || portfolioPosition.assetClass === AssetClass.LIQUIDITY - ? portfolioPosition.assetClass - : undefined, - assetSubClass: - hasDetails || portfolioPosition.assetSubClass === AssetSubClass.CASH - ? portfolioPosition.assetSubClass - : undefined, - countries: hasDetails ? portfolioPosition.countries : [], - currency: hasDetails ? portfolioPosition.currency : undefined, - holdings: hasDetails ? portfolioPosition.holdings : [], + assetProfile: { + ...assetProfile, + assetClass: + hasDetails || assetProfile.assetClass === AssetClass.LIQUIDITY + ? assetProfile.assetClass + : undefined, + assetSubClass: + hasDetails || assetProfile.assetSubClass === AssetSubClass.CASH + ? assetProfile.assetSubClass + : undefined, + countries: hasDetails ? assetProfile.countries : [], + currency: hasDetails ? assetProfile.currency : undefined, + holdings: hasDetails ? assetProfile.holdings : [], + sectors: hasDetails ? assetProfile.sectors : [] + }, markets: hasDetails ? portfolioPosition.markets : undefined, marketsAdvanced: hasDetails ? portfolioPosition.marketsAdvanced - : undefined, - sectors: hasDetails ? portfolioPosition.sectors : [] + : undefined }; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 5dab27939..77b5202cc 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -613,17 +613,15 @@ export class PortfolioService { holdings[symbol] = { activitiesCount, - currency, markets, marketsAdvanced, marketPrice, - symbol, tags, allocationInPercentage: filteredValueInBaseCurrency.eq(0) ? 0 : valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(), - assetClass: assetProfile.assetClass, assetProfile: { + currency, assetClass: assetProfile.assetClass, assetSubClass: assetProfile.assetSubClass, countries: assetProfile.countries, @@ -645,9 +643,6 @@ export class PortfolioService { symbol: assetProfile.symbol, url: assetProfile.url }, - assetSubClass: assetProfile.assetSubClass, - countries: assetProfile.countries, - dataSource: assetProfile.dataSource, dateOfFirstActivity: parseDate(dateOfFirstActivity), dividend: dividend?.toNumber() ?? 0, grossPerformance: grossPerformance?.toNumber() ?? 0, @@ -656,19 +651,7 @@ export class PortfolioService { grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, grossPerformanceWithCurrencyEffect: grossPerformanceWithCurrencyEffect?.toNumber() ?? 0, - holdings: assetProfile.holdings.map( - ({ allocationInPercentage, name }) => { - return { - allocationInPercentage, - name, - valueInBaseCurrency: valueInBaseCurrency - .mul(allocationInPercentage) - .toNumber() - }; - } - ), investment: investment.toNumber(), - name: assetProfile.name, netPerformance: netPerformance?.toNumber() ?? 0, netPerformancePercent: netPerformancePercentage?.toNumber() ?? 0, netPerformancePercentWithCurrencyEffect: @@ -678,8 +661,6 @@ export class PortfolioService { netPerformanceWithCurrencyEffect: netPerformanceWithCurrencyEffectMap?.[dateRange]?.toNumber() ?? 0, quantity: quantity.toNumber(), - sectors: assetProfile.sectors, - url: assetProfile.url, valueInBaseCurrency: valueInBaseCurrency.toNumber() }; } @@ -1447,8 +1428,8 @@ export class PortfolioService { for (const [, position] of Object.entries(holdings)) { const value = position.valueInBaseCurrency; - if (position.assetClass !== AssetClass.LIQUIDITY) { - if (position.countries.length > 0) { + if (position.assetProfile.assetClass !== AssetClass.LIQUIDITY) { + if (position.assetProfile.countries.length > 0) { markets.developedMarkets.valueInBaseCurrency += position.markets.developedMarkets * value; markets.emergingMarkets.valueInBaseCurrency += @@ -1694,15 +1675,13 @@ export class PortfolioService { currency: string; }): PortfolioPosition { return { - currency, activitiesCount: 0, allocationInPercentage: 0, - assetClass: AssetClass.LIQUIDITY, - assetSubClass: AssetSubClass.CASH, assetProfile: { currency, assetClass: AssetClass.LIQUIDITY, assetSubClass: AssetSubClass.CASH, + currency, countries: [], dataSource: undefined, holdings: [], @@ -1710,25 +1689,19 @@ export class PortfolioService { sectors: [], symbol: currency }, - countries: [], - dataSource: undefined, dateOfFirstActivity: undefined, dividend: 0, grossPerformance: 0, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: 0, grossPerformanceWithCurrencyEffect: 0, - holdings: [], investment: balance, marketPrice: 0, - name: currency, netPerformance: 0, netPerformancePercent: 0, netPerformancePercentWithCurrencyEffect: 0, netPerformanceWithCurrencyEffect: 0, quantity: 0, - sectors: [], - symbol: currency, tags: [], valueInBaseCurrency: balance }; diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index 622375b5b..930702342 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,6 +1,5 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; -import { groupBy } from '@ghostfolio/common/helper'; import { PortfolioPosition, PortfolioReportRule, @@ -9,6 +8,7 @@ import { } from '@ghostfolio/common/interfaces'; import { Big } from 'big.js'; +import { groupBy } from 'lodash'; import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; @@ -41,10 +41,12 @@ export abstract class Rule implements RuleInterface { public groupCurrentHoldingsByAttribute( holdings: PortfolioPosition[], - attribute: keyof PortfolioPosition, + attribute: + | keyof PortfolioPosition + | `assetProfile.${Extract}`, baseCurrency: string ) { - return Array.from(groupBy(attribute, holdings).entries()).map( + return Object.entries(groupBy(holdings, attribute)).map( ([attributeValue, objs]) => ({ groupKey: attributeValue, investment: objs.reduce( @@ -59,7 +61,7 @@ export abstract class Rule implements RuleInterface { new Big(currentValue.quantity) .mul(currentValue.marketPrice ?? 0) .toNumber(), - currentValue.currency, + currentValue.assetProfile.currency, baseCurrency ), 0 diff --git a/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts b/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts index f70756e91..cd4e0d3a9 100644 --- a/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts +++ b/apps/api/src/models/rules/asset-class-cluster-risk/equity.ts @@ -27,9 +27,10 @@ export class AssetClassClusterRiskEquity extends Rule { public evaluate(ruleSettings: Settings) { const holdingsGroupedByAssetClass = this.groupCurrentHoldingsByAttribute( this.holdings, - 'assetClass', + 'assetProfile.assetClass', ruleSettings.baseCurrency ); + let totalValue = 0; const equityValueInBaseCurrency = diff --git a/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts b/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts index 3bd835e4d..03f7e8f99 100644 --- a/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts +++ b/apps/api/src/models/rules/asset-class-cluster-risk/fixed-income.ts @@ -27,9 +27,10 @@ export class AssetClassClusterRiskFixedIncome extends Rule { public evaluate(ruleSettings: Settings) { const holdingsGroupedByAssetClass = this.groupCurrentHoldingsByAttribute( this.holdings, - 'assetClass', + 'assetProfile.assetClass', ruleSettings.baseCurrency ); + let totalValue = 0; const fixedIncomeValueInBaseCurrency = diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index d3176582f..733999f6b 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -27,7 +27,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { public evaluate(ruleSettings: Settings) { const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute( this.holdings, - 'currency', + 'assetProfile.currency', ruleSettings.baseCurrency ); diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index c7cd63191..282dcfaee 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -136,34 +136,25 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ holdings }) => { this.defaultLookupItems = holdings - .filter(({ assetSubClass }) => { - return !['CASH'].includes(assetSubClass); + .filter(({ assetProfile }) => { + return !['CASH'].includes(assetProfile.assetSubClass); }) .sort((a, b) => { - return a.name?.localeCompare(b.name); + return a.assetProfile.name?.localeCompare(b.assetProfile.name); }) - .map( - ({ - assetClass, - assetSubClass, - currency, - dataSource, - name, - symbol - }) => { - return { - assetClass, - assetSubClass, - currency, - dataSource, - name, - symbol, - dataProviderInfo: { - isPremium: false - } - }; - } - ); + .map(({ assetProfile }) => { + return { + assetClass: assetProfile.assetClass, + assetSubClass: assetProfile.assetSubClass, + currency: assetProfile.currency, + dataProviderInfo: { + isPremium: false + }, + dataSource: assetProfile.dataSource, + name: assetProfile.name, + symbol: assetProfile.symbol + }; + }); this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 1a84e9f31..e32f3b27a 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -154,9 +154,10 @@ export class GfImportActivitiesDialogComponent implements OnDestroy { }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ holdings }) => { - this.holdings = sortBy(holdings, ({ name }) => { - return name.toLowerCase(); + this.holdings = sortBy(holdings, ({ assetProfile }) => { + return assetProfile.name.toLowerCase(); }); + this.assetProfileForm.get('assetProfileIdentifier').enable(); this.isLoading = false; diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html index 508fdd753..85fb73ba2 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.html @@ -38,18 +38,18 @@ {{ holding.name }}{{ holding.assetProfile.name }}
{{ holding.symbol | gfSymbol }} · - {{ holding.currency }}{{ holding.assetProfile.symbol | gfSymbol }} · + {{ holding.assetProfile.currency }}
} 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 5226c3c12..9ef11147e 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 @@ -74,16 +74,10 @@ export class GfAllocationsPageComponent implements OnInit { public deviceType: string; public hasImpersonationId: boolean; public holdings: { - [symbol: string]: Pick< - PortfolioPosition, - | 'assetClass' - | 'assetClassLabel' - | 'assetSubClass' - | 'assetSubClassLabel' - | 'currency' - | 'exchange' - | 'name' - > & { etfProvider: string; value: number }; + [symbol: string]: Pick & { + etfProvider: string; + value: number; + }; }; public isLoading = false; public markets: { @@ -209,7 +203,7 @@ export class GfAllocationsPageComponent implements OnInit { assetSubClass, name }: { - assetSubClass: PortfolioPosition['assetSubClass']; + assetSubClass: PortfolioPosition['assetProfile']['assetSubClass']; name: string; }) { if (assetSubClass === 'ETF') { @@ -326,6 +320,7 @@ export class GfAllocationsPageComponent implements OnInit { for (const [symbol, position] of Object.entries( this.portfolioDetails.holdings )) { + const { assetProfile } = position; let value = 0; if (this.hasImpersonationId) { @@ -336,24 +331,28 @@ export class GfAllocationsPageComponent implements OnInit { this.holdings[symbol] = { value, - assetClass: position.assetClass || (UNKNOWN_KEY as AssetClass), - assetClassLabel: position.assetClassLabel || UNKNOWN_KEY, - assetSubClass: position.assetSubClass || (UNKNOWN_KEY as AssetSubClass), - assetSubClassLabel: position.assetSubClassLabel || UNKNOWN_KEY, - currency: position.currency, + assetProfile: { + ...assetProfile, + assetClass: assetProfile.assetClass || (UNKNOWN_KEY as AssetClass), + assetClassLabel: assetProfile.assetClassLabel || UNKNOWN_KEY, + assetSubClass: + assetProfile.assetSubClass || (UNKNOWN_KEY as AssetSubClass), + assetSubClassLabel: assetProfile.assetSubClassLabel || UNKNOWN_KEY, + currency: assetProfile.currency, + name: assetProfile.name + }, etfProvider: this.extractEtfProvider({ - assetSubClass: position.assetSubClass, - name: position.name + assetSubClass: assetProfile.assetSubClass, + name: assetProfile.name }), - exchange: position.exchange, - name: position.name + exchange: position.exchange }; - if (position.assetClass !== AssetClass.LIQUIDITY) { + if (assetProfile.assetClass !== AssetClass.LIQUIDITY) { // Prepare analysis data by continents, countries, holdings and sectors except for liquidity - if (position.countries.length > 0) { - for (const country of position.countries) { + if (assetProfile.countries.length > 0) { + for (const country of assetProfile.countries) { const { code, continent, name, weight } = country; if (this.continents[continent]?.value) { @@ -404,8 +403,8 @@ export class GfAllocationsPageComponent implements OnInit { : this.portfolioDetails.holdings[symbol].valueInPercentage; } - if (position.holdings.length > 0) { - for (const holding of position.holdings) { + if (assetProfile.holdings.length > 0) { + for (const holding of assetProfile.holdings) { const { allocationInPercentage, name, valueInBaseCurrency } = holding; @@ -426,8 +425,8 @@ export class GfAllocationsPageComponent implements OnInit { } } - if (position.sectors.length > 0) { - for (const sector of position.sectors) { + if (assetProfile.sectors.length > 0) { + for (const sector of assetProfile.sectors) { const { name, weight } = sector; if (this.sectors[name]?.value) { @@ -456,13 +455,13 @@ export class GfAllocationsPageComponent implements OnInit { } } - if (this.holdings[symbol].assetSubClass === 'ETF') { + if (this.holdings[symbol].assetProfile.assetSubClass === 'ETF') { this.totalValueInEtf += this.holdings[symbol].value; } this.symbols[prettifySymbol(symbol)] = { - dataSource: position.dataSource, - name: position.name, + dataSource: assetProfile.dataSource, + name: assetProfile.name, symbol: prettifySymbol(symbol), value: isNumber(position.valueInBaseCurrency) ? position.valueInBaseCurrency @@ -515,8 +514,8 @@ export class GfAllocationsPageComponent implements OnInit { this.totalValueInEtf > 0 ? value / this.totalValueInEtf : 0, parents: Object.entries(this.portfolioDetails.holdings) .map(([symbol, holding]) => { - if (holding.holdings.length > 0) { - const currentParentHolding = holding.holdings.find( + if (holding.assetProfile.holdings.length > 0) { + const currentParentHolding = holding.assetProfile.holdings.find( (parentHolding) => { return parentHolding.name === name; } @@ -526,7 +525,7 @@ export class GfAllocationsPageComponent implements OnInit { ? { allocationInPercentage: currentParentHolding.valueInBaseCurrency / value, - name: holding.name, + name: holding.assetProfile.name, position: holding, symbol: prettifySymbol(symbol), valueInBaseCurrency: diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html index 517ad7101..adb577e37 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -312,13 +312,15 @@ -
{{ holding.name }}
+
+ {{ holding.assetProfile.name }} +
-
{{ holding.name }}
+
+ {{ holding.assetProfile.name }} +
& { + [symbol: string]: { + assetProfile: Pick< + PortfolioPosition['assetProfile'], + 'currency' | 'name' + >; value: number; }; }; @@ -166,19 +170,23 @@ export class GfPublicPageComponent implements OnInit { for (const [symbol, position] of Object.entries( this.publicPortfolioDetails.holdings )) { + const { assetProfile } = position; + this.holdings.push(position); this.positions[symbol] = { - currency: position.currency, - name: position.name, + assetProfile: { + currency: assetProfile.currency, + name: assetProfile.name + }, value: position.allocationInPercentage }; - if (position.assetClass !== AssetClass.LIQUIDITY) { + if (assetProfile.assetClass !== AssetClass.LIQUIDITY) { // Prepare analysis data by continents, countries, holdings and sectors except for liquidity - if (position.countries.length > 0) { - for (const country of position.countries) { + if (assetProfile.countries.length > 0) { + for (const country of assetProfile.countries) { const { code, continent, name, weight } = country; if (this.continents[continent]?.value) { @@ -215,8 +223,8 @@ export class GfPublicPageComponent implements OnInit { this.publicPortfolioDetails.holdings[symbol].valueInBaseCurrency; } - if (position.sectors.length > 0) { - for (const sector of position.sectors) { + if (assetProfile.sectors.length > 0) { + for (const sector of assetProfile.sectors) { const { name, weight } = sector; if (this.sectors[name]?.value) { @@ -238,7 +246,7 @@ export class GfPublicPageComponent implements OnInit { } this.symbols[prettifySymbol(symbol)] = { - name: position.name, + name: assetProfile.name, symbol: prettifySymbol(symbol), value: isNumber(position.valueInBaseCurrency) ? position.valueInBaseCurrency diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 4db1fcf2d..2d8cd90e4 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -342,20 +342,6 @@ export function getYesterday() { return subDays(new Date(Date.UTC(year, month, day)), 1); } -export function groupBy( - key: K, - arr: T[] -): Map { - const map = new Map(); - arr.forEach((t) => { - if (!map.has(t[key])) { - map.set(t[key], []); - } - map.get(t[key])!.push(t); - }); - return map; -} - export function interpolate(template: string, context: any) { return template?.replace(/[$]{([^}]+)}/g, (_, objectPath) => { const properties = objectPath.split('.'); diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index c4ef2e3dc..5ac346f5c 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -1,22 +1,13 @@ import { Market, MarketAdvanced } from '@ghostfolio/common/types'; -import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client'; +import { Tag } from '@prisma/client'; -import { Country } from './country.interface'; import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; -import { Holding } from './holding.interface'; -import { Sector } from './sector.interface'; export interface PortfolioPosition { activitiesCount: number; allocationInPercentage: number; - /** @deprecated */ - assetClass?: AssetClass; - - /** @deprecated */ - assetClassLabel?: string; - assetProfile: Pick< EnhancedSymbolProfile, | 'assetClass' @@ -34,21 +25,6 @@ export interface PortfolioPosition { assetSubClassLabel?: string; }; - /** @deprecated */ - assetSubClass?: AssetSubClass; - - /** @deprecated */ - assetSubClassLabel?: string; - - /** @deprecated */ - countries: Country[]; - - /** @deprecated */ - currency: string; - - /** @deprecated */ - dataSource: DataSource; - dateOfFirstActivity: Date; dividend: number; exchange?: string; @@ -56,38 +32,19 @@ export interface PortfolioPosition { grossPerformancePercent: number; grossPerformancePercentWithCurrencyEffect: number; grossPerformanceWithCurrencyEffect: number; - - /** @deprecated */ - holdings: Holding[]; - investment: number; marketChange?: number; marketChangePercent?: number; marketPrice: number; markets?: { [key in Market]: number }; marketsAdvanced?: { [key in MarketAdvanced]: number }; - - /** @deprecated */ - name: string; - netPerformance: number; netPerformancePercent: number; netPerformancePercentWithCurrencyEffect: number; netPerformanceWithCurrencyEffect: number; quantity: number; - - /** @deprecated */ - sectors: Sector[]; - - /** @deprecated */ - symbol: string; - tags?: Tag[]; type?: string; - - /** @deprecated */ - url?: string; - valueInBaseCurrency?: number; valueInPercentage?: number; } diff --git a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts index eae14cec6..18c7dc57a 100644 --- a/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts +++ b/libs/common/src/lib/interfaces/responses/public-portfolio-response.interface.ts @@ -14,32 +14,10 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { [symbol: string]: Pick< PortfolioPosition, | 'allocationInPercentage' - - /** @deprecated */ - | 'assetClass' | 'assetProfile' - - /** @deprecated */ - | 'countries' - | 'currency' - - /** @deprecated */ - | 'dataSource' | 'dateOfFirstActivity' | 'markets' - - /** @deprecated */ - | 'name' | 'netPerformancePercentWithCurrencyEffect' - - /** @deprecated */ - | 'sectors' - - /** @deprecated */ - | 'symbol' - - /** @deprecated */ - | 'url' | 'valueInBaseCurrency' | 'valueInPercentage' >; diff --git a/libs/ui/src/lib/assistant/assistant.component.ts b/libs/ui/src/lib/assistant/assistant.component.ts index 1c67e4fa2..226ac1b53 100644 --- a/libs/ui/src/lib/assistant/assistant.component.ts +++ b/libs/ui/src/lib/assistant/assistant.component.ts @@ -480,11 +480,14 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ holdings }) => { this.holdings = holdings - .filter(({ assetSubClass }) => { - return assetSubClass && !['CASH'].includes(assetSubClass); + .filter(({ assetProfile }) => { + return ( + assetProfile.assetSubClass && + !['CASH'].includes(assetProfile.assetSubClass) + ); }) .sort((a, b) => { - return a.name?.localeCompare(b.name); + return a.assetProfile.name?.localeCompare(b.assetProfile.name); }); this.setPortfolioFilterFormValues(); @@ -506,11 +509,11 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { type: 'ASSET_CLASS' }, { - id: filterValue?.holding?.dataSource ?? '', + id: filterValue?.holding?.assetProfile?.dataSource ?? '', type: 'DATA_SOURCE' }, { - id: filterValue?.holding?.symbol ?? '', + id: filterValue?.holding?.assetProfile?.symbol ?? '', type: 'SYMBOL' }, { @@ -697,18 +700,16 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { return EMPTY; }), map(({ holdings }) => { - return holdings.map( - ({ assetSubClass, currency, dataSource, name, symbol }) => { - return { - currency, - dataSource, - name, - symbol, - assetSubClassString: translate(assetSubClass ?? ''), - mode: SearchMode.HOLDING as const - }; - } - ); + return holdings.map(({ assetProfile }) => { + return { + assetSubClassString: translate(assetProfile.assetSubClass ?? ''), + currency: assetProfile.currency, + dataSource: assetProfile.dataSource, + mode: SearchMode.HOLDING as const, + name: assetProfile.name, + symbol: assetProfile.symbol + }; + }); }), takeUntil(this.unsubscribeSubject) ); @@ -752,12 +753,13 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit { 'filters.dataSource' ] as DataSource; const symbol = this.user?.settings?.['filters.symbol']; - const selectedHolding = this.holdings.find((holding) => { + + const selectedHolding = this.holdings.find(({ assetProfile }) => { return ( !!(dataSource && symbol) && getAssetProfileIdentifier({ - dataSource: holding.dataSource, - symbol: holding.symbol + dataSource: assetProfile.dataSource, + symbol: assetProfile.symbol }) === getAssetProfileIdentifier({ dataSource, symbol }) ); }); diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.html b/libs/ui/src/lib/holdings-table/holdings-table.component.html index 250eff578..28a0ac10f 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.html +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -11,9 +11,9 @@ @@ -24,13 +24,13 @@
- {{ element.name }} - @if (element.name === element.symbol) { - ({{ element.assetSubClassLabel }}) + {{ element.assetProfile.name }} + @if (element.assetProfile.name === element.assetProfile.symbol) { + ({{ element.assetProfile.assetSubClassLabel }}) }
- {{ element.symbol }} + {{ element.assetProfile.symbol }}
@@ -185,8 +185,8 @@ (click)=" canShowDetails(row) && onOpenHoldingDialog({ - dataSource: row.dataSource, - symbol: row.symbol + dataSource: row.assetProfile.dataSource, + symbol: row.assetProfile.symbol }) " > diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.ts index bea555a0b..35251bf04 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.ts @@ -103,10 +103,10 @@ export class GfHoldingsTableComponent { }); } - protected canShowDetails(holding: PortfolioPosition): boolean { + protected canShowDetails({ assetProfile }: PortfolioPosition): boolean { return ( this.hasPermissionToOpenDetails() && - !this.ignoreAssetSubClasses.includes(holding.assetSubClass) + !this.ignoreAssetSubClasses.includes(assetProfile.assetSubClass) ); } diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html index f5dbac698..18001fe29 100644 --- a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.html @@ -29,18 +29,22 @@ [compareWith]="holdingComparisonFunction" > {{ - filterForm.get('holding')?.value?.name + filterForm.get('holding')?.value?.assetProfile?.name }} - @for (holding of holdings(); track holding.name) { + @for ( + holding of holdings(); + track getAssetProfileIdentifier(holding.assetProfile) + ) {
{{ holding.name }}{{ holding.assetProfile.name }}
{{ holding.symbol | gfSymbol }} · {{ holding.currency }}{{ holding.assetProfile.symbol | gfSymbol }} · + {{ holding.currency }}
diff --git a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts index c1f82315c..766d954f8 100644 --- a/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts +++ b/libs/ui/src/lib/portfolio-filter-form/portfolio-filter-form.component.ts @@ -63,6 +63,8 @@ export class GfPortfolioFilterFormComponent public readonly holdings = input([]); public readonly tags = input([]); + public getAssetProfileIdentifier = getAssetProfileIdentifier; + public filterForm: FormGroup<{ account: FormControl; assetClass: FormControl; @@ -109,7 +111,8 @@ export class GfPortfolioFilterFormComponent } return ( - getAssetProfileIdentifier(option) === getAssetProfileIdentifier(value) + getAssetProfileIdentifier(option.assetProfile) === + getAssetProfileIdentifier(value.assetProfile) ); } diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.html b/libs/ui/src/lib/top-holdings/top-holdings.component.html index 7a2a84126..5ecebc661 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.html +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -121,7 +121,7 @@ *matRowDef="let row; columns: displayedColumns" mat-row [ngClass]="{ 'cursor-pointer': row.position }" - (click)="onClickHolding(row.position)" + (click)="onClickHolding(row.position.assetProfile)" > { const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`; - const name = raw._data.name; + const name = raw._data.assetProfile.name; const sign = raw._data.netPerformancePercentWithCurrencyEffect > 0 ? '+' : ''; - const symbol = raw._data.symbol; + const symbol = raw._data.assetProfile.symbol; const netPerformanceInPercentageWithSign = `${sign}${(raw._data.netPerformancePercentWithCurrencyEffect * 100).toFixed(2)}%`; From 7e3542f655b42c9b883f1dff39c7d4f1daf2bacb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 8 Mar 2026 07:22:03 +0100 Subject: [PATCH 2/3] Update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98bcdb20f..590f151a7 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 + +### Changed + +- Switched to using asset profile data from the endpoint `GET api/v1/portfolio/holdings` +- Switched to using asset profile data from the holdings of the public page + ## 2.249.0 - 2026-03-10 ### Added From 80a62e1e90eab1d46f35f28daeab337f5f0fce61 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:18:43 +0100 Subject: [PATCH 3/3] Use asset profile data --- apps/api/src/app/portfolio/portfolio.service.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 77b5202cc..55e6ee3f4 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -570,7 +570,6 @@ export class PortfolioService { for (const { activitiesCount, - currency, dateOfFirstActivity, dividend, grossPerformance, @@ -621,7 +620,6 @@ export class PortfolioService { ? 0 : valueInBaseCurrency.div(filteredValueInBaseCurrency).toNumber(), assetProfile: { - currency, assetClass: assetProfile.assetClass, assetSubClass: assetProfile.assetSubClass, countries: assetProfile.countries, @@ -1681,7 +1679,6 @@ export class PortfolioService { currency, assetClass: AssetClass.LIQUIDITY, assetSubClass: AssetSubClass.CASH, - currency, countries: [], dataSource: undefined, holdings: [],