diff --git a/CHANGELOG.md b/CHANGELOG.md index f86d7c01a..dab53a614 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Included wealth projection data calculated for the retirement date in the _FIRE_ section (experimental) + ### Changed - Moved the notification module to `@ghostfolio/ui` diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 65ce92cb2..3280fbfac 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -248,6 +248,11 @@ export class UserService { }; } + // Set default value for annual interest rate + if (!(user.settings.settings as UserSettings)?.annualInterestRate) { + (user.settings.settings as UserSettings).annualInterestRate = 5; + } + // Set default value for base currency if (!(user.settings.settings as UserSettings)?.baseCurrency) { (user.settings.settings as UserSettings).baseCurrency = DEFAULT_CURRENCY; @@ -265,11 +270,21 @@ export class UserService { PerformanceCalculationType.ROAI; } + // Set default value for projected total amount + if (!(user.settings.settings as UserSettings)?.projectedTotalAmount) { + (user.settings.settings as UserSettings).projectedTotalAmount = 0; + } + // Set default value for safe withdrawal rate if (!(user.settings.settings as UserSettings)?.safeWithdrawalRate) { (user.settings.settings as UserSettings).safeWithdrawalRate = 0.04; } + // Set default value for savings rate + if (!(user.settings.settings as UserSettings)?.savingsRate) { + (user.settings.settings as UserSettings).savingsRate = 0; + } + // Set default value for view mode if (!(user.settings.settings as UserSettings).viewMode) { (user.settings.settings as UserSettings).viewMode = 'DEFAULT'; 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 63187c05c..da1378d22 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 @@ -1,7 +1,11 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { FireWealth, User } from '@ghostfolio/common/interfaces'; +import { + FireCalculationCompleteEvent, + FireWealth, + User +} from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { GfFireCalculatorComponent } from '@ghostfolio/ui/fire-calculator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; @@ -38,11 +42,15 @@ export class GfFirePageComponent implements OnDestroy, OnInit { public hasImpersonationId: boolean; public hasPermissionToUpdateUserSettings: boolean; public isLoading = false; + public projectedTotalAmount: number; + public retirementDate: Date; public safeWithdrawalRateControl = new FormControl(undefined); public safeWithdrawalRateOptions = [0.025, 0.03, 0.035, 0.04, 0.045]; public user: User; public withdrawalRatePerMonth: Big; + public withdrawalRatePerMonthProjected: Big; public withdrawalRatePerYear: Big; + public withdrawalRatePerYearProjected: Big; private unsubscribeSubject = new Subject(); @@ -79,8 +87,6 @@ export class GfFirePageComponent implements OnDestroy, OnInit { this.calculateWithdrawalRates(); - this.isLoading = false; - this.changeDetectorRef.markForCheck(); }); @@ -139,6 +145,18 @@ export class GfFirePageComponent implements OnDestroy, OnInit { }); } + public onCalculationComplete({ + projectedTotalAmount, + retirementDate + }: FireCalculationCompleteEvent) { + this.projectedTotalAmount = projectedTotalAmount; + this.retirementDate = retirementDate; + + this.calculateWithdrawalRatesProjected(); + + this.isLoading = false; + } + public onRetirementDateChange(retirementDate: Date) { this.dataService .putUserSetting({ @@ -170,6 +188,7 @@ export class GfFirePageComponent implements OnDestroy, OnInit { this.user = user; this.calculateWithdrawalRates(); + this.calculateWithdrawalRatesProjected(); this.changeDetectorRef.markForCheck(); }); @@ -225,4 +244,19 @@ export class GfFirePageComponent implements OnDestroy, OnInit { this.withdrawalRatePerMonth = this.withdrawalRatePerYear.div(12); } } + + private calculateWithdrawalRatesProjected() { + if ( + this.fireWealth && + this.projectedTotalAmount && + this.user?.settings?.safeWithdrawalRate + ) { + this.withdrawalRatePerYearProjected = new Big( + this.projectedTotalAmount + ).mul(this.user.settings.safeWithdrawalRate); + + this.withdrawalRatePerMonthProjected = + this.withdrawalRatePerYearProjected.div(12); + } + } } 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 ce51717fa..a4a17fa86 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.html +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html @@ -28,6 +28,7 @@ [retirementDate]="user?.settings?.retirementDate" [savingsRate]="user?.settings?.savingsRate" (annualInterestRateChanged)="onAnnualInterestRateChange($event)" + (calculationCompleted)="onCalculationComplete($event)" (projectedTotalAmountChanged)="onProjectedTotalAmountChange($event)" (retirementDateChanged)="onRetirementDateChange($event)" (savingsRateChanged)="onSavingsRateChange($event)" @@ -62,74 +63,129 @@ } @else {
- If you retire today, you would be able to withdraw -   - -   - per year -   - or -   - +
+ If you retire today, you would be able to withdraw   - per month, -   - based on your total assets of -   - - -   - and a safe withdrawal rate (SWR) of - @if ( - !hasImpersonationId && - hasPermissionToUpdateUserSettings && - user?.settings?.isExperimentalFeatures - ) { - . - } @else { +   + or   . + [unit]="user?.settings?.baseCurrency" + [value]="withdrawalRatePerMonth?.toNumber()" + /> +   + per month, +   + based on your total assets of +   + + +   + and a safe withdrawal rate (SWR) of + @if ( + !hasImpersonationId && + hasPermissionToUpdateUserSettings && + user?.settings?.isExperimentalFeatures + ) { + . + } @else { +   + . + } +
+ + @if (user?.settings?.isExperimentalFeatures) { +
+ By +   + {{ + user?.settings?.retirementDate ?? retirementDate + | date: 'MMMM yyyy' + }} + , +   + this is projected to increase to +   + +   + per year +   + or +   + +   + per month, +   + assuming a +   + +   + annual interest rate. +
}
} diff --git a/libs/common/src/lib/interfaces/fire-calculation-complete-event.interface.ts b/libs/common/src/lib/interfaces/fire-calculation-complete-event.interface.ts new file mode 100644 index 000000000..1238b0729 --- /dev/null +++ b/libs/common/src/lib/interfaces/fire-calculation-complete-event.interface.ts @@ -0,0 +1,4 @@ +export interface FireCalculationCompleteEvent { + projectedTotalAmount: number; + retirementDate: Date; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 1d7991e40..4b8e8009a 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -18,6 +18,7 @@ import type { DataProviderInfo } from './data-provider-info.interface'; import type { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; import type { FilterGroup } from './filter-group.interface'; import type { Filter } from './filter.interface'; +import type { FireCalculationCompleteEvent } from './fire-calculation-complete-event.interface'; import type { FireWealth } from './fire-wealth.interface'; import type { HistoricalDataItem } from './historical-data-item.interface'; import type { HoldingWithParents } from './holding-with-parents.interface'; @@ -140,6 +141,7 @@ export { ExportResponse, Filter, FilterGroup, + FireCalculationCompleteEvent, FireWealth, HistoricalDataItem, HistoricalResponse, diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index 44276ec43..655798b3d 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -4,6 +4,7 @@ import { } from '@ghostfolio/common/chart-helper'; import { primaryColorRgb } from '@ghostfolio/common/config'; import { getLocale } from '@ghostfolio/common/helper'; +import { FireCalculationCompleteEvent } from '@ghostfolio/common/interfaces'; import { ColorScheme } from '@ghostfolio/common/types'; import { CommonModule } from '@angular/common'; @@ -88,6 +89,8 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { @Input() savingsRate: number; @Output() annualInterestRateChanged = new EventEmitter(); + @Output() calculationCompleted = + new EventEmitter(); @Output() projectedTotalAmountChanged = new EventEmitter(); @Output() retirementDateChanged = new EventEmitter(); @Output() savingsRateChanged = new EventEmitter(); @@ -131,6 +134,18 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { this.initialize(); }); + this.calculatorForm.valueChanges + .pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + const { projectedTotalAmount, retirementDate } = + this.calculatorForm.getRawValue(); + + this.calculationCompleted.emit({ + projectedTotalAmount, + retirementDate + }); + }); + this.calculatorForm .get('annualInterestRate') .valueChanges.pipe(debounceTime(500), takeUntil(this.unsubscribeSubject)) @@ -161,10 +176,10 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { if (isNumber(this.fireWealth) && this.fireWealth >= 0) { this.calculatorForm.setValue( { - annualInterestRate: this.annualInterestRate ?? 5, - paymentPerPeriod: this.savingsRate ?? 0, + annualInterestRate: this.annualInterestRate, + paymentPerPeriod: this.savingsRate, principalInvestmentAmount: this.fireWealth, - projectedTotalAmount: this.projectedTotalAmount ?? 0, + projectedTotalAmount: this.projectedTotalAmount, retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE }, {