Some checks failed
CI Pipeline (nottags) / security (push) Successful in 17s
CI Pipeline (tags) / security (push) Successful in 17s
CI Pipeline (nottags) / test (push) Failing after 52s
CI Pipeline (tags) / test (push) Failing after 50s
CI Pipeline (tags) / release (push) Has been skipped
CI Pipeline (tags) / metadata (push) Has been skipped
313 lines
9.7 KiB
TypeScript
313 lines
9.7 KiB
TypeScript
import * as plugins from './calculation.plugins.js';
|
|
import { Calculator } from './calculation.classes.calculator.js';
|
|
|
|
export interface IAmortizationPayment {
|
|
period: number;
|
|
payment: plugins.Decimal;
|
|
principal: plugins.Decimal;
|
|
interest: plugins.Decimal;
|
|
balance: plugins.Decimal;
|
|
cumulativePrincipal: plugins.Decimal;
|
|
cumulativeInterest: plugins.Decimal;
|
|
}
|
|
|
|
export interface IAmortizationSchedule {
|
|
payments: IAmortizationPayment[];
|
|
totalPayment: plugins.Decimal;
|
|
totalPrincipal: plugins.Decimal;
|
|
totalInterest: plugins.Decimal;
|
|
monthlyPayment: plugins.Decimal;
|
|
}
|
|
|
|
export interface ILoanOptions {
|
|
principal: plugins.Decimal.Value;
|
|
annualRate: plugins.Decimal.Value;
|
|
termYears?: plugins.Decimal.Value;
|
|
termMonths?: plugins.Decimal.Value;
|
|
paymentFrequency?: 'monthly' | 'biweekly' | 'weekly';
|
|
extraPayment?: plugins.Decimal.Value;
|
|
}
|
|
|
|
/**
|
|
* Amortization calculations class for loan schedules and payment analysis
|
|
*/
|
|
export class Amortization extends Calculator {
|
|
constructor() {
|
|
super({ precision: 15 });
|
|
}
|
|
|
|
/**
|
|
* Generate a complete amortization schedule
|
|
* @param options Loan options
|
|
*/
|
|
public schedule(options: ILoanOptions): IAmortizationSchedule {
|
|
const principal = this.decimal(options.principal);
|
|
const annualRate = this.decimal(options.annualRate);
|
|
const termMonths = this.calculateTermMonths(options);
|
|
const extraPayment = options.extraPayment ? this.decimal(options.extraPayment) : this.decimal(0);
|
|
|
|
const monthlyRate = this.divide(annualRate, 12);
|
|
const monthlyPayment = this.calculatePayment(principal, monthlyRate, termMonths);
|
|
|
|
const payments: IAmortizationPayment[] = [];
|
|
let balance = principal;
|
|
let cumulativePrincipal = this.decimal(0);
|
|
let cumulativeInterest = this.decimal(0);
|
|
|
|
for (let period = 1; period <= termMonths.toNumber() && balance.greaterThan(0); period++) {
|
|
const interestPayment = this.multiply(balance, monthlyRate);
|
|
const scheduledPrincipal = this.subtract(monthlyPayment, interestPayment);
|
|
|
|
// Apply extra payment to principal
|
|
const totalPrincipal = this.add(scheduledPrincipal, extraPayment);
|
|
|
|
// Ensure we don't pay more than the remaining balance
|
|
const actualPrincipal = this.min(totalPrincipal, balance);
|
|
const actualPayment = this.add(interestPayment, actualPrincipal);
|
|
|
|
balance = this.subtract(balance, actualPrincipal);
|
|
cumulativePrincipal = this.add(cumulativePrincipal, actualPrincipal);
|
|
cumulativeInterest = this.add(cumulativeInterest, interestPayment);
|
|
|
|
payments.push({
|
|
period,
|
|
payment: actualPayment,
|
|
principal: actualPrincipal,
|
|
interest: interestPayment,
|
|
balance: balance,
|
|
cumulativePrincipal: cumulativePrincipal,
|
|
cumulativeInterest: cumulativeInterest
|
|
});
|
|
|
|
// If balance is paid off early, stop
|
|
if (balance.isZero()) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
const totalPayment = payments.reduce((sum, p) => this.add(sum, p.payment), this.decimal(0));
|
|
const totalPrincipal = cumulativePrincipal;
|
|
const totalInterest = cumulativeInterest;
|
|
|
|
return {
|
|
payments,
|
|
totalPayment,
|
|
totalPrincipal,
|
|
totalInterest,
|
|
monthlyPayment
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Calculate monthly payment for a loan
|
|
*/
|
|
private calculatePayment(
|
|
principal: plugins.Decimal,
|
|
monthlyRate: plugins.Decimal,
|
|
termMonths: plugins.Decimal
|
|
): plugins.Decimal {
|
|
if (monthlyRate.isZero()) {
|
|
return this.divide(principal, termMonths);
|
|
}
|
|
|
|
const numerator = this.multiply(principal, monthlyRate);
|
|
const denominator = this.subtract(
|
|
1,
|
|
this.power(this.add(1, monthlyRate), this.multiply(-1, termMonths))
|
|
);
|
|
|
|
return this.divide(numerator, denominator);
|
|
}
|
|
|
|
/**
|
|
* Calculate total term in months
|
|
*/
|
|
private calculateTermMonths(options: ILoanOptions): plugins.Decimal {
|
|
if (options.termMonths) {
|
|
return this.decimal(options.termMonths);
|
|
} else if (options.termYears) {
|
|
return this.multiply(this.decimal(options.termYears), 12);
|
|
} else {
|
|
throw new Error('Either termYears or termMonths must be specified');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate remaining balance after a specific number of payments
|
|
* @param principal Original loan amount
|
|
* @param annualRate Annual interest rate
|
|
* @param termMonths Total term in months
|
|
* @param paymentsMade Number of payments made
|
|
*/
|
|
public remainingBalance(
|
|
principal: plugins.Decimal.Value,
|
|
annualRate: plugins.Decimal.Value,
|
|
termMonths: plugins.Decimal.Value,
|
|
paymentsMade: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const p = this.decimal(principal);
|
|
const r = this.divide(this.decimal(annualRate), 12);
|
|
const n = this.decimal(termMonths);
|
|
const m = this.decimal(paymentsMade);
|
|
|
|
if (r.isZero()) {
|
|
const paymentAmount = this.divide(p, n);
|
|
const paidAmount = this.multiply(paymentAmount, m);
|
|
return this.subtract(p, paidAmount);
|
|
}
|
|
|
|
const onePlusR = this.add(1, r);
|
|
const factor1 = this.power(onePlusR, n);
|
|
const factor2 = this.power(onePlusR, m);
|
|
|
|
const numerator = this.multiply(p, this.subtract(factor1, factor2));
|
|
const denominator = this.subtract(factor1, 1);
|
|
|
|
return this.divide(numerator, denominator);
|
|
}
|
|
|
|
/**
|
|
* Calculate interest paid over a specific period
|
|
* @param schedule The amortization schedule
|
|
* @param startPeriod Start period (inclusive)
|
|
* @param endPeriod End period (inclusive)
|
|
*/
|
|
public interestForPeriod(
|
|
schedule: IAmortizationSchedule,
|
|
startPeriod: number,
|
|
endPeriod: number
|
|
): plugins.Decimal {
|
|
return schedule.payments
|
|
.filter(p => p.period >= startPeriod && p.period <= endPeriod)
|
|
.reduce((sum, p) => this.add(sum, p.interest), this.decimal(0));
|
|
}
|
|
|
|
/**
|
|
* Calculate principal paid over a specific period
|
|
* @param schedule The amortization schedule
|
|
* @param startPeriod Start period (inclusive)
|
|
* @param endPeriod End period (inclusive)
|
|
*/
|
|
public principalForPeriod(
|
|
schedule: IAmortizationSchedule,
|
|
startPeriod: number,
|
|
endPeriod: number
|
|
): plugins.Decimal {
|
|
return schedule.payments
|
|
.filter(p => p.period >= startPeriod && p.period <= endPeriod)
|
|
.reduce((sum, p) => this.add(sum, p.principal), this.decimal(0));
|
|
}
|
|
|
|
/**
|
|
* Calculate the payoff date with extra payments
|
|
* @param options Loan options with extra payment
|
|
*/
|
|
public payoffDate(options: ILoanOptions, startDate: Date = new Date()): Date {
|
|
const schedule = this.schedule(options);
|
|
const lastPayment = schedule.payments[schedule.payments.length - 1];
|
|
|
|
const monthsToPayoff = lastPayment.period;
|
|
const payoffDate = new Date(startDate);
|
|
payoffDate.setMonth(payoffDate.getMonth() + monthsToPayoff);
|
|
|
|
return payoffDate;
|
|
}
|
|
|
|
/**
|
|
* Calculate interest savings with extra payments
|
|
* @param regularOptions Loan options without extra payment
|
|
* @param extraPaymentOptions Loan options with extra payment
|
|
*/
|
|
public interestSavings(
|
|
regularOptions: ILoanOptions,
|
|
extraPaymentOptions: ILoanOptions
|
|
): plugins.Decimal {
|
|
const regularSchedule = this.schedule(regularOptions);
|
|
const extraSchedule = this.schedule(extraPaymentOptions);
|
|
|
|
return this.subtract(regularSchedule.totalInterest, extraSchedule.totalInterest);
|
|
}
|
|
|
|
/**
|
|
* Calculate biweekly payment amount (26 payments per year)
|
|
* @param monthlyPayment The regular monthly payment
|
|
*/
|
|
public biweeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal {
|
|
return this.divide(this.decimal(monthlyPayment), 2);
|
|
}
|
|
|
|
/**
|
|
* Calculate weekly payment amount (52 payments per year)
|
|
* @param monthlyPayment The regular monthly payment
|
|
*/
|
|
public weeklyPayment(monthlyPayment: plugins.Decimal.Value): plugins.Decimal {
|
|
const monthly = this.decimal(monthlyPayment);
|
|
const yearlyPayment = this.multiply(monthly, 12);
|
|
return this.divide(yearlyPayment, 52);
|
|
}
|
|
|
|
/**
|
|
* Calculate the maximum loan amount based on payment capacity
|
|
* @param maxPayment Maximum monthly payment
|
|
* @param annualRate Annual interest rate
|
|
* @param termMonths Term in months
|
|
*/
|
|
public maxLoanAmount(
|
|
maxPayment: plugins.Decimal.Value,
|
|
annualRate: plugins.Decimal.Value,
|
|
termMonths: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const payment = this.decimal(maxPayment);
|
|
const r = this.divide(this.decimal(annualRate), 12);
|
|
const n = this.decimal(termMonths);
|
|
|
|
if (r.isZero()) {
|
|
return this.multiply(payment, n);
|
|
}
|
|
|
|
const onePlusR = this.add(1, r);
|
|
const factor = this.power(onePlusR, this.multiply(-1, n));
|
|
const numerator = this.subtract(1, factor);
|
|
const denominator = this.divide(numerator, r);
|
|
|
|
return this.multiply(payment, denominator);
|
|
}
|
|
|
|
/**
|
|
* Calculate loan-to-value ratio (LTV)
|
|
* @param loanAmount The loan amount
|
|
* @param propertyValue The property value
|
|
*/
|
|
public ltv(
|
|
loanAmount: plugins.Decimal.Value,
|
|
propertyValue: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const loan = this.decimal(loanAmount);
|
|
const value = this.decimal(propertyValue);
|
|
|
|
if (value.isZero()) {
|
|
throw new Error('Property value cannot be zero');
|
|
}
|
|
|
|
return this.divide(loan, value);
|
|
}
|
|
|
|
/**
|
|
* Calculate debt-to-income ratio (DTI)
|
|
* @param monthlyDebtPayments Total monthly debt payments
|
|
* @param monthlyIncome Monthly gross income
|
|
*/
|
|
public dti(
|
|
monthlyDebtPayments: plugins.Decimal.Value,
|
|
monthlyIncome: plugins.Decimal.Value
|
|
): plugins.Decimal {
|
|
const debt = this.decimal(monthlyDebtPayments);
|
|
const income = this.decimal(monthlyIncome);
|
|
|
|
if (income.isZero()) {
|
|
throw new Error('Monthly income cannot be zero');
|
|
}
|
|
|
|
return this.divide(debt, income);
|
|
}
|
|
} |