From cce9ab1c552b315d4e9d7a0577ea8335d02b4744 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 5 Sep 2026 15:25:10 +0200 Subject: [PATCH] Improve handling of excluded holdings --- .../src/app/activities/activities.service.ts | 7 ++- .../portfolio-calculator.factory.ts | 8 +++- .../calculator/portfolio-calculator.ts | 12 ++++- .../src/app/portfolio/portfolio.service.ts | 48 +++++++++++++++---- .../activities-table.component.ts | 21 +------- 5 files changed, 63 insertions(+), 33 deletions(-) diff --git a/apps/api/src/app/activities/activities.service.ts b/apps/api/src/app/activities/activities.service.ts index a0d9ffef5..7f6cbc8b0 100644 --- a/apps/api/src/app/activities/activities.service.ts +++ b/apps/api/src/app/activities/activities.service.ts @@ -759,7 +759,8 @@ export class ActivitiesService { filters, userCurrency, userId, - withCash = false + withCash = false, + withExcludedAccountsAndActivities = false }: { /** Optional filters to apply to the activities. */ filters?: Filter[]; @@ -769,13 +770,15 @@ export class ActivitiesService { userId: string; /** Whether to include cash activities in the result. */ withCash?: boolean; + /** Whether to include activities that are excluded from analysis. */ + withExcludedAccountsAndActivities?: boolean; }) { const [activities, splits] = await Promise.all([ this.getActivities({ filters, userCurrency, userId, - withExcludedAccountsAndActivities: false // TODO + withExcludedAccountsAndActivities }), this.assetProfileSplitService.getSplitsByUserId({ userId }) ]); diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts index 7b5ab1a0d..effb185c3 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -34,7 +34,8 @@ export class PortfolioCalculatorFactory { calculationType, currency, filters = [], - userId + userId, + usePortfolioSnapshotCache = true }: { accountBalanceItems?: HistoricalDataItem[]; activities: Activity[]; @@ -42,6 +43,7 @@ export class PortfolioCalculatorFactory { currency: string; filters?: Filter[]; userId: string; + usePortfolioSnapshotCache?: boolean; }): PortfolioCalculator { switch (calculationType) { case PerformanceCalculationType.MWR: @@ -51,6 +53,7 @@ export class PortfolioCalculatorFactory { currency, filters, userId, + usePortfolioSnapshotCache, configurationService: this.configurationService, currentRateService: this.currentRateService, exchangeRateDataService: this.exchangeRateDataService, @@ -65,6 +68,7 @@ export class PortfolioCalculatorFactory { currency, filters, userId, + usePortfolioSnapshotCache, configurationService: this.configurationService, currentRateService: this.currentRateService, exchangeRateDataService: this.exchangeRateDataService, @@ -79,6 +83,7 @@ export class PortfolioCalculatorFactory { currency, filters, userId, + usePortfolioSnapshotCache, configurationService: this.configurationService, currentRateService: this.currentRateService, exchangeRateDataService: this.exchangeRateDataService, @@ -93,6 +98,7 @@ export class PortfolioCalculatorFactory { currency, filters, userId, + usePortfolioSnapshotCache, configurationService: this.configurationService, currentRateService: this.currentRateService, exchangeRateDataService: this.exchangeRateDataService, diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 971d8b564..13c2d64fb 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -91,6 +91,7 @@ export abstract class PortfolioCalculator { private startDate: Date; private transactionPoints: TransactionPoint[]; private userId: string; + private usePortfolioSnapshotCache: boolean; public constructor({ accountBalanceItems, @@ -102,7 +103,8 @@ export abstract class PortfolioCalculator { filters, portfolioSnapshotService, redisCacheService, - userId + userId, + usePortfolioSnapshotCache = true }: { accountBalanceItems: HistoricalDataItem[]; activities: Activity[]; @@ -114,6 +116,7 @@ export abstract class PortfolioCalculator { portfolioSnapshotService: PortfolioSnapshotService; redisCacheService: RedisCacheService; userId: string; + usePortfolioSnapshotCache?: boolean; }) { this.accountBalanceItems = accountBalanceItems; this.configurationService = configurationService; @@ -176,6 +179,7 @@ export abstract class PortfolioCalculator { this.portfolioSnapshotService = portfolioSnapshotService; this.redisCacheService = redisCacheService; this.userId = userId; + this.usePortfolioSnapshotCache = usePortfolioSnapshotCache; const { endDate, startDate } = getIntervalFromDateRange({ dateRange: 'max', @@ -187,7 +191,11 @@ export abstract class PortfolioCalculator { this.computeTransactionPoints(); - this.snapshotPromise = this.initialize(); + this.snapshotPromise = this.usePortfolioSnapshotCache + ? this.initialize() + : this.computeSnapshot().then((snapshot) => { + this.snapshot = snapshot; + }); // Mark the rejection as handled to prevent an unhandled promise rejection // in case the snapshot promise is never awaited. Consumers awaiting it diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9f81754f4..f4de83fc5 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -895,12 +895,36 @@ export class PortfolioService { const user = await this.userService.user({ id: userId }); const userCurrency = this.getUserCurrency(user); - const { activities } = + const holdingFilters: Filter[] = [ + { id: dataSource, type: 'DATA_SOURCE' }, + { id: symbol, type: 'SYMBOL' } + ]; + + const { activities: allActivitiesOfHolding } = await this.activitiesService.getActivitiesForPortfolioCalculator({ userCurrency, - userId + userId, + filters: holdingFilters, + withExcludedAccountsAndActivities: true }); + if (allActivitiesOfHolding.length === 0) { + return undefined; + } + + const hasExcludedActivities = allActivitiesOfHolding.some((activity) => { + return this.isExcludedFromAnalysis(activity); + }); + + const activities = hasExcludedActivities + ? allActivitiesOfHolding + : ( + await this.activitiesService.getActivitiesForPortfolioCalculator({ + userCurrency, + userId + }) + ).activities; + if (activities.length === 0) { return undefined; } @@ -927,7 +951,9 @@ export class PortfolioService { activities, userId, calculationType: this.getUserPerformanceCalculationType(user), - currency: userCurrency + currency: userCurrency, + filters: hasExcludedActivities ? holdingFilters : undefined, + usePortfolioSnapshotCache: !hasExcludedActivities }); const transactionPoints = portfolioCalculator.getTransactionPoints(); @@ -2033,12 +2059,7 @@ export class PortfolioService { const nonExcludedActivities: Activity[] = []; for (const activity of activities) { - if ( - (activity.account && isAccountExcluded(activity.account)) || - activity.tags?.some(({ id }) => { - return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; - }) - ) { + if (this.isExcludedFromAnalysis(activity)) { excludedActivities.push(activity); } else { nonExcludedActivities.push(activity); @@ -2204,6 +2225,15 @@ export class PortfolioService { }; } + private isExcludedFromAnalysis(activity: Activity) { + return ( + isAccountExcluded(activity.account) || + activity.tags?.some(({ id }) => { + return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; + }) === true + ); + } + private getSumOfActivityType({ activities, activityType, diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index acf17c933..464660657 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -1,13 +1,6 @@ -import { - DEFAULT_PAGE_SIZE, - TAG_ID_EXCLUDE_FROM_ANALYSIS -} from '@ghostfolio/common/config'; +import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { ConfirmationDialogType } from '@ghostfolio/common/enums'; -import { - getLocale, - isAccountExcluded, - isDraftActivity -} from '@ghostfolio/common/helper'; +import { getLocale, isDraftActivity } from '@ghostfolio/common/helper'; import { Activity, AssetProfileIdentifier @@ -286,7 +279,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { public canClickActivity(activity: Activity) { return ( this.hasPermissionToOpenDetails && - this.isExcludedFromAnalysis(activity) === false && isDraftActivity(activity) === false && ['BUY', 'DIVIDEND', 'SELL'].includes(activity.type) ); @@ -306,15 +298,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit { ); } - public isExcludedFromAnalysis(activity: Activity) { - return ( - isAccountExcluded(activity.account) || - activity.tags?.some(({ id }) => { - return id === TAG_ID_EXCLUDE_FROM_ANALYSIS; - }) === true - ); - } - public onChangePage(page: PageEvent) { this.pageChanged.emit(page); }