Files
calculation/ts/calculation.classes.amortization.ts

313 lines
9.7 KiB
TypeScript
Raw Normal View History

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);
}
}