diff --git a/CHANGELOG.md b/CHANGELOG.md
index f16aa2147..71c65d179 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
+
+- Added an option to use the annualized performance as the annual interest rate in the _FIRE_ calculator
+
### Changed
- Moved the improved symbol lookup results by removing the currency from the name of cryptocurrencies from experimental to general availability
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 b7ef8b302..6f065aaf0 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
@@ -50,6 +50,7 @@ export class GfFirePageComponent implements OnInit {
() => this.deviceDetectorService.deviceInfo().deviceType
);
+ protected annualizedPerformancePercent: number | undefined;
protected fireWealth: FireWealth;
protected hasImpersonationId: boolean;
protected hasPermissionToUpdateUserSettings: boolean;
@@ -99,6 +100,9 @@ export class GfFirePageComponent implements OnInit {
.fetchPortfolioDetails()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => {
+ this.annualizedPerformancePercent =
+ summary?.annualizedPerformancePercentWithCurrencyEffect;
+
this.fireWealth = {
today: {
valueInBaseCurrency: summary?.fireWealth
@@ -243,6 +247,24 @@ export class GfFirePageComponent implements OnInit {
});
}
+ protected onUseAnnualizedPerformanceRateChange(
+ useAnnualizedPerformanceRate: boolean
+ ) {
+ this.dataService
+ .putUserSetting({ useAnnualizedPerformanceRate })
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(() => {
+ this.userService
+ .get(true)
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((user) => {
+ this.user = user;
+
+ this.changeDetectorRef.markForCheck();
+ });
+ });
+ }
+
private calculateWithdrawalRates() {
if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
this.withdrawalRatePerYear = new Big(
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 7315f10cb..1d5d8ff5e 100644
--- a/apps/client/src/app/pages/portfolio/fire/fire-page.html
+++ b/apps/client/src/app/pages/portfolio/fire/fire-page.html
@@ -11,6 +11,7 @@
diff --git a/libs/common/src/lib/dtos/update-user-setting.dto.ts b/libs/common/src/lib/dtos/update-user-setting.dto.ts
index d46982e70..daf1430a8 100644
--- a/libs/common/src/lib/dtos/update-user-setting.dto.ts
+++ b/libs/common/src/lib/dtos/update-user-setting.dto.ts
@@ -120,6 +120,10 @@ export class UpdateUserSettingDto {
@IsOptional()
savingsRate?: number;
+ @IsBoolean()
+ @IsOptional()
+ useAnnualizedPerformanceRate?: boolean;
+
@IsIn(['DEFAULT', 'ZEN'] as ViewMode[])
@IsOptional()
viewMode?: ViewMode;
diff --git a/libs/common/src/lib/interfaces/user-settings.interface.ts b/libs/common/src/lib/interfaces/user-settings.interface.ts
index 65325a42f..f4377f693 100644
--- a/libs/common/src/lib/interfaces/user-settings.interface.ts
+++ b/libs/common/src/lib/interfaces/user-settings.interface.ts
@@ -31,6 +31,7 @@ export interface UserSettings {
retirementDate?: string;
safeWithdrawalRate?: number;
savingsRate?: number;
+ useAnnualizedPerformanceRate?: boolean;
viewMode?: ViewMode;
xRayRules?: XRayRulesSettings;
}
diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.html b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html
index b2162a764..80a8b7870 100644
--- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.html
+++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.html
@@ -28,6 +28,15 @@
%
+
+ Use annualized performance
+
+
Retirement Date
{{ retirementDateLabel }}
diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss b/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss
index 5662415da..8e3a84454 100644
--- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss
+++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.scss
@@ -1,6 +1,10 @@
:host {
display: block;
+ .use-annualized-performance-rate {
+ margin-top: -0.75rem;
+ }
+
.chart-container {
aspect-ratio: 16 / 9;
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 cfa2ee5cd..db69e9266 100644
--- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
+++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
@@ -28,6 +28,7 @@ import {
ReactiveFormsModule
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
+import { MatCheckboxModule } from '@angular/material/checkbox';
import {
MatDatepicker,
MatDatepickerModule
@@ -72,6 +73,7 @@ import { FireCalculatorService } from './fire-calculator.service';
FormsModule,
IonIcon,
MatButtonModule,
+ MatCheckboxModule,
MatDatepickerModule,
MatFormFieldModule,
MatInputModule,
@@ -85,6 +87,7 @@ import { FireCalculatorService } from './fire-calculator.service';
})
export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() annualInterestRate = 0;
+ @Input() annualizedPerformancePercent: number;
@Input() colorScheme: ColorScheme;
@Input() currency: string;
@Input() deviceType: string;
@@ -94,13 +97,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
@Input() projectedTotalAmount = 0;
@Input() retirementDate: Date;
@Input() savingsRate = 0;
+ @Input() useAnnualizedPerformanceRate = false;
public calculatorForm = this.formBuilder.group({
annualInterestRate: new FormControl(null),
paymentPerPeriod: new FormControl(null),
principalInvestmentAmount: new FormControl(null),
projectedTotalAmount: new FormControl(null),
- retirementDate: new FormControl(null)
+ retirementDate: new FormControl(null),
+ useAnnualizedPerformanceRate: new FormControl(false)
});
public chart: Chart<'bar'>;
@@ -116,6 +121,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
protected readonly projectedTotalAmountChanged = output();
protected readonly retirementDateChanged = output();
protected readonly savingsRateChanged = output();
+ protected readonly useAnnualizedPerformanceRateChanged = output();
private readonly CONTRIBUTION_PERIOD = 12;
@@ -206,6 +212,27 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.retirementDateChanged.emit(retirementDate);
}
});
+ this.calculatorForm
+ .get('useAnnualizedPerformanceRate')
+ ?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((useAnnualizedPerformanceRate) => {
+ if (useAnnualizedPerformanceRate !== null) {
+ this.applyAnnualizedPerformanceRate(useAnnualizedPerformanceRate);
+ }
+ });
+ this.calculatorForm
+ .get('useAnnualizedPerformanceRate')
+ ?.valueChanges.pipe(
+ debounceTime(500),
+ takeUntilDestroyed(this.destroyRef)
+ )
+ .subscribe((useAnnualizedPerformanceRate) => {
+ if (useAnnualizedPerformanceRate !== null) {
+ this.useAnnualizedPerformanceRateChanged.emit(
+ useAnnualizedPerformanceRate
+ );
+ }
+ });
}
protected get retirementDateLabel(): string {
@@ -222,14 +249,24 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}
public ngOnChanges() {
+ const useAnnualizedPerformanceRate =
+ this.useAnnualizedPerformanceRate ?? false;
+
+ const annualInterestRate =
+ useAnnualizedPerformanceRate &&
+ this.isAnnualizedPerformancePercentAvailable()
+ ? this.getAnnualizedInterestRatePercent()
+ : this.annualInterestRate;
+
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
this.calculatorForm.setValue(
{
- annualInterestRate: this.annualInterestRate,
+ annualInterestRate,
paymentPerPeriod: this.savingsRate,
principalInvestmentAmount: this.fireWealth,
projectedTotalAmount: this.projectedTotalAmount,
- retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE
+ retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE,
+ useAnnualizedPerformanceRate
},
{
emitEvent: false
@@ -271,6 +308,16 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm
.get('projectedTotalAmount')
?.enable({ emitEvent: false });
+
+ if (this.isAnnualizedPerformancePercentAvailable()) {
+ this.calculatorForm
+ .get('useAnnualizedPerformanceRate')
+ ?.enable({ emitEvent: false });
+ } else {
+ this.calculatorForm
+ .get('useAnnualizedPerformanceRate')
+ ?.disable({ emitEvent: false });
+ }
} else {
this.calculatorForm
.get('annualInterestRate')
@@ -281,6 +328,15 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
this.calculatorForm
.get('projectedTotalAmount')
?.disable({ emitEvent: false });
+ this.calculatorForm
+ .get('useAnnualizedPerformanceRate')
+ ?.disable({ emitEvent: false });
+ }
+
+ if (useAnnualizedPerformanceRate) {
+ this.calculatorForm
+ .get('annualInterestRate')
+ ?.disable({ emitEvent: false });
}
this.calculatorForm.get('retirementDate')?.disable({ emitEvent: false });
@@ -467,6 +523,45 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
};
}
+ private applyAnnualizedPerformanceRate(
+ useAnnualizedPerformanceRate: boolean
+ ) {
+ const annualInterestRateControl =
+ this.calculatorForm.get('annualInterestRate');
+
+ if (
+ useAnnualizedPerformanceRate &&
+ this.isAnnualizedPerformancePercentAvailable()
+ ) {
+ annualInterestRateControl?.setValue(
+ this.getAnnualizedInterestRatePercent(),
+ { emitEvent: false }
+ );
+ annualInterestRateControl?.disable({ emitEvent: false });
+ } else {
+ annualInterestRateControl?.setValue(this.annualInterestRate, {
+ emitEvent: false
+ });
+
+ if (this.hasPermissionToUpdateUserSettings === true) {
+ annualInterestRateControl?.enable({ emitEvent: false });
+ }
+ }
+
+ this.initialize();
+ }
+
+ private isAnnualizedPerformancePercentAvailable(): boolean {
+ return (
+ isNumber(this.annualizedPerformancePercent) &&
+ !Number.isNaN(this.annualizedPerformancePercent)
+ );
+ }
+
+ private getAnnualizedInterestRatePercent(): number {
+ return Math.round(this.annualizedPerformancePercent * 10000) / 100;
+ }
+
private getP() {
return this.fireWealth || 0;
}