Feature/add fire calculator (#822)
* Add fire calculator * Update changelog
This commit is contained in:
parent
d5ba624403
commit
23f2ac472e
@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Added a calculator to the _FIRE_ section
|
||||||
- Added support for the cryptocurrency _Terra_ (`LUNA1-USD`)
|
- Added support for the cryptocurrency _Terra_ (`LUNA1-USD`)
|
||||||
- Added support for the cryptocurrency _THORChain_ (`RUNE-USD`)
|
- Added support for the cryptocurrency _THORChain_ (`RUNE-USD`)
|
||||||
|
|
||||||
|
@ -10,7 +10,10 @@ import {
|
|||||||
ViewChild
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { primaryColorRgb } from '@ghostfolio/common/config';
|
import { primaryColorRgb } from '@ghostfolio/common/config';
|
||||||
import { parseDate } from '@ghostfolio/common/helper';
|
import {
|
||||||
|
parseDate,
|
||||||
|
transformTickToAbbreviation
|
||||||
|
} from '@ghostfolio/common/helper';
|
||||||
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
|
||||||
import {
|
import {
|
||||||
Chart,
|
Chart,
|
||||||
@ -148,19 +151,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
|
|||||||
display: false
|
display: false
|
||||||
},
|
},
|
||||||
ticks: {
|
ticks: {
|
||||||
display: true,
|
callback: (value: number) => {
|
||||||
callback: (tickValue, index, ticks) => {
|
return transformTickToAbbreviation(value);
|
||||||
if (index === 0 || index === ticks.length - 1) {
|
|
||||||
// Only print last and first legend entry
|
|
||||||
if (typeof tickValue === 'number') {
|
|
||||||
return tickValue.toFixed(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tickValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
},
|
},
|
||||||
|
display: true,
|
||||||
mirror: true,
|
mirror: true,
|
||||||
z: 1
|
z: 1
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { DataService } from '@ghostfolio/client/services/data.service';
|
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 { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||||
import { User } from '@ghostfolio/common/interfaces';
|
import { User } from '@ghostfolio/common/interfaces';
|
||||||
import Big from 'big.js';
|
import Big from 'big.js';
|
||||||
|
import { DeviceDetectorService } from 'ngx-device-detector';
|
||||||
import { Subject } from 'rxjs';
|
import { Subject } from 'rxjs';
|
||||||
import { takeUntil } from 'rxjs/operators';
|
import { takeUntil } from 'rxjs/operators';
|
||||||
|
|
||||||
@ -14,6 +14,7 @@ import { takeUntil } from 'rxjs/operators';
|
|||||||
templateUrl: './fire-page.html'
|
templateUrl: './fire-page.html'
|
||||||
})
|
})
|
||||||
export class FirePageComponent implements OnDestroy, OnInit {
|
export class FirePageComponent implements OnDestroy, OnInit {
|
||||||
|
public deviceType: string;
|
||||||
public fireWealth: Big;
|
public fireWealth: Big;
|
||||||
public isLoading = false;
|
public isLoading = false;
|
||||||
public user: User;
|
public user: User;
|
||||||
@ -28,7 +29,7 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
public constructor(
|
public constructor(
|
||||||
private changeDetectorRef: ChangeDetectorRef,
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
private dataService: DataService,
|
private dataService: DataService,
|
||||||
private impersonationStorageService: ImpersonationStorageService,
|
private deviceService: DeviceDetectorService,
|
||||||
private userService: UserService
|
private userService: UserService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ export class FirePageComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
public ngOnInit() {
|
public ngOnInit() {
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
|
this.deviceType = this.deviceService.getDeviceInfo().deviceType;
|
||||||
|
|
||||||
this.dataService
|
this.dataService
|
||||||
.fetchPortfolioSummary()
|
.fetchPortfolioSummary()
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg">
|
<div class="col-lg">
|
||||||
<h3 class="d-flex justify-content-center mb-3" i18n>FIRE</h3>
|
<h3 class="d-flex justify-content-center mb-3" i18n>FIRE</h3>
|
||||||
<div class="mb-4">
|
<div class="mb-5">
|
||||||
<h4 i18n>4% Rule</h4>
|
<h4 i18n>4% Rule</h4>
|
||||||
<div *ngIf="isLoading">
|
<div *ngIf="isLoading">
|
||||||
<ngx-skeleton-loader
|
<ngx-skeleton-loader
|
||||||
@ -53,4 +53,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 class="mb-3" i18n>Calculator</h4>
|
||||||
|
<gf-fire-calculator
|
||||||
|
[currency]="user?.settings?.baseCurrency"
|
||||||
|
[deviceType]="deviceType"
|
||||||
|
[fireWealth]="fireWealth?.toNumber()"
|
||||||
|
[locale]="user?.settings?.locale"
|
||||||
|
></gf-fire-calculator>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||||
|
import { GfFireCalculatorModule } from '@ghostfolio/ui/fire-calculator';
|
||||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
@ -11,6 +12,7 @@ import { FirePageComponent } from './fire-page.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
FirePageRoutingModule,
|
FirePageRoutingModule,
|
||||||
|
GfFireCalculatorModule,
|
||||||
GfValueModule,
|
GfValueModule,
|
||||||
NgxSkeletonLoaderModule
|
NgxSkeletonLoaderModule
|
||||||
],
|
],
|
||||||
|
@ -176,3 +176,7 @@ export function parseDate(date: string) {
|
|||||||
export function prettifySymbol(aSymbol: string): string {
|
export function prettifySymbol(aSymbol: string): string {
|
||||||
return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, '');
|
return aSymbol?.replace(ghostfolioScraperApiSymbolPrefix, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function transformTickToAbbreviation(value: number) {
|
||||||
|
return value < 1000000 ? `${value / 1000}K` : `${value / 1000000}M`;
|
||||||
|
}
|
||||||
|
@ -0,0 +1,65 @@
|
|||||||
|
<div class="container p-0">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<form class="" [formGroup]="calculatorForm">
|
||||||
|
<!--<mat-form-field appearance="outline">
|
||||||
|
<input formControlName="principalInvestmentAmount" matInput />
|
||||||
|
</mat-form-field>-->
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Savings Rate</mat-label>
|
||||||
|
<input
|
||||||
|
formControlName="paymentPerPeriod"
|
||||||
|
matInput
|
||||||
|
step="100"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<span class="ml-2" i18n matSuffix>{{ currency }} per month</span>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Investment Horizon</mat-label>
|
||||||
|
<input formControlName="time" matInput type="number" />
|
||||||
|
<span class="ml-2" i18n matSuffix>years</span>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<mat-form-field appearance="outline" class="w-100">
|
||||||
|
<mat-label i18n>Annual Interest Rate</mat-label>
|
||||||
|
<input
|
||||||
|
formControlName="annualInterestRate"
|
||||||
|
matInput
|
||||||
|
step="0.25"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
<span class="ml-2" i18n matSuffix>%</span>
|
||||||
|
</mat-form-field>
|
||||||
|
|
||||||
|
<gf-value
|
||||||
|
label="Projected Total Amount"
|
||||||
|
size="large"
|
||||||
|
[currency]="currency"
|
||||||
|
[isCurrency]="true"
|
||||||
|
[locale]="locale"
|
||||||
|
[value]="projectedTotalAmount"
|
||||||
|
></gf-value>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-9 text-center">
|
||||||
|
<div class="chart-container mb-4">
|
||||||
|
<ngx-skeleton-loader
|
||||||
|
*ngIf="isLoading"
|
||||||
|
animation="pulse"
|
||||||
|
[theme]="{
|
||||||
|
height: '100%',
|
||||||
|
width: '100%'
|
||||||
|
}"
|
||||||
|
></ngx-skeleton-loader>
|
||||||
|
<canvas
|
||||||
|
#chartCanvas
|
||||||
|
class="h-100"
|
||||||
|
[ngStyle]="{ display: isLoading ? 'none' : 'block' }"
|
||||||
|
></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,11 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
|
||||||
|
ngx-skeleton-loader {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { baseCurrency, locale } from '@ghostfolio/common/config';
|
||||||
|
import { Meta, Story, moduleMetadata } from '@storybook/angular';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
import { GfValueModule } from '../value';
|
||||||
|
|
||||||
|
import { FireCalculatorComponent } from './fire-calculator.component';
|
||||||
|
import { FireCalculatorService } from './fire-calculator.service';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'FIRE Calculator',
|
||||||
|
component: FireCalculatorComponent,
|
||||||
|
decorators: [
|
||||||
|
moduleMetadata({
|
||||||
|
declarations: [FireCalculatorComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
GfValueModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
NoopAnimationsModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
providers: [FireCalculatorService]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
} as Meta<FireCalculatorComponent>;
|
||||||
|
|
||||||
|
const Template: Story<FireCalculatorComponent> = (
|
||||||
|
args: FireCalculatorComponent
|
||||||
|
) => ({
|
||||||
|
props: args
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Simple = Template.bind({});
|
||||||
|
Simple.args = {
|
||||||
|
currency: baseCurrency,
|
||||||
|
fireWealth: 0,
|
||||||
|
locale: locale
|
||||||
|
};
|
247
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
Normal file
247
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
import 'chartjs-adapter-date-fns';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AfterViewInit,
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Component,
|
||||||
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
|
ViewChild
|
||||||
|
} from '@angular/core';
|
||||||
|
import { FormBuilder, FormControl } from '@angular/forms';
|
||||||
|
import { primaryColorRgb, secondaryColorRgb } from '@ghostfolio/common/config';
|
||||||
|
import {
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
Chart,
|
||||||
|
LinearScale,
|
||||||
|
Tooltip
|
||||||
|
} from 'chart.js';
|
||||||
|
|
||||||
|
import { FireCalculatorService } from './fire-calculator.service';
|
||||||
|
import { Subject, takeUntil } from 'rxjs';
|
||||||
|
import { transformTickToAbbreviation } from '@ghostfolio/common/helper';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'gf-fire-calculator',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
templateUrl: './fire-calculator.component.html',
|
||||||
|
styleUrls: ['./fire-calculator.component.scss']
|
||||||
|
})
|
||||||
|
export class FireCalculatorComponent
|
||||||
|
implements AfterViewInit, OnChanges, OnDestroy
|
||||||
|
{
|
||||||
|
@Input() currency: string;
|
||||||
|
@Input() deviceType: string;
|
||||||
|
@Input() fireWealth: number;
|
||||||
|
@Input() locale: string;
|
||||||
|
|
||||||
|
@ViewChild('chartCanvas') chartCanvas;
|
||||||
|
|
||||||
|
public calculatorForm = this.formBuilder.group({
|
||||||
|
annualInterestRate: new FormControl(),
|
||||||
|
paymentPerPeriod: new FormControl(),
|
||||||
|
principalInvestmentAmount: new FormControl(),
|
||||||
|
time: new FormControl()
|
||||||
|
});
|
||||||
|
public chart: Chart;
|
||||||
|
public isLoading = true;
|
||||||
|
public projectedTotalAmount: number;
|
||||||
|
|
||||||
|
private unsubscribeSubject = new Subject<void>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private fireCalculatorService: FireCalculatorService,
|
||||||
|
private formBuilder: FormBuilder
|
||||||
|
) {
|
||||||
|
Chart.register(
|
||||||
|
BarController,
|
||||||
|
BarElement,
|
||||||
|
CategoryScale,
|
||||||
|
LinearScale,
|
||||||
|
Tooltip
|
||||||
|
);
|
||||||
|
|
||||||
|
this.calculatorForm.setValue({
|
||||||
|
annualInterestRate: 5,
|
||||||
|
paymentPerPeriod: 500,
|
||||||
|
principalInvestmentAmount: 0,
|
||||||
|
time: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
this.calculatorForm.valueChanges
|
||||||
|
.pipe(takeUntil(this.unsubscribeSubject))
|
||||||
|
.subscribe(() => {
|
||||||
|
this.initialize();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngAfterViewInit() {
|
||||||
|
if (this.fireWealth >= 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Wait for the chartCanvas
|
||||||
|
this.calculatorForm.patchValue({
|
||||||
|
principalInvestmentAmount: this.fireWealth
|
||||||
|
});
|
||||||
|
this.calculatorForm.get('principalInvestmentAmount').disable();
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnChanges() {
|
||||||
|
if (this.fireWealth >= 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Wait for the chartCanvas
|
||||||
|
this.calculatorForm.patchValue({
|
||||||
|
principalInvestmentAmount: this.fireWealth
|
||||||
|
});
|
||||||
|
this.calculatorForm.get('principalInvestmentAmount').disable();
|
||||||
|
|
||||||
|
this.changeDetectorRef.markForCheck();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ngOnDestroy() {
|
||||||
|
this.chart?.destroy();
|
||||||
|
|
||||||
|
this.unsubscribeSubject.next();
|
||||||
|
this.unsubscribeSubject.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
private initialize() {
|
||||||
|
this.isLoading = true;
|
||||||
|
|
||||||
|
const chartData = this.getChartData();
|
||||||
|
|
||||||
|
if (this.chartCanvas) {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.data.labels = chartData.labels;
|
||||||
|
this.chart.data.datasets[0].data = chartData.datasets[0].data;
|
||||||
|
this.chart.data.datasets[1].data = chartData.datasets[1].data;
|
||||||
|
|
||||||
|
this.chart.update();
|
||||||
|
} else {
|
||||||
|
this.chart = new Chart(this.chartCanvas.nativeElement, {
|
||||||
|
data: chartData,
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: (context) => {
|
||||||
|
let label = context.dataset.label || '';
|
||||||
|
|
||||||
|
if (label) {
|
||||||
|
label += ': ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.parsed.y !== null) {
|
||||||
|
label += new Intl.NumberFormat(this.locale, {
|
||||||
|
currency: this.currency,
|
||||||
|
currencyDisplay: 'code',
|
||||||
|
style: 'currency'
|
||||||
|
}).format(context.parsed.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
stacked: true
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
display: this.deviceType !== 'mobile',
|
||||||
|
grid: {
|
||||||
|
display: false
|
||||||
|
},
|
||||||
|
stacked: true,
|
||||||
|
ticks: {
|
||||||
|
callback: (value: number) => {
|
||||||
|
return transformTickToAbbreviation(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
type: 'bar'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isLoading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getChartData() {
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const labels = [];
|
||||||
|
|
||||||
|
// Principal investment amount
|
||||||
|
const P: number =
|
||||||
|
this.calculatorForm.get('principalInvestmentAmount').value || 0;
|
||||||
|
|
||||||
|
// Payment per period
|
||||||
|
const PMT: number = parseFloat(
|
||||||
|
this.calculatorForm.get('paymentPerPeriod').value
|
||||||
|
);
|
||||||
|
|
||||||
|
// Annual interest rate
|
||||||
|
const r: number = this.calculatorForm.get('annualInterestRate').value / 100;
|
||||||
|
|
||||||
|
// Time
|
||||||
|
const t: number = parseFloat(this.calculatorForm.get('time').value);
|
||||||
|
|
||||||
|
for (let year = currentYear; year < currentYear + t; year++) {
|
||||||
|
labels.push(year);
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasetInterest = {
|
||||||
|
backgroundColor: `rgb(${secondaryColorRgb.r}, ${secondaryColorRgb.g}, ${secondaryColorRgb.b})`,
|
||||||
|
data: [],
|
||||||
|
label: 'Interest'
|
||||||
|
};
|
||||||
|
|
||||||
|
const datasetPrincipal = {
|
||||||
|
backgroundColor: `rgb(${primaryColorRgb.r}, ${primaryColorRgb.g}, ${primaryColorRgb.b})`,
|
||||||
|
data: [],
|
||||||
|
label: 'Principal'
|
||||||
|
};
|
||||||
|
|
||||||
|
for (let period = 1; period <= t; period++) {
|
||||||
|
const { interest, principal, totalAmount } =
|
||||||
|
this.fireCalculatorService.calculateCompoundInterest({
|
||||||
|
P,
|
||||||
|
period,
|
||||||
|
PMT,
|
||||||
|
r
|
||||||
|
});
|
||||||
|
|
||||||
|
datasetPrincipal.data.push(principal.toNumber());
|
||||||
|
datasetInterest.data.push(interest.toNumber());
|
||||||
|
|
||||||
|
if (period === t - 1) {
|
||||||
|
this.projectedTotalAmount = totalAmount.toNumber();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
datasets: [datasetPrincipal, datasetInterest]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
28
libs/ui/src/lib/fire-calculator/fire-calculator.module.ts
Normal file
28
libs/ui/src/lib/fire-calculator/fire-calculator.module.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
import { MatButtonModule } from '@angular/material/button';
|
||||||
|
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||||
|
import { MatInputModule } from '@angular/material/input';
|
||||||
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||||
|
|
||||||
|
import { GfValueModule } from '../value';
|
||||||
|
import { FireCalculatorComponent } from './fire-calculator.component';
|
||||||
|
import { FireCalculatorService } from './fire-calculator.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [FireCalculatorComponent],
|
||||||
|
exports: [FireCalculatorComponent],
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
GfValueModule,
|
||||||
|
MatButtonModule,
|
||||||
|
MatFormFieldModule,
|
||||||
|
MatInputModule,
|
||||||
|
NgxSkeletonLoaderModule,
|
||||||
|
ReactiveFormsModule
|
||||||
|
],
|
||||||
|
providers: [FireCalculatorService]
|
||||||
|
})
|
||||||
|
export class GfFireCalculatorModule {}
|
49
libs/ui/src/lib/fire-calculator/fire-calculator.service.ts
Normal file
49
libs/ui/src/lib/fire-calculator/fire-calculator.service.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import Big from 'big.js';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class FireCalculatorService {
|
||||||
|
private readonly COMPOUND_PERIOD = 12;
|
||||||
|
private readonly CONTRIBUTION_PERIOD = 12;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public constructor() {}
|
||||||
|
|
||||||
|
public calculateCompoundInterest({
|
||||||
|
P,
|
||||||
|
period,
|
||||||
|
PMT,
|
||||||
|
r
|
||||||
|
}: {
|
||||||
|
P: number;
|
||||||
|
period: number;
|
||||||
|
PMT: number;
|
||||||
|
r: number;
|
||||||
|
}) {
|
||||||
|
let interest = new Big(0);
|
||||||
|
const principal = new Big(P).plus(
|
||||||
|
new Big(PMT).mul(this.CONTRIBUTION_PERIOD).mul(period)
|
||||||
|
);
|
||||||
|
let totalAmount = principal;
|
||||||
|
|
||||||
|
if (r) {
|
||||||
|
const compoundInterestForPrincipal = new Big(1)
|
||||||
|
.plus(new Big(r).div(this.COMPOUND_PERIOD))
|
||||||
|
.pow(new Big(this.COMPOUND_PERIOD).mul(period).toNumber());
|
||||||
|
const compoundInterest = new Big(P).mul(compoundInterestForPrincipal);
|
||||||
|
const contributionInterest = new Big(
|
||||||
|
new Big(PMT).mul(compoundInterestForPrincipal.minus(1))
|
||||||
|
).div(new Big(r).div(this.CONTRIBUTION_PERIOD));
|
||||||
|
interest = compoundInterest.plus(contributionInterest).minus(principal);
|
||||||
|
totalAmount = compoundInterest.plus(contributionInterest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
interest,
|
||||||
|
principal,
|
||||||
|
totalAmount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
1
libs/ui/src/lib/fire-calculator/index.ts
Normal file
1
libs/ui/src/lib/fire-calculator/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './fire-calculator.module';
|
Loading…
x
Reference in New Issue
Block a user