From f89a9f28fbbe9ef573edef903c5853d390643c1d Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:33:20 +0700 Subject: [PATCH 1/5] Task/improve Angular template type safety in client (#7762) * feat(client): resolve angular type errors * feat(libs): resolve angular type errors * feat(client): enable strict input access modifiers * fix(client): set strict templates to false * fix(client): resolve type errors * feat(client): enable strict literal types * feat(client): lock down other strict rules to prevent regression * feat(client): enable strict null input types --- .../asset-profile-dialog.html | 1 - .../app/components/header/header.component.ts | 2 +- .../home-holdings/home-holdings.component.ts | 27 ++++++++++--------- .../investment-chart.component.ts | 20 +++++++------- .../components/markets/markets.component.ts | 9 ++++--- .../create-or-update-activity-dialog.html | 1 + .../allocations/allocations-page.component.ts | 17 +++++++----- .../analysis/analysis-page.component.ts | 2 +- .../user-account-registration-dialog.html | 2 +- apps/client/tsconfig.json | 12 ++++++++- .../src/lib/types/toggle-option.type.ts | 6 ++--- .../lib/portfolio-proportion-chart/index.ts | 1 + .../symbol-autocomplete.component.ts | 2 +- libs/ui/src/lib/toggle/toggle.component.ts | 11 ++++---- 14 files changed, 65 insertions(+), 48 deletions(-) diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index b59b7e39f9..a68768ff19 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -660,7 +660,6 @@ formControlName="headers" matInput type="text" - [matAutocomplete]="auto" > diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 8e70c1a8c2..d4a2fdbabc 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -220,7 +220,7 @@ export class GfHeaderComponent implements OnChanges { this.assistentMenuTriggerElement().closeMenu(); } - protected impersonateAccount(aId: string) { + protected impersonateAccount(aId: string | null) { if (aId) { this.impersonationStorageService.setId(aId); } else { diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index ca6351429f..34b672bc33 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -56,20 +56,21 @@ export class GfHomeHoldingsComponent implements OnInit { protected holdings: PortfolioPosition[] | undefined; protected holdingsViewMode: HoldingsViewMode = GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE; - protected readonly holdingsViewModeOptions: ToggleOption[] = [ - { - iconName: 'reorder-four-outline', - title: $localize`Table`, - value: 'TABLE' - }, - { - iconName: 'grid-outline', - title: $localize`Chart`, - value: 'CHART' - } - ]; + protected readonly holdingsViewModeOptions: ToggleOption[] = + [ + { + iconName: 'reorder-four-outline', + title: $localize`Table`, + value: 'TABLE' + }, + { + iconName: 'grid-outline', + title: $localize`Chart`, + value: 'CHART' + } + ]; protected holdingType: HoldingType = 'ACTIVE'; - protected readonly holdingTypeOptions: ToggleOption[] = [ + protected readonly holdingTypeOptions: ToggleOption[] = [ { label: $localize`Active`, value: 'ACTIVE' }, { label: $localize`Closed`, value: 'CLOSED' } ]; diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index 2a1ffd97c0..43ac5ff99b 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -53,16 +53,16 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; templateUrl: './investment-chart.component.html' }) export class GfInvestmentChartComponent implements OnChanges, OnDestroy { - @Input() public readonly benchmarkDataItems: InvestmentItem[] = []; - @Input() public readonly benchmarkDataLabel = ''; - @Input() public readonly colorScheme: ColorScheme; - @Input() public readonly currency: string; - @Input() public readonly groupBy: GroupBy; - @Input() public readonly historicalDataItems: LineChartItem[] = []; - @Input() public readonly isInPercentage = false; - @Input() public readonly isLoading = false; - @Input() public readonly locale = getLocale(); - @Input() public readonly savingsRate = 0; + @Input() public benchmarkDataItems: InvestmentItem[] = []; + @Input() public benchmarkDataLabel = ''; + @Input() public colorScheme: ColorScheme; + @Input() public currency: string; + @Input() public groupBy: GroupBy; + @Input() public historicalDataItems: LineChartItem[] = []; + @Input() public isInPercentage = false; + @Input() public isLoading = false; + @Input() public locale = getLocale(); + @Input() public savingsRate = 0; private readonly chartCanvas = viewChild.required>('chartCanvas'); diff --git a/apps/client/src/app/components/markets/markets.component.ts b/apps/client/src/app/components/markets/markets.component.ts index 0317ce6480..5e5a2d937c 100644 --- a/apps/client/src/app/components/markets/markets.component.ts +++ b/apps/client/src/app/components/markets/markets.component.ts @@ -49,10 +49,11 @@ export class GfMarketsComponent implements OnInit { () => this.deviceDetectorService.deviceInfo().deviceType ); - protected readonly fearAndGreedIndexModeOptions: ToggleOption[] = [ - { label: $localize`Stocks`, value: 'STOCKS' }, - { label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' } - ]; + protected readonly fearAndGreedIndexModeOptions: ToggleOption[] = + [ + { label: $localize`Stocks`, value: 'STOCKS' }, + { label: $localize`Cryptocurrencies`, value: 'CRYPTOCURRENCIES' } + ]; protected readonly fearLabel = $localize`Fear`; protected readonly greedLabel = $localize`Greed`; diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 8a797250c0..4740dbbf04 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html @@ -210,6 +210,7 @@ currencyOfAssetProfile === activityForm.get('currencyOfUnitPrice')?.value && currentMarketPrice && + data.activity.type && ['BUY', 'SELL'].includes(data.activity.type) && isToday(activityForm.get('date')?.value) ) { 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 358db2edb5..341509d0c6 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 @@ -12,7 +12,6 @@ import { getCountryName } from '@ghostfolio/common/helper'; import { - AssetProfileIdentifier, HoldingWithParents, PortfolioDetails, PortfolioPosition, @@ -22,7 +21,10 @@ import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasScope, scopes } from '@ghostfolio/common/scopes'; import { MarketAdvanced } from '@ghostfolio/common/types'; import { translate } from '@ghostfolio/ui/i18n'; -import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; +import { + GfPortfolioProportionChartComponent, + PortfolioProportionChartClickEvent +} from '@ghostfolio/ui/portfolio-proportion-chart'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { DataService } from '@ghostfolio/ui/services'; import { GfTopHoldingsComponent } from '@ghostfolio/ui/top-holdings'; @@ -206,7 +208,9 @@ export class GfAllocationsPageComponent implements OnInit { this.initialize(); } - protected onAccountChartClicked({ accountId }: { accountId: string }) { + protected onAccountChartClicked(event: PortfolioProportionChartClickEvent) { + const accountId = 'accountId' in event ? event.accountId : undefined; + if (accountId && accountId !== UNKNOWN_KEY) { void this.router.navigate([], { queryParams: { accountId, accountDetailDialog: true } @@ -214,10 +218,9 @@ export class GfAllocationsPageComponent implements OnInit { } } - protected onSymbolChartClicked({ - dataSource, - symbol - }: AssetProfileIdentifier) { + protected onSymbolChartClicked(event: PortfolioProportionChartClickEvent) { + const { dataSource, symbol } = 'symbol' in event ? event : {}; + if (dataSource && symbol) { void this.router.navigate([], { queryParams: { dataSource, symbol, holdingDetailDialog: true } diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 970f1379c4..3a3c44269d 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -94,7 +94,7 @@ export class GfAnalysisPageComponent implements OnInit { protected isLoadingInvestmentTimelineChart: boolean; protected isLoadingPortfolioPrompt: boolean; protected readonly mode = signal('month'); - protected readonly modeOptions: ToggleOption[] = [ + protected readonly modeOptions: ToggleOption[] = [ { label: $localize`Monthly`, value: 'month' }, { label: $localize`Yearly`, value: 'year' } ]; diff --git a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html index c37abeadd7..660af96905 100644 --- a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html +++ b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.html @@ -75,7 +75,7 @@ matInput readonly type="text" - [(value)]="accessToken" + [value]="accessToken" >
@if ( - assetProfile?.countries?.length > 0 || - assetProfile?.sectors?.length > 0 + (assetProfile?.countries?.length ?? 0) > 0 || + (assetProfile?.sectors?.length ?? 0) > 0 ) { @if ( assetProfile?.countries?.length === 1 && diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.html b/apps/client/src/app/components/admin-settings/admin-settings.component.html index 260aac002e..9d15a6a768 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.html +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.html @@ -230,13 +230,13 @@

Platforms

- +

Tags

- +
diff --git a/apps/client/src/app/components/admin-settings/admin-settings.component.ts b/apps/client/src/app/components/admin-settings/admin-settings.component.ts index 5080e9f856..b0c3dc2d22 100644 --- a/apps/client/src/app/components/admin-settings/admin-settings.component.ts +++ b/apps/client/src/app/components/admin-settings/admin-settings.component.ts @@ -3,6 +3,7 @@ import { GfAdminTagComponent } from '@ghostfolio/client/components/admin-tag/adm import { GfDataProviderStatusComponent } from '@ghostfolio/client/components/data-provider-status/data-provider-status.component'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { + DEFAULT_LOCALE, E_MAIL_LINE_BREAK, PROPERTY_API_KEY_GHOSTFOLIO } from '@ghostfolio/common/config'; @@ -98,6 +99,8 @@ export class GfAdminSettingsComponent implements OnInit { public pricingUrl: string; public user: User; + protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE; + public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index 34b672bc33..7f02cfb64e 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -1,4 +1,5 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; import { AssetProfileIdentifier, PortfolioPosition, @@ -50,6 +51,8 @@ import { DeviceDetectorService } from 'ngx-device-detector'; export class GfHomeHoldingsComponent implements OnInit { public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; + protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE; + protected deviceType: string; protected hasPermissionToAccessHoldingsChart: boolean; protected hasPermissionToCreateActivity: boolean; diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index 5c6a95d8e3..df6d57c0e9 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -37,11 +37,11 @@ }
- @if (hasPermissionToCreateActivity && holdings?.length > 0) { + @if (hasPermissionToCreateActivity && (holdings?.length ?? 0) > 0) {
(undefined); protected readonly deviceType = computed( diff --git a/apps/client/src/app/components/markets/markets.html b/apps/client/src/app/components/markets/markets.html index d834dfd0e8..68b22d26f0 100644 --- a/apps/client/src/app/components/markets/markets.html +++ b/apps/client/src/app/components/markets/markets.html @@ -52,7 +52,7 @@ diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html index d6339a5ffa..8aeb0c4335 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html @@ -1,7 +1,7 @@
- @if (errors()?.length > 0 && !isLoading()) { + @if ((errors()?.length ?? 0) > 0 && !isLoading()) { Holdings @if ( !hasImpersonationId && - summary?.totalValueInBaseCurrency > 0 && + (summary?.totalValueInBaseCurrency ?? 0) > 0 && user?.settings?.isExperimentalFeatures ) { Cash @if ( !hasImpersonationId && - summary?.totalValueInBaseCurrency > 0 && + (summary?.totalValueInBaseCurrency ?? 0) > 0 && user?.settings?.isExperimentalFeatures ) { Excluded from Analysis @if ( !hasImpersonationId && - summary?.totalValueInBaseCurrency > 0 && + (summary?.totalValueInBaseCurrency ?? 0) > 0 && user?.settings?.isExperimentalFeatures ) { Emergency Fund @if ( !hasImpersonationId && - summary?.totalValueInBaseCurrency > 0 && + (summary?.totalValueInBaseCurrency ?? 0) > 0 && user?.settings?.isExperimentalFeatures ) {

Accounts

@@ -236,7 +238,7 @@ >Other Markets
- @if (markets?.[UNKNOWN_KEY]?.valueInPercentage > 0) { + @if ((markets?.[UNKNOWN_KEY]?.valueInPercentage ?? 0) > 0) {
diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index 48c367749d..2899fc61c6 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -25,7 +25,7 @@ "strictLiteralTypes": true, "strictNullInputTypes": true, "strictOutputEventTypes": true, - "strictSafeNavigationTypes": false, + "strictSafeNavigationTypes": true, // TODO: Enable stricter rules for this project "strictTemplates": false }, diff --git a/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html b/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html index 3ff7f03ab6..2f415d874c 100644 --- a/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html +++ b/libs/ui/src/lib/benchmark/benchmark-detail-dialog/benchmark-detail-dialog.html @@ -1,7 +1,7 @@ diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index edc377fa31..5dbb6cc31c 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -4,7 +4,11 @@ import { getValueAxisOptions, getVerticalHoverLinePlugin } from '@ghostfolio/common/chart-helper'; -import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config'; +import { + DEFAULT_COLOR_SCHEME, + primaryColorRgb, + secondaryColorRgb +} from '@ghostfolio/common/config'; import { getBackgroundColor, getLocale } from '@ghostfolio/common/helper'; import { LineChartItem } from '@ghostfolio/common/interfaces'; import { ColorScheme } from '@ghostfolio/common/types'; @@ -52,7 +56,7 @@ export class GfLineChartComponent { @Input() benchmarkDataItems: LineChartItem[] = []; @Input() benchmarkLabel = ''; - @Input() colorScheme: ColorScheme; + @Input() colorScheme: ColorScheme = DEFAULT_COLOR_SCHEME; @Input() currency: string; @Input() historicalDataItems: LineChartItem[]; @Input() isAnimated = false; From a36b7f9ea4d49c98fb850290eddabbc86fc06501 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:25:55 +0200 Subject: [PATCH 3/5] Feature/extend MCP tool to get accounts by account id (#7765) * Extend MCP tool to get accounts by account id * Update changelog --- CHANGELOG.md | 1 + .../src/app/endpoints/ai/ai.service.spec.ts | 18 ++++++++++++++++-- apps/api/src/app/endpoints/ai/ai.service.ts | 7 +++++++ .../src/app/endpoints/mcp/mcp.controller.ts | 16 +++++++++++++++- libs/common/src/lib/config.ts | 1 + 5 files changed, 40 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c9a6de0e2..32ecd7b9f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Extended the tool to get the accounts of the portfolio in the server of the Model Context Protocol (MCP) to support the filtering by account (experimental) - Upgraded `uuid` from version `14.0.1` to `14.0.2` ## 3.64.0 - 2026-08-30 diff --git a/apps/api/src/app/endpoints/ai/ai.service.spec.ts b/apps/api/src/app/endpoints/ai/ai.service.spec.ts index 3b64791548..6328df1952 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.spec.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.spec.ts @@ -29,13 +29,16 @@ interface AiServiceWithMarkdownTable { } function createAccount({ + id = 'account-a-id', isExcluded = false, name = 'Account A' }: { + id?: string; isExcluded?: boolean; name?: string; } = {}) { return { + id, name, activitiesCount: 3, allocationInPercentage: 0.25, @@ -86,6 +89,7 @@ describe('AiService', () => { describe('getAccountsTableColumnNames', () => { it('gives no column with a monetary value', () => { expect(AiService.getAccountsTableColumnNames()).toEqual([ + 'Id', 'Name', 'Currency', 'Platform', @@ -121,10 +125,20 @@ describe('AiService', () => { expect(result).not.toContain('2000'); }); + // The accountIds parameter of the tool takes the identifiers, hence the + // table has to give them + it('gives the identifier of an account', async () => { + const aiService = createAiService([createAccount()]); + + const result = await aiService.getAccountsTable({ userId: 'user-id' }); + + expect(result).toContain('account-a-id'); + }); + it('marks an account which is excluded from the analysis', async () => { const aiService = createAiService([ createAccount({ isExcluded: true }), - createAccount({ name: 'Account B' }) + createAccount({ id: 'account-b-id', name: 'Account B' }) ]); const result = await aiService.getAccountsTable({ userId: 'user-id' }); @@ -132,7 +146,7 @@ describe('AiService', () => { const [rowOfAccountA, rowOfAccountB] = result .split('\n') .filter((line) => { - return line.startsWith('Account '); + return line.startsWith('account-'); }); expect(rowOfAccountA).toContain('true'); diff --git a/apps/api/src/app/endpoints/ai/ai.service.ts b/apps/api/src/app/endpoints/ai/ai.service.ts index 029fcc81bf..758499c6e6 100644 --- a/apps/api/src/app/endpoints/ai/ai.service.ts +++ b/apps/api/src/app/endpoints/ai/ai.service.ts @@ -30,9 +30,11 @@ export class AiService { | 'ALLOCATION_PERCENTAGE' | 'CURRENCY' | 'EXCLUDED_FROM_ANALYSIS' + | 'ID' | 'NAME' | 'PLATFORM'; } & ColumnDescriptor)[] = [ + { key: 'ID', name: 'Id' }, { key: 'NAME', name: 'Name' }, { key: 'CURRENCY', name: 'Currency' }, { key: 'PLATFORM', name: 'Platform' }, @@ -160,6 +162,7 @@ export class AiService { activitiesCount, allocationInPercentage, currency, + id, name: label, platform, tags @@ -183,6 +186,10 @@ export class AiService { row[name] = isAccountExcluded({ tags }).toString(); break; + case 'ID': + row[name] = id; + break; + case 'NAME': row[name] = label ?? ''; break; diff --git a/apps/api/src/app/endpoints/mcp/mcp.controller.ts b/apps/api/src/app/endpoints/mcp/mcp.controller.ts index f238b4890e..f8261f5c05 100644 --- a/apps/api/src/app/endpoints/mcp/mcp.controller.ts +++ b/apps/api/src/app/endpoints/mcp/mcp.controller.ts @@ -8,6 +8,7 @@ import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper' import { DATE_RANGES, DEFAULT_LANGUAGE_CODE, + MCP_MAX_ACCOUNTS, MCP_MAX_ACTIVITIES } from '@ghostfolio/common/config'; import { scopes } from '@ghostfolio/common/scopes'; @@ -20,6 +21,14 @@ import { McpController, Tool } from '@rekog/mcp-nest'; import { z } from 'zod'; const GET_ACCOUNTS_PARAMETERS = z.object({ + accountIds: z + .array(z.string().min(1)) + .min(1) + .max(MCP_MAX_ACCOUNTS) + .optional() + .describe( + `The identifiers of the accounts to get, at most ${MCP_MAX_ACCOUNTS}` + ), assetClasses: z .array(z.enum(AssetClass)) .min(1) @@ -104,9 +113,14 @@ export class GhostfolioMcpController { public async getAccounts( @Impersonation() { userId }: ImpersonationContext, @Payload() - { assetClasses, holding }: z.infer + { + accountIds, + assetClasses, + holding + }: z.infer ) { const filters = this.apiService.buildFiltersFromQueryParams({ + filterByAccounts: accountIds?.join(','), filterByAssetClasses: assetClasses?.join(','), filterByDataSource: holding?.dataSource, filterBySymbol: holding?.symbol diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 8946d986c8..5e11f8ae17 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -277,6 +277,7 @@ export const HTTP_RESPONSE_MESSAGE_IMPERSONATION_UNRESOLVED = export const MAX_TOP_HOLDINGS = 50; export const MCP_ENDPOINT = '/mcp'; +export const MCP_MAX_ACCOUNTS = 50; export const MCP_MAX_ACTIVITIES = 100; export const MCP_REALM = 'Ghostfolio'; From 92b9ca3e2e75cbef17c9c075219f33e6d3acd574 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 30 Aug 2026 17:41:52 +0200 Subject: [PATCH 4/5] Task/upgrade undici to version 8.10.0 (#7759) * Update undici to version 8.10.0 * Update changelog --- CHANGELOG.md | 1 + package-lock.json | 8 ++++---- package.json | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ecd7b9f3..269d816714 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Extended the tool to get the accounts of the portfolio in the server of the Model Context Protocol (MCP) to support the filtering by account (experimental) +- Upgraded `undici` from version `8.5.0` to `8.10.0` - Upgraded `uuid` from version `14.0.1` to `14.0.2` ## 3.64.0 - 2026-08-30 diff --git a/package-lock.json b/package-lock.json index ae6d3369c9..d374872e25 100644 --- a/package-lock.json +++ b/package-lock.json @@ -102,7 +102,7 @@ "svgmap": "2.21.0", "tablemark": "4.1.0", "twitter-api-v2": "1.29.1", - "undici": "8.5.0", + "undici": "8.10.0", "uuid": "14.0.2", "yahoo-finance2": "4.0.2", "zod": "4.4.3", @@ -34889,9 +34889,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "8.5.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", - "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", "license": "MIT", "engines": { "node": ">=22.19.0" diff --git a/package.json b/package.json index 759b17bc63..7188db305a 100644 --- a/package.json +++ b/package.json @@ -146,7 +146,7 @@ "svgmap": "2.21.0", "tablemark": "4.1.0", "twitter-api-v2": "1.29.1", - "undici": "8.5.0", + "undici": "8.10.0", "uuid": "14.0.2", "yahoo-finance2": "4.0.2", "zod": "4.4.3", From 82b243f9ed92bb11cda285fd8c9fc6d45471014c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 30 Aug 2026 19:33:10 +0200 Subject: [PATCH 5/5] Bugfix/handling of cash positions (#7660) * Fix handling of cash positions * Update changelog --- CHANGELOG.md | 5 + .../app/endpoints/public/public.service.ts | 20 +- .../src/app/portfolio/portfolio.controller.ts | 49 +---- apps/api/src/helper/portfolio.helper.ts | 49 +++++ .../allocations/allocations-page.component.ts | 203 ++++++++++-------- .../app/pages/public/public-page.component.ts | 53 +++-- libs/common/src/lib/helper.ts | 24 +++ .../public-portfolio-response.interface.ts | 1 - libs/ui/src/lib/services/data.service.ts | 6 - 9 files changed, 234 insertions(+), 176 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 269d816714..07dec9f693 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Upgraded `undici` from version `8.5.0` to `8.10.0` - Upgraded `uuid` from version `14.0.1` to `14.0.2` +### Fixed + +- Fixed the cash positions being included in the by continent, by country and by sector charts on the allocations page and the public page +- Fixed the allocations in percentage exceeding 100% in the restricted view + ## 3.64.0 - 2026-08-30 ### Added diff --git a/apps/api/src/app/endpoints/public/public.service.ts b/apps/api/src/app/endpoints/public/public.service.ts index d86f1f0d8e..c32df6d82d 100644 --- a/apps/api/src/app/endpoints/public/public.service.ts +++ b/apps/api/src/app/endpoints/public/public.service.ts @@ -6,18 +6,14 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import { SubscriptionType } from '@ghostfolio/common/enums'; -import { getSum } from '@ghostfolio/common/helper'; +import { getSum, isCashPosition } from '@ghostfolio/common/helper'; import { AccessSettings, PublicPortfolioResponse } from '@ghostfolio/common/interfaces'; import { HttpException, Injectable } from '@nestjs/common'; -import { - AssetClass, - AssetSubClass, - Type as ActivityType -} from '@prisma/client'; +import { Type as ActivityType } from '@prisma/client'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -173,23 +169,19 @@ export class PublicService { assetProfile: { ...holding.assetProfile, assetClass: - hasDetails || - holding.assetProfile.assetClass === AssetClass.LIQUIDITY + hasDetails || isCashPosition(holding.assetProfile) ? holding.assetProfile.assetClass : undefined, assetClassLabel: - hasDetails || - holding.assetProfile.assetClass === AssetClass.LIQUIDITY + hasDetails || isCashPosition(holding.assetProfile) ? holding.assetProfile.assetClassLabel : undefined, assetSubClass: - hasDetails || - holding.assetProfile.assetSubClass === AssetSubClass.CASH + hasDetails || isCashPosition(holding.assetProfile) ? holding.assetProfile.assetSubClass : undefined, assetSubClassLabel: - hasDetails || - holding.assetProfile.assetSubClass === AssetSubClass.CASH + hasDetails || isCashPosition(holding.assetProfile) ? holding.assetProfile.assetSubClassLabel : undefined, holdings: holding.assetProfile.holdings?.map( diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 02b8479323..431a8da20b 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -6,6 +6,7 @@ import { hasNotDefinedValuesInObject, nullifyValuesInObject } from '@ghostfolio/api/helper/object.helper'; +import { convertValuesToPercentages } from '@ghostfolio/api/helper/portfolio.helper'; import { PerformanceLoggingInterceptor } from '@ghostfolio/api/interceptors/performance-logging/performance-logging.interceptor'; import { RedactValuesInResponseInterceptor } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; @@ -15,6 +16,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { SubscriptionType } from '@ghostfolio/common/enums'; +import { isCashPosition } from '@ghostfolio/common/helper'; import { PortfolioDetails, PortfolioDividendsResponse, @@ -44,7 +46,7 @@ import { Version } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; -import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { DataSource } from '@prisma/client'; import { Big } from 'big.js'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -128,38 +130,7 @@ export class PortfolioController { !hasScope(impersonationScopes, scopes.portfolioReadValues) || isRestrictedView(this.request.user) ) { - const totalInvestment = holdings - .map(({ investment }) => { - return investment; - }) - .reduce((a, b) => a + b, 0); - - const totalValue = holdings - .filter(({ assetProfile }) => { - return ( - assetProfile.assetClass !== AssetClass.LIQUIDITY && - assetProfile.assetSubClass !== AssetSubClass.CASH - ); - }) - .map(({ valueInBaseCurrency }) => { - return valueInBaseCurrency; - }) - .reduce((a, b) => { - return a + b; - }, 0); - - for (const holding of holdings) { - holding.investment = holding.investment / totalInvestment; - holding.valueInPercentage = holding.valueInBaseCurrency / totalValue; - } - - for (const [name, { valueInBaseCurrency }] of Object.entries(accounts)) { - accounts[name].valueInPercentage = valueInBaseCurrency / totalValue; - } - - for (const [name, { valueInBaseCurrency }] of Object.entries(platforms)) { - platforms[name].valueInPercentage = valueInBaseCurrency / totalValue; - } + convertValuesToPercentages({ accounts, holdings, platforms }); } if ( @@ -208,23 +179,19 @@ export class PortfolioController { assetProfile: { ...portfolioPosition.assetProfile, assetClass: - hasDetails || - portfolioPosition.assetProfile.assetClass === AssetClass.LIQUIDITY + hasDetails || isCashPosition(portfolioPosition.assetProfile) ? portfolioPosition.assetProfile.assetClass : undefined, assetClassLabel: - hasDetails || - portfolioPosition.assetProfile.assetClass === AssetClass.LIQUIDITY + hasDetails || isCashPosition(portfolioPosition.assetProfile) ? portfolioPosition.assetProfile.assetClassLabel : undefined, assetSubClass: - hasDetails || - portfolioPosition.assetProfile.assetSubClass === AssetSubClass.CASH + hasDetails || isCashPosition(portfolioPosition.assetProfile) ? portfolioPosition.assetProfile.assetSubClass : undefined, assetSubClassLabel: - hasDetails || - portfolioPosition.assetProfile.assetSubClass === AssetSubClass.CASH + hasDetails || isCashPosition(portfolioPosition.assetProfile) ? portfolioPosition.assetProfile.assetSubClassLabel : undefined, ...(hasDetails diff --git a/apps/api/src/helper/portfolio.helper.ts b/apps/api/src/helper/portfolio.helper.ts index 6ebe48d3c7..b8152414bb 100644 --- a/apps/api/src/helper/portfolio.helper.ts +++ b/apps/api/src/helper/portfolio.helper.ts @@ -1,5 +1,54 @@ +import { PortfolioDetails } from '@ghostfolio/common/interfaces'; + import { Type as ActivityType } from '@prisma/client'; +export function convertValuesToPercentages({ + accounts, + holdings, + platforms +}: { + accounts: PortfolioDetails['accounts']; + holdings: PortfolioDetails['holdings']; + platforms: PortfolioDetails['platforms']; +}) { + const totalInvestment = holdings + .map(({ investment }) => { + return investment; + }) + .reduce((a, b) => { + return a + b; + }, 0); + + const totalValue = holdings + .map(({ valueInBaseCurrency }) => { + return valueInBaseCurrency; + }) + .reduce((a, b) => { + return a + b; + }, 0); + + for (const holding of holdings) { + holding.investment = totalInvestment + ? holding.investment / totalInvestment + : 0; + holding.valueInPercentage = totalValue + ? holding.valueInBaseCurrency / totalValue + : 0; + } + + for (const account of Object.values(accounts)) { + account.valueInPercentage = totalValue + ? account.valueInBaseCurrency / totalValue + : 0; + } + + for (const platform of Object.values(platforms)) { + platform.valueInPercentage = totalValue + ? platform.valueInBaseCurrency / totalValue + : 0; + } +} + export function getFactor(activityType: ActivityType) { let factor: number; 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 341509d0c6..ceda1d4c07 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 @@ -8,8 +8,10 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { canOpenHoldingDetail, + convertValuesToPercentagesOfTotal, getAssetProfileIdentifier, - getCountryName + getCountryName, + isCashPosition } from '@ghostfolio/common/helper'; import { HoldingWithParents, @@ -270,6 +272,20 @@ export class GfAllocationsPageComponent implements OnInit { return UNKNOWN_KEY; } + private extractValue({ + valueInBaseCurrency, + valueInPercentage + }: { + valueInBaseCurrency?: PortfolioPosition['valueInBaseCurrency']; + valueInPercentage?: PortfolioPosition['valueInPercentage']; + }) { + return ( + (isNumber(valueInBaseCurrency) + ? valueInBaseCurrency + : valueInPercentage) ?? 0 + ); + } + private fetchPortfolioDetails() { return this.dataService.fetchPortfolioDetails({ filters: this.userService.getFilters(), @@ -373,6 +389,8 @@ export class GfAllocationsPageComponent implements OnInit { }; } + let totalValueExcludingCashPositions = 0; + for (const position of this.portfolioDetails.holdings) { const assetProfileIdentifier = getAssetProfileIdentifier( position.assetProfile @@ -397,111 +415,104 @@ export class GfAllocationsPageComponent implements OnInit { : (position.valueInBaseCurrency ?? 0) }; - // 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; - const value = - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage) ?? 0; + if (!isCashPosition(position.assetProfile)) { + // Prepare analysis data by continents, countries, holdings and sectors + // except for cash + + totalValueExcludingCashPositions += this.extractValue(position); + + if (position.assetProfile.countries.length > 0) { + for (const country of position.assetProfile.countries) { + const { code, continent, weight } = country; + const value = this.extractValue(position); + + const continentData = this.continents[continent]; + + if (continentData) { + continentData.value += weight * value; + } else { + this.continents[continent] = { + name: translate(continent), + value: weight * value + }; + } + + const countryData = this.countries[code]; + + if (countryData) { + countryData.value += weight * value; + } else { + this.countries[code] = { + name: getCountryName({ code }), + value: weight * value + }; + } + } + } else { + const value = this.extractValue(position); - const continentData = this.continents[continent]; + const continentData = this.continents[UNKNOWN_KEY]; if (continentData) { - continentData.value += weight * value; - } else { - this.continents[continent] = { - name: translate(continent), - value: weight * value - }; + continentData.value += value; } - const countryData = this.countries[code]; + const countryData = this.countries[UNKNOWN_KEY]; if (countryData) { - countryData.value += weight * value; - } else { - this.countries[code] = { - name: getCountryName({ code }), - value: weight * value - }; + countryData.value += value; } } - } else { - const value = - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage) ?? 0; - - const continentData = this.continents[UNKNOWN_KEY]; - - if (continentData) { - continentData.value += value; - } - - const countryData = this.countries[UNKNOWN_KEY]; - if (countryData) { - countryData.value += value; - } - } - - if (position.assetProfile.holdings.length > 0) { - for (const { - allocationInPercentage, - name, - valueInBaseCurrency - } of position.assetProfile.holdings) { - const normalizedAssetName = this.normalizeAssetName(name); - const value = isNumber(valueInBaseCurrency) - ? valueInBaseCurrency - : allocationInPercentage * (position.valueInPercentage ?? 0); - - const holdingData = this.topHoldingsMap[normalizedAssetName]; - - if (holdingData) { - holdingData.value += value; - } else { - this.topHoldingsMap[normalizedAssetName] = { - name, - value - }; + if (position.assetProfile.holdings.length > 0) { + for (const { + allocationInPercentage, + name, + valueInBaseCurrency + } of position.assetProfile.holdings) { + const normalizedAssetName = this.normalizeAssetName(name); + const value = isNumber(valueInBaseCurrency) + ? valueInBaseCurrency + : allocationInPercentage * (position.valueInPercentage ?? 0); + + const holdingData = this.topHoldingsMap[normalizedAssetName]; + + if (holdingData) { + holdingData.value += value; + } else { + this.topHoldingsMap[normalizedAssetName] = { + name, + value + }; + } } } - } - if (position.assetProfile.sectors.length > 0) { - for (const sector of position.assetProfile.sectors) { - const { name, weight } = sector; - const value = - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage) ?? 0; + if (position.assetProfile.sectors.length > 0) { + for (const sector of position.assetProfile.sectors) { + const { name, weight } = sector; + const value = this.extractValue(position); + + const sectorData = this.sectors[name]; + + if (sectorData) { + sectorData.value += weight * value; + } else { + this.sectors[name] = { + name: translate(name), + value: weight * value + }; + } + } + } else { + const value = this.extractValue(position); - const sectorData = this.sectors[name]; + const sectorData = this.sectors[UNKNOWN_KEY]; if (sectorData) { - sectorData.value += weight * value; - } else { - this.sectors[name] = { - name: translate(name), - value: weight * value - }; + sectorData.value += value; } } - } else { - const value = - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage) ?? 0; - - const sectorData = this.sectors[UNKNOWN_KEY]; - - if (sectorData) { - sectorData.value += value; - } } if (this.holdings[assetProfileIdentifier].assetSubClass === 'ETF') { @@ -510,10 +521,7 @@ export class GfAllocationsPageComponent implements OnInit { const symbol = position.assetProfile.symbol; - const value = - (isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : position.valueInPercentage) ?? 0; + const value = this.extractValue(position); const symbolData = this.symbols[symbol]; @@ -533,6 +541,17 @@ export class GfAllocationsPageComponent implements OnInit { } } + if (this.showValuesInPercentage()) { + // The values are percentages of the whole portfolio, but the analysis + // data does not contain the cash positions + for (const values of [this.continents, this.countries, this.sectors]) { + convertValuesToPercentagesOfTotal({ + values, + total: totalValueExcludingCashPositions + }); + } + } + this.markets = this.portfolioDetails.markets; if (this.portfolioDetails.marketsAdvanced) { diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index e76154ce0e..46606753e4 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -1,7 +1,9 @@ import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { + convertValuesToPercentagesOfTotal, getAssetProfileIdentifier, - getCountryName + getCountryName, + isCashPosition } from '@ghostfolio/common/helper'; import { InfoItem, @@ -34,9 +36,7 @@ import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatTableDataSource } from '@angular/material/table'; import { ActivatedRoute, Router } from '@angular/router'; -import { AssetClass } from '@prisma/client'; import { StatusCodes } from 'http-status-codes'; -import { isNumber } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; import { EMPTY } from 'rxjs'; import { catchError } from 'rxjs/operators'; @@ -178,6 +178,8 @@ export class GfPublicPageComponent implements OnInit { } }; + let totalValueExcludingCashPositions = 0; + for (const position of this.publicPortfolioDetails.holdings) { const assetProfileIdentifier = getAssetProfileIdentifier( position.assetProfile @@ -191,39 +193,40 @@ export class GfPublicPageComponent implements OnInit { value: position.allocationInPercentage }; - if (position.assetProfile.assetClass !== AssetClass.LIQUIDITY) { - // Prepare analysis data by continents, countries, holdings and sectors except for liquidity + if (!isCashPosition(position.assetProfile)) { + // Prepare analysis data by continents, countries, holdings and sectors + // except for cash + + const value = position.valueInPercentage ?? 0; + + totalValueExcludingCashPositions += value; 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 * (position.valueInBaseCurrency ?? 0); + this.continents[continent].value += weight * value; } else { this.continents[continent] = { name: translate(continent), - value: weight * (position.valueInBaseCurrency ?? 0) + value: weight * value }; } if (this.countries[code]?.value) { - this.countries[code].value += - weight * (position.valueInBaseCurrency ?? 0); + this.countries[code].value += weight * value; } else { this.countries[code] = { name: getCountryName({ code }), - value: weight * (position.valueInBaseCurrency ?? 0) + value: weight * value }; } } } else { - this.continents[UNKNOWN_KEY].value += - position.valueInBaseCurrency ?? 0; + this.continents[UNKNOWN_KEY].value += value; - this.countries[UNKNOWN_KEY].value += - position.valueInBaseCurrency ?? 0; + this.countries[UNKNOWN_KEY].value += value; } if (position.assetProfile.sectors.length > 0) { @@ -231,25 +234,22 @@ export class GfPublicPageComponent implements OnInit { const { name, weight } = sector; if (this.sectors[name]?.value) { - this.sectors[name].value += - weight * (position.valueInBaseCurrency ?? 0); + this.sectors[name].value += weight * value; } else { this.sectors[name] = { name: translate(name), - value: weight * (position.valueInBaseCurrency ?? 0) + value: weight * value }; } } } else { - this.sectors[UNKNOWN_KEY].value += position.valueInBaseCurrency ?? 0; + this.sectors[UNKNOWN_KEY].value += value; } } const symbol = position.assetProfile.symbol; - const value = isNumber(position.valueInBaseCurrency) - ? position.valueInBaseCurrency - : (position.valueInPercentage ?? 0); + const value = position.valueInPercentage ?? 0; const symbolData = this.symbols[symbol]; @@ -264,5 +264,14 @@ export class GfPublicPageComponent implements OnInit { }; } } + + // The values are percentages of the whole portfolio, but the analysis data + // does not contain the cash positions + for (const values of [this.continents, this.countries, this.sectors]) { + convertValuesToPercentagesOfTotal({ + values, + total: totalValueExcludingCashPositions + }); + } } } diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 6accde31c9..b6b097be94 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -232,6 +232,22 @@ export function capitalize(aString: string) { return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); } +export function convertValuesToPercentagesOfTotal({ + total, + values +}: { + total: number; + values: { [key: string]: { value: number } }; +}) { + if (!total) { + return; + } + + for (const item of Object.values(values)) { + item.value = item.value / total; + } +} + export function downloadAsFile({ content, contentType = 'text/plain', @@ -553,6 +569,14 @@ export function isAccountExcluded(account?: { tags?: { id: string }[] }) { ); } +export function isCashPosition({ + assetSubClass +}: { + assetSubClass?: AssetSubClass; +} = {}) { + return assetSubClass === AssetSubClass.CASH; +} + export function isCurrency(aCurrency: string) { if (!aCurrency) { return false; 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 ce4bc2817a..616dbac992 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 @@ -17,7 +17,6 @@ export interface PublicPortfolioResponse extends PublicPortfolioResponseV1 { | 'dateOfFirstActivity' | 'markets' | 'netPerformancePercentWithCurrencyEffect' - | 'valueInBaseCurrency' | 'valueInPercentage' >[]; latestActivities: (Pick< diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index fb8a5aea68..5f86b7a328 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -788,12 +788,6 @@ export class DataService { holding.assetProfile.assetSubClassLabel = translate( holding.assetProfile.assetSubClass ); - - holding.valueInBaseCurrency = isNumber( - holding.valueInBaseCurrency - ) - ? holding.valueInBaseCurrency - : holding.valueInPercentage; } }