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 6cc5edeaf..8ade63a33 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.factory.ts @@ -5,20 +5,16 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { Filter, HistoricalDataItem } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Injectable } from '@nestjs/common'; import { MwrPortfolioCalculator } from './mwr/portfolio-calculator'; import { PortfolioCalculator } from './portfolio-calculator'; import { RoaiPortfolioCalculator } from './roai/portfolio-calculator'; +import { RoiPortfolioCalculator } from './roi/portfolio-calculator'; import { TwrPortfolioCalculator } from './twr/portfolio-calculator'; -export enum PerformanceCalculationType { - MWR = 'MWR', // Money-Weighted Rate of Return - ROAI = 'ROAI', // Return on Average Investment - TWR = 'TWR' // Time-Weighted Rate of Return -} - @Injectable() export class PortfolioCalculatorFactory { public constructor( @@ -49,6 +45,7 @@ export class PortfolioCalculatorFactory { return new MwrPortfolioCalculator({ accountBalanceItems, activities, + calculationType, currency, filters, userId, @@ -58,10 +55,12 @@ export class PortfolioCalculatorFactory { portfolioSnapshotService: this.portfolioSnapshotService, redisCacheService: this.redisCacheService }); + case PerformanceCalculationType.ROAI: return new RoaiPortfolioCalculator({ accountBalanceItems, activities, + calculationType, currency, filters, userId, @@ -71,10 +70,27 @@ export class PortfolioCalculatorFactory { portfolioSnapshotService: this.portfolioSnapshotService, redisCacheService: this.redisCacheService }); + + case PerformanceCalculationType.ROI: + return new RoiPortfolioCalculator({ + accountBalanceItems, + activities, + calculationType, + currency, + filters, + userId, + configurationService: this.configurationService, + currentRateService: this.currentRateService, + exchangeRateDataService: this.exchangeRateDataService, + portfolioSnapshotService: this.portfolioSnapshotService, + redisCacheService: this.redisCacheService + }); + case PerformanceCalculationType.TWR: return new TwrPortfolioCalculator({ accountBalanceItems, activities, + calculationType, currency, filters, userId, @@ -84,6 +100,7 @@ export class PortfolioCalculatorFactory { portfolioSnapshotService: this.portfolioSnapshotService, redisCacheService: this.redisCacheService }); + default: throw new Error('Invalid calculation type'); } diff --git a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts index 9698cb315..b746f9f58 100644 --- a/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts +++ b/apps/api/src/app/portfolio/calculator/portfolio-calculator.ts @@ -35,6 +35,7 @@ import { } from '@ghostfolio/common/interfaces'; import { PortfolioSnapshot, TimelinePosition } from '@ghostfolio/common/models'; import { GroupBy } from '@ghostfolio/common/types'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Logger } from '@nestjs/common'; import { Big } from 'big.js'; @@ -57,6 +58,7 @@ export abstract class PortfolioCalculator { protected accountBalanceItems: HistoricalDataItem[]; protected activities: PortfolioOrder[]; + private calculationType: PerformanceCalculationType; private configurationService: ConfigurationService; private currency: string; private currentRateService: CurrentRateService; @@ -75,6 +77,7 @@ export abstract class PortfolioCalculator { public constructor({ accountBalanceItems, activities, + calculationType, configurationService, currency, currentRateService, @@ -86,6 +89,7 @@ export abstract class PortfolioCalculator { }: { accountBalanceItems: HistoricalDataItem[]; activities: Activity[]; + calculationType: PerformanceCalculationType; configurationService: ConfigurationService; currency: string; currentRateService: CurrentRateService; @@ -96,6 +100,7 @@ export abstract class PortfolioCalculator { userId: string; }) { this.accountBalanceItems = accountBalanceItems; + this.calculationType = calculationType; this.configurationService = configurationService; this.currency = currency; this.currentRateService = currentRateService; @@ -1073,6 +1078,7 @@ export abstract class PortfolioCalculator { // Compute in the background this.portfolioSnapshotService.addJobToQueue({ data: { + calculationType: this.calculationType, filters: this.filters, userCurrency: this.currency, userId: this.userId @@ -1089,6 +1095,7 @@ export abstract class PortfolioCalculator { // Wait for computation await this.portfolioSnapshotService.addJobToQueue({ data: { + calculationType: this.calculationType, filters: this.filters, userCurrency: this.currency, userId: this.userId diff --git a/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts new file mode 100644 index 000000000..16a390584 --- /dev/null +++ b/apps/api/src/app/portfolio/calculator/roi/portfolio-calculator.ts @@ -0,0 +1,24 @@ +import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; +import { + AssetProfileIdentifier, + SymbolMetrics +} from '@ghostfolio/common/interfaces'; +import { PortfolioSnapshot } from '@ghostfolio/common/models'; + +export class RoiPortfolioCalculator extends PortfolioCalculator { + protected calculateOverallPerformance(): PortfolioSnapshot { + throw new Error('Method not implemented.'); + } + + protected getSymbolMetrics({}: { + end: Date; + exchangeRates: { [dateString: string]: number }; + marketSymbolMap: { + [date: string]: { [symbol: string]: Big }; + }; + start: Date; + step?: number; + } & AssetProfileIdentifier): SymbolMetrics { + throw new Error('Method not implemented.'); + } +} diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index d3bbc1e06..72d6db498 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -50,13 +50,14 @@ import { UserSettings } from '@ghostfolio/common/interfaces'; import { TimelinePosition } from '@ghostfolio/common/models'; -import type { +import { AccountWithValue, DateRange, GroupBy, RequestWithUser, UserWithSettings } from '@ghostfolio/common/types'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Inject, Injectable } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; @@ -85,10 +86,7 @@ import { import { isEmpty } from 'lodash'; import { PortfolioCalculator } from './calculator/portfolio-calculator'; -import { - PerformanceCalculationType, - PortfolioCalculatorFactory -} from './calculator/portfolio-calculator.factory'; +import { PortfolioCalculatorFactory } from './calculator/portfolio-calculator.factory'; import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface'; import { RulesService } from './rules.service'; @@ -278,6 +276,8 @@ export class PortfolioService { savingsRate: number; }): Promise { const userId = await this.getUserId(impersonationId, this.request.user.id); + const user = await this.userService.user({ id: userId }); + const userCurrency = this.getUserCurrency(user); const { endDate, startDate } = getIntervalFromDateRange(dateRange); @@ -285,7 +285,7 @@ export class PortfolioService { await this.orderService.getOrdersForPortfolioCalculator({ filters, userId, - userCurrency: this.getUserCurrency() + userCurrency }); if (activities.length === 0) { @@ -299,8 +299,8 @@ export class PortfolioService { activities, filters, userId, - calculationType: PerformanceCalculationType.ROAI, - currency: this.request.user.Settings.settings.baseCurrency + calculationType: this.getUserPerformanceCalculationType(user), + currency: userCurrency }); const { historicalData } = await portfolioCalculator.getSnapshot(); @@ -376,7 +376,7 @@ export class PortfolioService { activities, filters, userId, - calculationType: PerformanceCalculationType.ROAI, + calculationType: this.getUserPerformanceCalculationType(user), currency: userCurrency }); @@ -684,7 +684,7 @@ export class PortfolioService { const portfolioCalculator = this.calculatorFactory.createCalculator({ activities, userId, - calculationType: PerformanceCalculationType.ROAI, + calculationType: this.getUserPerformanceCalculationType(user), currency: userCurrency }); @@ -935,12 +935,13 @@ export class PortfolioService { })?.id; const userId = await this.getUserId(impersonationId, this.request.user.id); const user = await this.userService.user({ id: userId }); + const userCurrency = this.getUserCurrency(user); const { activities } = await this.orderService.getOrdersForPortfolioCalculator({ filters, userId, - userCurrency: this.getUserCurrency() + userCurrency }); if (activities.length === 0) { @@ -954,8 +955,8 @@ export class PortfolioService { activities, filters, userId, - calculationType: PerformanceCalculationType.ROAI, - currency: this.request.user.Settings.settings.baseCurrency + calculationType: this.getUserPerformanceCalculationType(user), + currency: userCurrency }); const portfolioSnapshot = await portfolioCalculator.getSnapshot(); @@ -1120,7 +1121,7 @@ export class PortfolioService { activities, filters, userId, - calculationType: PerformanceCalculationType.ROAI, + calculationType: this.getUserPerformanceCalculationType(user), currency: userCurrency }); @@ -2021,6 +2022,15 @@ export class PortfolioService { return impersonationUserId || aUserId; } + private getUserPerformanceCalculationType( + aUser: UserWithSettings + ): PerformanceCalculationType { + return ( + aUser?.Settings?.settings.performanceCalculationType ?? + PerformanceCalculationType.ROI + ); + } + private async getValueOfAccountsAndPlatforms({ activities, filters = [], diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index ebdf09ba5..b6b31dada 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -41,6 +41,7 @@ import { permissions } from '@ghostfolio/common/permissions'; import { UserWithSettings } from '@ghostfolio/common/types'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { Injectable } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; @@ -246,6 +247,12 @@ export class UserService { ? 'max' : ((user.Settings.settings as UserSettings)?.dateRange ?? 'max'); + // Set default value for performance calculation type + if (!(user.Settings.settings as UserSettings)?.performanceCalculationType) { + (user.Settings.settings as UserSettings).performanceCalculationType = + PerformanceCalculationType.ROAI; + } + // Set default value for view mode if (!(user.Settings.settings as UserSettings).viewMode) { (user.Settings.settings as UserSettings).viewMode = 'DEFAULT'; diff --git a/apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts b/apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts index 24948e211..b9f315c5d 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/interfaces/portfolio-snapshot-queue-job.interface.ts @@ -1,6 +1,8 @@ import { Filter } from '@ghostfolio/common/interfaces'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; export interface IPortfolioSnapshotQueueJob { + calculationType: PerformanceCalculationType; filters: Filter[]; userCurrency: string; userId: string; diff --git a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts index 60c3cf695..6a2a3114e 100644 --- a/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts +++ b/apps/api/src/services/queues/portfolio-snapshot/portfolio-snapshot.processor.ts @@ -1,9 +1,6 @@ import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service'; -import { - PerformanceCalculationType, - PortfolioCalculatorFactory -} from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; +import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioSnapshotValue } from '@ghostfolio/api/app/portfolio/interfaces/snapshot-value.interface'; import { RedisCacheService } from '@ghostfolio/api/app/redis-cache/redis-cache.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; @@ -68,7 +65,7 @@ export class PortfolioSnapshotProcessor { const portfolioCalculator = this.calculatorFactory.createCalculator({ accountBalanceItems, activities, - calculationType: PerformanceCalculationType.ROAI, + calculationType: job.data.calculationType, currency: job.data.userCurrency, filters: job.data.filters, userId: job.data.userId diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index 8bd2efd1f..c4655a4ce 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -2,25 +2,7 @@

Settings

-
-
-
Presenter View
-
- Protection for sensitive information like absolute performances and - quantity values -
-
-
- -
-
-
+
@@ -43,6 +25,32 @@
+ @if (user?.settings?.isExperimentalFeatures && !!user?.subscription) { +
+
+ Performance Calculation +
+
+ + + Return on Average Investment (ROAI) + + +
+
+ }
Language
@@ -172,6 +180,24 @@
+
+
+
Presenter View
+
+ Protection for sensitive information like absolute performances and + quantity values +
+
+
+ +
+
Zen Mode
diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts index d72be7c7c..942f6e616 100644 --- a/libs/common/src/lib/interfaces/user-settings.interface.ts +++ b/libs/common/src/lib/interfaces/user-settings.interface.ts @@ -5,6 +5,7 @@ import { HoldingsViewMode, ViewMode } from '@ghostfolio/common/types'; +import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; export interface UserSettings { annualInterestRate?: number; @@ -22,6 +23,7 @@ export interface UserSettings { isRestrictedView?: boolean; language?: string; locale?: string; + performanceCalculationType?: PerformanceCalculationType; projectedTotalAmount?: number; retirementDate?: string; savingsRate?: number; diff --git a/libs/common/src/lib/types/performance-calculation-type.type.ts b/libs/common/src/lib/types/performance-calculation-type.type.ts new file mode 100644 index 000000000..a970636b6 --- /dev/null +++ b/libs/common/src/lib/types/performance-calculation-type.type.ts @@ -0,0 +1,6 @@ +export enum PerformanceCalculationType { + MWR = 'MWR', // Money-Weighted Rate of Return + ROAI = 'ROAI', // Return on Average Investment + ROI = 'ROI', // Return on Investment + TWR = 'TWR' // Time-Weighted Rate of Return +}