From 6cc6075838e865a9e0ae9bf994e0941230eaeec2 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Fri, 4 Oct 2024 20:30:45 +0200 Subject: [PATCH] Modernize rules implementation --- .../src/app/portfolio/portfolio.service.ts | 103 +++++++----------- apps/api/src/models/rule.ts | 19 ++-- .../base-currency-current-investment.ts | 19 ++-- .../current-investment.ts | 17 ++- .../rule-settings-dialog.component.ts | 2 - 5 files changed, 67 insertions(+), 93 deletions(-) diff --git a/apps/api/src/app/portfolio/portfolio.service.ts b/apps/api/src/app/portfolio/portfolio.service.ts index fc06545a5..7aae6f8d9 100644 --- a/apps/api/src/app/portfolio/portfolio.service.ts +++ b/apps/api/src/app/portfolio/portfolio.service.ts @@ -1148,74 +1148,49 @@ export class PortfolioService { public async getReport(impersonationId: string): 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 { activities } = - await this.orderService.getOrdersForPortfolioCalculator({ - userCurrency, - userId - }); + const userSettings = this.request.user.Settings.settings; - const portfolioCalculator = this.calculatorFactory.createCalculator({ - activities, + const { accounts, holdings, summary } = await this.getDetails({ + impersonationId, userId, - calculationType: PerformanceCalculationType.TWR, - currency: this.request.user.Settings.settings.baseCurrency + withMarkets: true, + withSummary: true }); - let { totalFeesWithCurrencyEffect, positions, totalInvestment } = - await portfolioCalculator.getSnapshot(); - - positions = positions.filter((item) => !item.quantity.eq(0)); - - const portfolioItemsNow: { [symbol: string]: TimelinePosition } = {}; - - for (const position of positions) { - portfolioItemsNow[position.symbol] = position; - } - - const { accounts } = await this.getValueOfAccountsAndPlatforms({ - activities, - portfolioItemsNow, - userCurrency, - userId - }); - - const userSettings = this.request.user.Settings.settings; - return { rules: { - accountClusterRisk: isEmpty(activities) - ? undefined - : await this.rulesService.evaluate( - [ - new AccountClusterRiskCurrentInvestment( - this.exchangeRateDataService, - accounts - ), - new AccountClusterRiskSingleAccount( - this.exchangeRateDataService, - accounts - ) - ], - userSettings - ), - currencyClusterRisk: isEmpty(activities) - ? undefined - : await this.rulesService.evaluate( - [ - new CurrencyClusterRiskBaseCurrencyCurrentInvestment( - this.exchangeRateDataService, - positions - ), - new CurrencyClusterRiskCurrentInvestment( - this.exchangeRateDataService, - positions - ) - ], - userSettings - ), + accountClusterRisk: + summary.ordersCount > 0 + ? await this.rulesService.evaluate( + [ + new AccountClusterRiskCurrentInvestment( + this.exchangeRateDataService, + accounts + ), + new AccountClusterRiskSingleAccount( + this.exchangeRateDataService, + accounts + ) + ], + userSettings + ) + : undefined, + currencyClusterRisk: + summary.ordersCount > 0 + ? await this.rulesService.evaluate( + [ + new CurrencyClusterRiskBaseCurrencyCurrentInvestment( + this.exchangeRateDataService, + Object.values(holdings) + ), + new CurrencyClusterRiskCurrentInvestment( + this.exchangeRateDataService, + Object.values(holdings) + ) + ], + userSettings + ) + : undefined, emergencyFund: await this.rulesService.evaluate( [ new EmergencyFundSetup( @@ -1229,8 +1204,8 @@ export class PortfolioService { [ new FeeRatioInitialInvestment( this.exchangeRateDataService, - totalInvestment.toNumber(), - totalFeesWithCurrencyEffect.toNumber() + summary.committedFunds, + summary.fees ) ], userSettings diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index 8397f3e46..a1e0d9bee 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,8 +1,9 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { groupBy } from '@ghostfolio/common/helper'; -import { UserSettings } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; + +import { Big } from 'big.js'; import { EvaluationResult } from './interfaces/evaluation-result.interface'; import { RuleInterface } from './interfaces/rule.interface'; @@ -33,24 +34,26 @@ export abstract class Rule implements RuleInterface { return this.name; } - public groupCurrentPositionsByAttribute( - positions: TimelinePosition[], - attribute: keyof TimelinePosition, + public groupCurrentHoldingsByAttribute( + holdings: PortfolioPosition[], + attribute: keyof PortfolioPosition, baseCurrency: string ) { - return Array.from(groupBy(attribute, positions).entries()).map( + return Array.from(groupBy(attribute, holdings).entries()).map( ([attributeValue, objs]) => ({ groupKey: attributeValue, investment: objs.reduce( (previousValue, currentValue) => - previousValue + currentValue.investment.toNumber(), + previousValue + currentValue.investment, 0 ), value: objs.reduce( (previousValue, currentValue) => previousValue + this.exchangeRateDataService.toCurrency( - currentValue.quantity.mul(currentValue.marketPrice).toNumber(), + new Big(currentValue.quantity) + .mul(currentValue.marketPrice) + .toNumber(), currentValue.currency, baseCurrency ), diff --git a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts index e3050efcc..39ee8b88d 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/base-currency-current-investment.ts @@ -1,35 +1,34 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { UserSettings } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { - private positions: TimelinePosition[]; + private holdings: PortfolioPosition[]; public constructor( protected exchangeRateDataService: ExchangeRateDataService, - positions: TimelinePosition[] + holdings: PortfolioPosition[] ) { super(exchangeRateDataService, { key: CurrencyClusterRiskBaseCurrencyCurrentInvestment.name, name: 'Investment: Base Currency' }); - this.positions = positions; + this.holdings = holdings; } public evaluate(ruleSettings: Settings) { - const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute( - this.positions, + const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute( + this.holdings, 'currency', ruleSettings.baseCurrency ); - let maxItem = positionsGroupedByCurrency[0]; + let maxItem = holdingsGroupedByCurrency[0]; let totalValue = 0; - positionsGroupedByCurrency.forEach((groupItem) => { + holdingsGroupedByCurrency.forEach((groupItem) => { // Calculate total value totalValue += groupItem.value; @@ -39,7 +38,7 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { + const baseCurrencyItem = holdingsGroupedByCurrency.find((item) => { return item.groupKey === ruleSettings.baseCurrency; }); diff --git a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts index fadf47ba5..bdb36c78a 100644 --- a/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/currency-cluster-risk/current-investment.ts @@ -1,35 +1,34 @@ import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { Rule } from '@ghostfolio/api/models/rule'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; -import { UserSettings } from '@ghostfolio/common/interfaces'; -import { TimelinePosition } from '@ghostfolio/common/models'; +import { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; export class CurrencyClusterRiskCurrentInvestment extends Rule { - private positions: TimelinePosition[]; + private holdings: PortfolioPosition[]; public constructor( protected exchangeRateDataService: ExchangeRateDataService, - positions: TimelinePosition[] + holdings: PortfolioPosition[] ) { super(exchangeRateDataService, { key: CurrencyClusterRiskCurrentInvestment.name, name: 'Investment' }); - this.positions = positions; + this.holdings = holdings; } public evaluate(ruleSettings: Settings) { - const positionsGroupedByCurrency = this.groupCurrentPositionsByAttribute( - this.positions, + const holdingsGroupedByCurrency = this.groupCurrentHoldingsByAttribute( + this.holdings, 'currency', ruleSettings.baseCurrency ); - let maxItem = positionsGroupedByCurrency[0]; + let maxItem = holdingsGroupedByCurrency[0]; let totalValue = 0; - positionsGroupedByCurrency.forEach((groupItem) => { + holdingsGroupedByCurrency.forEach((groupItem) => { // Calculate total value totalValue += groupItem.value; diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts index 4fb68e780..265d3c941 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts @@ -35,8 +35,6 @@ export class GfRuleSettingsDialogComponent { @Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, public dialogRef: MatDialogRef ) { - console.log(this.data.rule); - this.settings = this.data.rule.settings; } }