From 2e9d40c201c4a5be4c7fcb4bc5ffcf00903d2d5a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 24 Feb 2024 19:58:13 +0100 Subject: [PATCH] Feature/switch to performance calculations with currency effects (#3039) * Switch to performance calculations with currency effects * Improve value redaction in portfolio details endpoint * Update changelog --- CHANGELOG.md | 1 + .../src/app/portfolio/portfolio.controller.ts | 18 +++++------- .../src/app/portfolio/portfolio.service.ts | 28 +++++++++++++++++++ .../redact-values-in-response.interceptor.ts | 2 ++ .../home-holdings/home-holdings.component.ts | 4 +-- .../home-overview/home-overview.component.ts | 4 +-- .../portfolio-performance.component.html | 10 +++++-- .../portfolio-performance.component.ts | 3 +- .../portfolio-summary.component.html | 28 +++++++++++++++---- .../position-detail-dialog.component.ts | 20 ++++++------- .../position-detail-dialog.html | 4 +-- .../position/position.component.html | 6 ++-- .../analysis/analysis-page.component.ts | 25 ++++++++++------- .../portfolio-position.interface.ts | 4 +++ .../interfaces/portfolio-summary.interface.ts | 1 + .../src/lib/interfaces/position.interface.ts | 2 ++ .../holdings-table.component.html | 8 ++++-- 17 files changed, 116 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fc948b02..e3f6ce8d6 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 +- Switched the performance calculations to take the currency effects into account - Removed the `isDefault` flag from the `Account` database schema - Exposed the database index of _Redis_ as an environment variable (`REDIS_DB`) - Improved the language localization for German (`de`) diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 31dd6cd73..6f21bb604 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -118,27 +118,23 @@ export class PortfolioController { this.userService.isRestrictedView(this.request.user) ) { const totalInvestment = Object.values(holdings) - .map((portfolioPosition) => { - return portfolioPosition.investment; + .map(({ investment }) => { + return investment; }) .reduce((a, b) => a + b, 0); const totalValue = Object.values(holdings) - .map((portfolioPosition) => { - return this.exchangeRateDataService.toCurrency( - portfolioPosition.quantity * portfolioPosition.marketPrice, - portfolioPosition.currency, - this.request.user.Settings.settings.baseCurrency - ); + .filter(({ assetClass, assetSubClass }) => { + return assetClass !== 'CASH' && assetSubClass !== 'CASH'; + }) + .map(({ valueInBaseCurrency }) => { + return valueInBaseCurrency; }) .reduce((a, b) => a + b, 0); for (const [symbol, portfolioPosition] of Object.entries(holdings)) { - portfolioPosition.grossPerformance = null; portfolioPosition.investment = portfolioPosition.investment / totalInvestment; - portfolioPosition.netPerformance = null; - portfolioPosition.quantity = null; portfolioPosition.valueInPercentage = portfolioPosition.valueInBaseCurrency / totalValue; } diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index 9c70c404b..3fafa6795 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -529,12 +529,20 @@ export class PortfolioService { grossPerformance: item.grossPerformance?.toNumber() ?? 0, grossPerformancePercent: item.grossPerformancePercentage?.toNumber() ?? 0, + grossPerformancePercentWithCurrencyEffect: + item.grossPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, + grossPerformanceWithCurrencyEffect: + item.grossPerformanceWithCurrencyEffect?.toNumber() ?? 0, investment: item.investment.toNumber(), marketPrice: item.marketPrice, marketState: dataProviderResponse?.marketState ?? 'delayed', name: symbolProfile.name, netPerformance: item.netPerformance?.toNumber() ?? 0, netPerformancePercent: item.netPerformancePercentage?.toNumber() ?? 0, + netPerformancePercentWithCurrencyEffect: + item.netPerformancePercentageWithCurrencyEffect?.toNumber() ?? 0, + netPerformanceWithCurrencyEffect: + item.netPerformanceWithCurrencyEffect?.toNumber() ?? 0, quantity: item.quantity.toNumber(), sectors: symbolProfile.sectors, symbol: item.symbol, @@ -1600,12 +1608,16 @@ export class PortfolioService { dateOfFirstActivity: undefined, grossPerformance: 0, grossPerformancePercent: 0, + grossPerformancePercentWithCurrencyEffect: 0, + grossPerformanceWithCurrencyEffect: 0, investment: balance, marketPrice: 0, marketState: 'open', name: currency, netPerformance: 0, netPerformancePercent: 0, + netPerformancePercentWithCurrencyEffect: 0, + netPerformanceWithCurrencyEffect: 0, quantity: 0, sectors: [], symbol: currency, @@ -1814,9 +1826,25 @@ export class PortfolioService { }) ?.toNumber(); + const annualizedPerformancePercentWithCurrencyEffect = + new PortfolioCalculator({ + currency: userCurrency, + currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, + orders: [] + }) + .getAnnualizedPerformancePercent({ + daysInMarket, + netPerformancePercent: new Big( + performanceInformation.performance.currentNetPerformancePercentWithCurrencyEffect + ) + }) + ?.toNumber(); + return { ...performanceInformation.performance, annualizedPerformancePercent, + annualizedPerformancePercentWithCurrencyEffect, cash, dividend, excludedAccountsAndActivities, diff --git a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts index c1df8157e..b1889cf9d 100644 --- a/apps/api/src/interceptors/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response.interceptor.ts @@ -51,8 +51,10 @@ export class RedactValuesInResponseInterceptor 'feeInBaseCurrency', 'filteredValueInBaseCurrency', 'grossPerformance', + 'grossPerformanceWithCurrencyEffect', 'investment', 'netPerformance', + 'netPerformanceWithCurrencyEffect', 'quantity', 'symbolMapping', 'totalBalanceInBaseCurrency', 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 aeb531379..9dbf9d9bf 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 @@ -154,8 +154,8 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { this.dataService .fetchPositions({ range: this.user?.settings?.dateRange }) .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((response) => { - this.positions = response.positions; + .subscribe(({ positions }) => { + this.positions = positions; this.changeDetectorRef.markForCheck(); }); diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index 75fd21eeb..7beb322a2 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -127,10 +127,10 @@ export class HomeOverviewComponent implements OnDestroy, OnInit { this.isLoadingPerformance = false; this.historicalDataItems = chart.map( - ({ date, netPerformanceInPercentage }) => { + ({ date, netPerformanceInPercentageWithCurrencyEffect }) => { return { date, - value: netPerformanceInPercentage + value: netPerformanceInPercentageWithCurrencyEffect }; } ); 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 fdc231579..68d191b5c 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 @@ -40,7 +40,11 @@ [colorizeSign]="true" [isCurrency]="true" [locale]="locale" - [value]="isLoading ? undefined : performance?.currentNetPerformance" + [value]=" + isLoading + ? undefined + : performance?.currentNetPerformanceWithCurrencyEffect + " />
@@ -49,7 +53,9 @@ [isPercent]="true" [locale]="locale" [value]=" - isLoading ? undefined : performance?.currentNetPerformancePercent + isLoading + ? undefined + : performance?.currentNetPerformancePercentWithCurrencyEffect " />
diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 720adb3f6..b76ecb004 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -63,7 +63,8 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit { } else if (this.showDetails === false) { new CountUp( 'value', - this.performance?.currentNetPerformancePercent * 100, + this.performance?.currentNetPerformancePercentWithCurrencyEffect * + 100, { decimal: getNumberFormatDecimal(this.locale), decimalPlaces: 2, diff --git a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html index 6adaca943..c4aea8891 100644 --- a/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html +++ b/apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html @@ -64,7 +64,11 @@ [isCurrency]="true" [locale]="locale" [unit]="baseCurrency" - [value]="isLoading ? undefined : summary?.currentGrossPerformance" + [value]=" + isLoading + ? undefined + : summary?.currentGrossPerformanceWithCurrencyEffect + " /> @@ -85,7 +89,9 @@ [isPercent]="true" [locale]="locale" [value]=" - isLoading ? undefined : summary?.currentGrossPerformancePercent + isLoading + ? undefined + : summary?.currentGrossPerformancePercentWithCurrencyEffect " /> @@ -114,7 +120,11 @@ [isCurrency]="true" [locale]="locale" [unit]="baseCurrency" - [value]="isLoading ? undefined : summary?.currentNetPerformance" + [value]=" + isLoading + ? undefined + : summary?.currentNetPerformanceWithCurrencyEffect + " /> @@ -134,7 +144,11 @@ [colorizeSign]="true" [isPercent]="true" [locale]="locale" - [value]="isLoading ? undefined : summary?.currentNetPerformancePercent" + [value]=" + isLoading + ? undefined + : summary?.currentNetPerformancePercentWithCurrencyEffect + " /> @@ -283,7 +297,11 @@ [colorizeSign]="true" [isPercent]="true" [locale]="locale" - [value]="isLoading ? undefined : summary?.annualizedPerformancePercent" + [value]=" + isLoading + ? undefined + : summary?.annualizedPerformancePercentWithCurrencyEffect + " /> diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index f44d41e82..2e3f8f1d3 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -50,15 +50,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit { public dividendInBaseCurrency: number; public feeInBaseCurrency: number; public firstBuyDate: string; - public grossPerformance: number; - public grossPerformancePercent: number; public historicalDataItems: LineChartItem[]; public investment: number; public marketPrice: number; public maxPrice: number; public minPrice: number; - public netPerformance: number; - public netPerformancePercent: number; + public netPerformancePercentWithCurrencyEffect: number; + public netPerformanceWithCurrencyEffect: number; public quantity: number; public quantityPrecision = 2; public reportDataGlitchMail: string; @@ -99,15 +97,13 @@ export class PositionDetailDialog implements OnDestroy, OnInit { dividendInBaseCurrency, feeInBaseCurrency, firstBuyDate, - grossPerformance, - grossPerformancePercent, historicalData, investment, marketPrice, maxPrice, minPrice, - netPerformance, - netPerformancePercent, + netPerformancePercentWithCurrencyEffect, + netPerformanceWithCurrencyEffect, orders, quantity, SymbolProfile, @@ -125,8 +121,6 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.dividendInBaseCurrency = dividendInBaseCurrency; this.feeInBaseCurrency = feeInBaseCurrency; this.firstBuyDate = firstBuyDate; - this.grossPerformance = grossPerformance; - this.grossPerformancePercent = grossPerformancePercent; this.historicalDataItems = historicalData.map( (historicalDataItem) => { this.benchmarkDataItems.push({ @@ -144,8 +138,10 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.marketPrice = marketPrice; this.maxPrice = maxPrice; this.minPrice = minPrice; - this.netPerformance = netPerformance; - this.netPerformancePercent = netPerformancePercent; + this.netPerformancePercentWithCurrencyEffect = + netPerformancePercentWithCurrencyEffect; + this.netPerformanceWithCurrencyEffect = + netPerformanceWithCurrencyEffect; this.quantity = quantity; this.reportDataGlitchMail = `mailto:hi@ghostfol.io?Subject=Ghostfolio Data Glitch Report&body=Hello%0D%0DI would like to report a data glitch for%0D%0DSymbol: ${SymbolProfile?.symbol}%0DData Source: ${SymbolProfile?.dataSource}%0D%0DAdditional notes:%0D%0DCan you please take a look?%0D%0DKind regards`; this.sectors = {}; diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 3c6c12d80..f87d11e04 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -44,7 +44,7 @@ [isCurrency]="true" [locale]="data.locale" [unit]="data.baseCurrency" - [value]="netPerformance" + [value]="netPerformanceWithCurrencyEffect" >Change @@ -55,7 +55,7 @@ [colorizeSign]="true" [isPercent]="true" [locale]="data.locale" - [value]="netPerformancePercent" + [value]="netPerformancePercentWithCurrencyEffect" >Performance diff --git a/apps/client/src/app/components/position/position.component.html b/apps/client/src/app/components/position/position.component.html index 007ccc70f..4a5ed6f9a 100644 --- a/apps/client/src/app/components/position/position.component.html +++ b/apps/client/src/app/components/position/position.component.html @@ -17,7 +17,7 @@ [isLoading]="isLoading" [marketState]="position?.marketState" [range]="range" - [value]="position?.netPerformancePercentage" + [value]="position?.netPerformancePercentageWithCurrencyEffect" />
@@ -49,13 +49,13 @@ [isCurrency]="true" [locale]="locale" [unit]="baseCurrency" - [value]="position?.netPerformance" + [value]="position?.netPerformanceWithCurrencyEffect" />
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 d2f8487ad..96c7c01dc 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 @@ -270,23 +270,28 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { index, { date, - netPerformanceInPercentage, - totalInvestment, - value, - valueInPercentage + netPerformanceInPercentageWithCurrencyEffect, + totalInvestmentValueWithCurrencyEffect, + valueInPercentage, + valueWithCurrencyEffect } ] of chart.entries()) { if (index > 0 || this.user?.settings?.dateRange === 'max') { // Ignore first item where value is 0 - this.investments.push({ date, investment: totalInvestment }); + this.investments.push({ + date, + investment: totalInvestmentValueWithCurrencyEffect + }); this.performanceDataItems.push({ date, - value: isNumber(value) ? value : valueInPercentage + value: isNumber(valueWithCurrencyEffect) + ? valueWithCurrencyEffect + : valueInPercentage }); } this.performanceDataItemsInPercentage.push({ date, - value: netPerformanceInPercentage + value: netPerformanceInPercentageWithCurrencyEffect }); } @@ -305,10 +310,10 @@ export class AnalysisPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ positions }) => { const positionsSorted = sortBy( - positions.filter(({ netPerformancePercentage }) => { - return isNumber(netPerformancePercentage); + positions.filter(({ netPerformancePercentageWithCurrencyEffect }) => { + return isNumber(netPerformancePercentageWithCurrencyEffect); }), - 'netPerformancePercentage' + 'netPerformancePercentageWithCurrencyEffect' ).reverse(); this.top3 = positionsSorted.slice(0, 3); diff --git a/libs/common/src/lib/interfaces/portfolio-position.interface.ts b/libs/common/src/lib/interfaces/portfolio-position.interface.ts index 0eebb2975..87db0117d 100644 --- a/libs/common/src/lib/interfaces/portfolio-position.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-position.interface.ts @@ -17,6 +17,8 @@ export interface PortfolioPosition { exchange?: string; grossPerformance: number; grossPerformancePercent: number; + grossPerformancePercentWithCurrencyEffect: number; + grossPerformanceWithCurrencyEffect: number; investment: number; marketChange?: number; marketChangePercent?: number; @@ -27,6 +29,8 @@ export interface PortfolioPosition { name: string; netPerformance: number; netPerformancePercent: number; + netPerformancePercentWithCurrencyEffect: number; + netPerformanceWithCurrencyEffect: number; quantity: number; sectors: Sector[]; symbol: string; diff --git a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts index 4ebe9afa4..f0bb4c3b1 100644 --- a/libs/common/src/lib/interfaces/portfolio-summary.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-summary.interface.ts @@ -2,6 +2,7 @@ import { PortfolioPerformance } from './portfolio-performance.interface'; export interface PortfolioSummary extends PortfolioPerformance { annualizedPerformancePercent: number; + annualizedPerformancePercentWithCurrencyEffect: number; cash: number; committedFunds: number; dividend: number; diff --git a/libs/common/src/lib/interfaces/position.interface.ts b/libs/common/src/lib/interfaces/position.interface.ts index 2a7c4a3c0..d1f74380b 100644 --- a/libs/common/src/lib/interfaces/position.interface.ts +++ b/libs/common/src/lib/interfaces/position.interface.ts @@ -18,6 +18,8 @@ export interface Position { name?: string; netPerformance?: number; netPerformancePercentage?: number; + netPerformancePercentageWithCurrencyEffect?: number; + netPerformanceWithCurrencyEffect?: number; quantity: number; symbol: string; transactionCount: number; 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 164c5e74b..17e7c9f36 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.html +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.html @@ -114,7 +114,7 @@ *matHeaderCellDef class="justify-content-end px-1" mat-header-cell - mat-sort-header="netPerformancePercent" + mat-sort-header="netPerformancePercentWithCurrencyEffect" > Performance ± @@ -125,7 +125,11 @@ [colorizeSign]="true" [isPercent]="true" [locale]="locale" - [value]="isLoading ? undefined : element.netPerformancePercent" + [value]=" + isLoading + ? undefined + : element.netPerformancePercentWithCurrencyEffect + " />