From f8da265f5f4d8d34a3e11232a31b6c5d2ccad44a Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 12 Oct 2024 21:04:09 +0200 Subject: [PATCH] Feature/restructure XRayRulesSettings (#3898) * Restructure XRayRulesSettings * Update changelog --- CHANGELOG.md | 1 + apps/api/src/app/portfolio/rules.service.ts | 7 +-- apps/api/src/app/user/user.service.ts | 46 ++++++++++++++----- apps/api/src/models/rule.ts | 10 +++- .../current-investment.ts | 15 +++++- .../account-cluster-risk/single-account.ts | 6 ++- .../base-currency-current-investment.ts | 6 ++- .../current-investment.ts | 15 +++++- .../emergency-fund/emergency-fund-setup.ts | 6 ++- .../fees/fee-ratio-initial-investment.ts | 15 +++++- .../interfaces/interfaces.ts | 2 + .../rule-settings-dialog.component.ts | 8 ++-- .../rule-settings-dialog.html | 14 ++++-- .../app/components/rule/rule.component.html | 2 +- .../src/app/components/rule/rule.component.ts | 15 +++--- .../app/components/rules/rules.component.html | 1 + .../app/components/rules/rules.component.ts | 4 +- .../portfolio/fire/fire-page.component.ts | 5 ++ .../app/pages/portfolio/fire/fire-page.html | 5 ++ .../portfolio-report-rule.interface.ts | 13 ++++-- .../lib/types/x-ray-rules-settings.type.ts | 6 +-- 21 files changed, 149 insertions(+), 53 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72c166601..6e1b35474 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the labels of the chart of the holdings tab on the home page (experimental) +- Refactored the rule thresholds in the _X-ray_ section (experimental) - Exposed the timeout of the portfolio snapshot computation as an environment variable (`PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT`) - Harmonized the processor concurrency environment variables - Improved the portfolio unit tests to work with exported activity files diff --git a/apps/api/src/app/portfolio/rules.service.ts b/apps/api/src/app/portfolio/rules.service.ts index fd9d794b2..5f0aa64d5 100644 --- a/apps/api/src/app/portfolio/rules.service.ts +++ b/apps/api/src/app/portfolio/rules.service.ts @@ -24,13 +24,10 @@ export class RulesService { return { evaluation, value, + configuration: rule.getConfiguration(), isActive: true, key: rule.getKey(), - name: rule.getName(), - settings: { - thresholdMax: settings['thresholdMax'], - thresholdMin: settings['thresholdMin'] - } + name: rule.getName() }; } else { return { diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 0f76b9540..e8a437be6 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -2,6 +2,12 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { SubscriptionService } from '@ghostfolio/api/app/subscription/subscription.service'; import { environment } from '@ghostfolio/api/environments/environment'; import { PortfolioChangedEvent } from '@ghostfolio/api/events/portfolio-changed.event'; +import { AccountClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/account-cluster-risk/current-investment'; +import { AccountClusterRiskSingleAccount } from '@ghostfolio/api/models/rules/account-cluster-risk/single-account'; +import { CurrencyClusterRiskBaseCurrencyCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/base-currency-current-investment'; +import { CurrencyClusterRiskCurrentInvestment } from '@ghostfolio/api/models/rules/currency-cluster-risk/current-investment'; +import { EmergencyFundSetup } from '@ghostfolio/api/models/rules/emergency-fund/emergency-fund-setup'; +import { FeeRatioInitialInvestment } from '@ghostfolio/api/models/rules/fees/fee-ratio-initial-investment'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { I18nService } from '@ghostfolio/api/services/i18n/i18n.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; @@ -200,17 +206,35 @@ export class UserService { (user.Settings.settings as UserSettings).viewMode = 'DEFAULT'; } - // Set default values for X-ray rules - if (!(user.Settings.settings as UserSettings).xRayRules) { - (user.Settings.settings as UserSettings).xRayRules = { - AccountClusterRiskCurrentInvestment: { isActive: true }, - AccountClusterRiskSingleAccount: { isActive: true }, - CurrencyClusterRiskBaseCurrencyCurrentInvestment: { isActive: true }, - CurrencyClusterRiskCurrentInvestment: { isActive: true }, - EmergencyFundSetup: { isActive: true }, - FeeRatioInitialInvestment: { isActive: true } - }; - } + (user.Settings.settings as UserSettings).xRayRules = { + AccountClusterRiskCurrentInvestment: + new AccountClusterRiskCurrentInvestment(undefined, {}).getSettings( + user.Settings.settings + ), + AccountClusterRiskSingleAccount: new AccountClusterRiskSingleAccount( + undefined, + {} + ).getSettings(user.Settings.settings), + CurrencyClusterRiskBaseCurrencyCurrentInvestment: + new CurrencyClusterRiskBaseCurrencyCurrentInvestment( + undefined, + undefined + ).getSettings(user.Settings.settings), + CurrencyClusterRiskCurrentInvestment: + new CurrencyClusterRiskCurrentInvestment( + undefined, + undefined + ).getSettings(user.Settings.settings), + EmergencyFundSetup: new EmergencyFundSetup( + undefined, + undefined + ).getSettings(user.Settings.settings), + FeeRatioInitialInvestment: new FeeRatioInitialInvestment( + undefined, + undefined, + undefined + ).getSettings(user.Settings.settings) + }; let currentPermissions = getPermissions(user.role); diff --git a/apps/api/src/models/rule.ts b/apps/api/src/models/rule.ts index a1e0d9bee..187527fbb 100644 --- a/apps/api/src/models/rule.ts +++ b/apps/api/src/models/rule.ts @@ -1,7 +1,11 @@ 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 { PortfolioPosition, UserSettings } from '@ghostfolio/common/interfaces'; +import { + PortfolioPosition, + PortfolioReportRule, + UserSettings +} from '@ghostfolio/common/interfaces'; import { Big } from 'big.js'; @@ -65,5 +69,9 @@ export abstract class Rule implements RuleInterface { public abstract evaluate(aRuleSettings: T): EvaluationResult; + public abstract getConfiguration(): Partial< + PortfolioReportRule['configuration'] + >; + public abstract getSettings(aUserSettings: UserSettings): T; } diff --git a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts index 13680270e..95a8022ed 100644 --- a/apps/api/src/models/rules/account-cluster-risk/current-investment.ts +++ b/apps/api/src/models/rules/account-cluster-risk/current-investment.ts @@ -76,11 +76,22 @@ export class AccountClusterRiskCurrentInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01 + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 }; } } diff --git a/apps/api/src/models/rules/account-cluster-risk/single-account.ts b/apps/api/src/models/rules/account-cluster-risk/single-account.ts index feaaf4e38..ef549e579 100644 --- a/apps/api/src/models/rules/account-cluster-risk/single-account.ts +++ b/apps/api/src/models/rules/account-cluster-risk/single-account.ts @@ -34,9 +34,13 @@ export class AccountClusterRiskSingleAccount extends Rule { }; } + public getConfiguration() { + return undefined; + } + public getSettings({ xRayRules }: UserSettings): RuleSettings { return { - isActive: xRayRules[this.getKey()].isActive + isActive: xRayRules?.[this.getKey()].isActive ?? true }; } } 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 39ee8b88d..573795799 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 @@ -61,10 +61,14 @@ export class CurrencyClusterRiskBaseCurrencyCurrentInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 1, + min: 0, + step: 0.01 + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.5 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.5 }; } } diff --git a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts index 819b8bd7b..d13f2ffc5 100644 --- a/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts +++ b/apps/api/src/models/rules/emergency-fund/emergency-fund-setup.ts @@ -32,10 +32,14 @@ export class EmergencyFundSetup extends Rule { }; } + public getConfiguration() { + return undefined; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive + isActive: xRayRules?.[this.getKey()].isActive ?? true }; } } diff --git a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts index 9b1961ed6..a3ea8d059 100644 --- a/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts +++ b/apps/api/src/models/rules/fees/fee-ratio-initial-investment.ts @@ -43,11 +43,22 @@ export class FeeRatioInitialInvestment extends Rule { }; } + public getConfiguration() { + return { + threshold: { + max: 0.1, + min: 0, + step: 0.005 + }, + thresholdMax: true + }; + } + public getSettings({ baseCurrency, xRayRules }: UserSettings): Settings { return { baseCurrency, - isActive: xRayRules[this.getKey()].isActive, - thresholdMax: xRayRules[this.getKey()]?.thresholdMax ?? 0.01 + isActive: xRayRules?.[this.getKey()].isActive ?? true, + thresholdMax: xRayRules?.[this.getKey()]?.thresholdMax ?? 0.01 }; } } diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts index a409ab503..7eee7e52d 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts @@ -1,5 +1,7 @@ import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; export interface IRuleSettingsDialogParams { rule: PortfolioReportRule; + settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; } 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 265d3c941..0dd23ab16 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 @@ -1,4 +1,4 @@ -import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; import { CommonModule } from '@angular/common'; import { Component, Inject } from '@angular/core'; @@ -29,12 +29,10 @@ import { IRuleSettingsDialogParams } from './interfaces/interfaces'; templateUrl: './rule-settings-dialog.html' }) export class GfRuleSettingsDialogComponent { - public settings: PortfolioReportRule['settings']; + public settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; public constructor( @Inject(MAT_DIALOG_DATA) public data: IRuleSettingsDialogParams, public dialogRef: MatDialogRef - ) { - this.settings = this.data.rule.settings; - } + ) {} } diff --git a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html index ef86549f6..0c2477b63 100644 --- a/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html +++ b/apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html @@ -4,34 +4,38 @@ Threshold Min Threshold Max
-
diff --git a/apps/client/src/app/components/rule/rule.component.html b/apps/client/src/app/components/rule/rule.component.html index 5491933c0..7cea512e3 100644 --- a/apps/client/src/app/components/rule/rule.component.html +++ b/apps/client/src/app/components/rule/rule.component.html @@ -62,7 +62,7 @@ - @if (rule?.isActive && !isEmpty(rule.settings)) { + @if (rule?.isActive && rule?.configuration) { diff --git a/apps/client/src/app/components/rule/rule.component.ts b/apps/client/src/app/components/rule/rule.component.ts index 6e6c368f0..f51ce805f 100644 --- a/apps/client/src/app/components/rule/rule.component.ts +++ b/apps/client/src/app/components/rule/rule.component.ts @@ -1,5 +1,7 @@ import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; +import { RuleSettings } from '@ghostfolio/api/models/interfaces/rule-settings.interface'; import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; import { ChangeDetectionStrategy, @@ -10,7 +12,6 @@ import { Output } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; -import { isEmpty } from 'lodash'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject, takeUntil } from 'rxjs'; @@ -27,11 +28,10 @@ export class RuleComponent implements OnInit { @Input() hasPermissionToUpdateUserSettings: boolean; @Input() isLoading: boolean; @Input() rule: PortfolioReportRule; + @Input() settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; @Output() ruleUpdated = new EventEmitter(); - public isEmpty = isEmpty; - private deviceType: string; private unsubscribeSubject = new Subject(); @@ -46,16 +46,17 @@ export class RuleComponent implements OnInit { public onCustomizeRule(rule: PortfolioReportRule) { const dialogRef = this.dialog.open(GfRuleSettingsDialogComponent, { - data: { - rule - }, + data: { + rule, + settings: this.settings + } as IRuleSettingsDialogParams, width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((settings: PortfolioReportRule['settings']) => { + .subscribe((settings: RuleSettings) => { if (settings) { this.ruleUpdated.emit({ xRayRules: { diff --git a/apps/client/src/app/components/rules/rules.component.html b/apps/client/src/app/components/rules/rules.component.html index 31e61bfc2..28343673d 100644 --- a/apps/client/src/app/components/rules/rules.component.html +++ b/apps/client/src/app/components/rules/rules.component.html @@ -12,6 +12,7 @@ hasPermissionToUpdateUserSettings " [rule]="rule" + [settings]="settings?.[rule.key]" (ruleUpdated)="onRuleUpdated($event)" /> } diff --git a/apps/client/src/app/components/rules/rules.component.ts b/apps/client/src/app/components/rules/rules.component.ts index b8493e7be..fb2ef1cdb 100644 --- a/apps/client/src/app/components/rules/rules.component.ts +++ b/apps/client/src/app/components/rules/rules.component.ts @@ -1,5 +1,6 @@ import { UpdateUserSettingDto } from '@ghostfolio/api/app/user/update-user-setting.dto'; import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; +import { XRayRulesSettings } from '@ghostfolio/common/types'; import { ChangeDetectionStrategy, @@ -19,11 +20,10 @@ export class RulesComponent { @Input() hasPermissionToUpdateUserSettings: boolean; @Input() isLoading: boolean; @Input() rules: PortfolioReportRule[]; + @Input() settings: XRayRulesSettings; @Output() rulesUpdated = new EventEmitter(); - public constructor() {} - public onRuleUpdated(event: UpdateUserSettingDto) { this.rulesUpdated.emit(event); } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 10a2eb604..54f65b531 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -138,6 +138,11 @@ export class FirePageComponent implements OnDestroy, OnInit { .putUserSetting(event) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { + this.userService + .get(true) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(); + this.initializePortfolioReport(); }); } diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.html b/apps/client/src/app/pages/portfolio/fire/fire-page.html index b0fade836..c4a521a8c 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -132,6 +132,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="emergencyFundRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -150,6 +151,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="currencyClusterRiskRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -168,6 +170,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="accountClusterRiskRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -186,6 +189,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="feeRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> @@ -200,6 +204,7 @@ " [isLoading]="isLoadingPortfolioReport" [rules]="inactiveRules" + [settings]="user?.settings?.xRayRules" (rulesUpdated)="onRulesUpdated($event)" /> diff --git a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts index 29cbb4a8f..f69c097fc 100644 --- a/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts +++ b/libs/common/src/lib/interfaces/portfolio-report-rule.interface.ts @@ -1,11 +1,16 @@ export interface PortfolioReportRule { + configuration?: { + threshold?: { + max: number; + min: number; + step: number; + }; + thresholdMax?: boolean; + thresholdMin?: boolean; + }; evaluation?: string; isActive: boolean; key: string; name: string; - settings?: { - thresholdMax?: number; - thresholdMin?: number; - }; value?: boolean; } diff --git a/libs/common/src/lib/types/x-ray-rules-settings.type.ts b/libs/common/src/lib/types/x-ray-rules-settings.type.ts index a55487f0b..fddd708cc 100644 --- a/libs/common/src/lib/types/x-ray-rules-settings.type.ts +++ b/libs/common/src/lib/types/x-ray-rules-settings.type.ts @@ -1,5 +1,3 @@ -import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; - export type XRayRulesSettings = { AccountClusterRiskCurrentInvestment?: RuleSettings; AccountClusterRiskSingleAccount?: RuleSettings; @@ -9,6 +7,8 @@ export type XRayRulesSettings = { FeeRatioInitialInvestment?: RuleSettings; }; -interface RuleSettings extends Pick { +interface RuleSettings { isActive: boolean; + thresholdMax?: number; + thresholdMin?: number; }