diff --git a/CHANGELOG.md b/CHANGELOG.md index 6cc09560af..b9cbcd7e21 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 ### Added +- Added the performance timeline grouped by year to the analysis page (experimental) - Extended the `GET api/v2/portfolio/performance` endpoint by the `groupBy` query parameter (`year`) ### Changed diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 109b5cca59..72468f1d6e 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -4,7 +4,8 @@ import { Impersonation } from '@ghostfolio/api/decorators/impersonation.decorato import { RequiresScope } from '@ghostfolio/api/decorators/requires-scope.decorator'; import { hasNotDefinedValuesInObject, - nullifyValuesInObject + nullifyValuesInObject, + nullifyValuesInObjects } 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'; @@ -583,14 +584,22 @@ export class PortfolioController { this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') && this.request.user.subscription?.type === SubscriptionType.Basic ) { - performanceInformation.chart = performanceInformation.chart.map( - (item) => { - return nullifyValuesInObject(item, [ - 'dividendInBaseCurrency', - 'totalInvestment', - 'value' - ]); - } + performanceInformation.chart = nullifyValuesInObjects( + performanceInformation.chart, + [ + 'dividendInBaseCurrency', + 'totalInvestment', + 'value', + ...(groupBy === 'year' + ? [ + 'investmentValueWithCurrencyEffect', + 'netPerformance', + 'netPerformanceInPercentage', + 'netPerformanceInPercentageWithCurrencyEffect', + 'netPerformanceWithCurrencyEffect' + ] + : []) + ] ); performanceInformation.performance = nullifyValuesInObject( performanceInformation.performance, 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 460ac9de8e..26e52678c7 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 @@ -5,6 +5,7 @@ import { DEFAULT_DATE_RANGE, NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; +import { SubscriptionType } from '@ghostfolio/common/enums'; import { canOpenHoldingDetail } from '@ghostfolio/common/helper'; import { InvestmentItem, @@ -93,6 +94,7 @@ export class GfAnalysisPageComponent implements OnInit { protected isLoadingDividendTimelineChart: boolean; protected isLoadingInvestmentChart: boolean; protected isLoadingInvestmentTimelineChart: boolean; + protected isLoadingPerformanceTimelineChart: boolean; protected isLoadingPortfolioPrompt: boolean; protected readonly mode = signal('month'); protected readonly modeOptions: ToggleOption[] = [ @@ -103,6 +105,8 @@ export class GfAnalysisPageComponent implements OnInit { protected readonly PerformanceCalculationType = PerformanceCalculationType; protected performanceDataItems: LineChartItem[]; protected performanceDataItemsInPercentage: LineChartItem[]; + protected performancePercentagesByYear: InvestmentItem[]; + protected readonly performanceTimelineDataLabel = $localize`Net Performance`; protected readonly portfolioEvolutionDataLabel = $localize`Invested Capital`; protected precision = 2; protected savingsRatePerMonth: number | undefined; @@ -312,6 +316,37 @@ export class GfAnalysisPageComponent implements OnInit { ); } + private fetchPerformanceByYear() { + this.isLoadingPerformanceTimelineChart = true; + + this.dataService + .fetchPortfolioPerformance({ + filters: this.userService.getFilters(), + groupBy: 'year', + range: this.user?.settings?.dateRange ?? DEFAULT_DATE_RANGE + }) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ chart }) => { + this.performancePercentagesByYear = []; + + for (const { + date, + netPerformanceInPercentageWithCurrencyEffect + } of chart ?? []) { + if (isNumber(netPerformanceInPercentageWithCurrencyEffect)) { + this.performancePercentagesByYear.push({ + date, + investment: netPerformanceInPercentageWithCurrencyEffect + }); + } + } + + this.isLoadingPerformanceTimelineChart = false; + + this.changeDetectorRef.markForCheck(); + }); + } + private update() { this.isLoadingInvestmentChart = true; @@ -420,6 +455,13 @@ export class GfAnalysisPageComponent implements OnInit { this.fetchDividendsAndInvestments(); + if ( + this.user?.settings?.isExperimentalFeatures && + this.user?.subscription?.type !== SubscriptionType.Basic + ) { + this.fetchPerformanceByYear(); + } + this.changeDetectorRef.markForCheck(); } 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 c39b774f4b..6f05dae069 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.html +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.html @@ -430,6 +430,35 @@ + @if (user?.settings?.isExperimentalFeatures) { +
+
+
+
+ Performance Timeline + @if (user?.subscription?.type === 'Basic') { + + } +
+
+
+ +
+
+
+ } +
diff --git a/libs/ui/src/lib/services/data.service.ts b/libs/ui/src/lib/services/data.service.ts index f18b3eef13..d7cea71864 100644 --- a/libs/ui/src/lib/services/data.service.ts +++ b/libs/ui/src/lib/services/data.service.ts @@ -728,14 +728,21 @@ export class DataService { public fetchPortfolioPerformance({ filters, + groupBy, range, withExcludedAccounts = false }: { filters?: Filter[]; + groupBy?: Extract; range: DateRange; withExcludedAccounts?: boolean; }): Observable { let params = this.buildFiltersAsQueryParams({ filters }); + + if (groupBy) { + params = params.append('groupBy', groupBy); + } + params = params.append('range', range); if (withExcludedAccounts) {