Skip to content

Commit

Permalink
refactor: calculate periodic payment based on monthly (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
EricRibeiro authored Aug 25, 2022
1 parent 2db6147 commit 7d3b82e
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 85 deletions.
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"cSpell.words": [
"Annualize"
]
}
33 changes: 4 additions & 29 deletions src/interestRate.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { calcEffectiveInterestRate, calcPeriodicInterestRate, resolvePaymentRate } from "./interestRate";
import { calcEffectiveRate, calcMonthlyRate } from "./interestRate";

describe("Tests if the effective interest rate is calculated correctly", () => {
it("should calculate the effective rate based on the nominal rate and compounding frequency", () => {
const nominalInterestRate = 2.34;
const compoundingFrequency = 2;
const effectiveRate = calcEffectiveInterestRate(nominalInterestRate, compoundingFrequency);
const effectiveRate = calcEffectiveRate(nominalInterestRate, compoundingFrequency);

expect(effectiveRate).toBe(0.023536890000000144);
});
Expand All @@ -14,35 +14,10 @@ describe("Tests if the periodic interest rate is calculated correctly", () => {
it("should calculate the monthly rate based on effective rate", () => {
const nominalInterestRate = 2.34;
const compoundingFrequency = 2;
const paymentSchedule = 'monthly';

const paymentRate = resolvePaymentRate(paymentSchedule);
const effectiveRate = calcEffectiveInterestRate(nominalInterestRate, compoundingFrequency);
const periodicRate = calcPeriodicInterestRate(effectiveRate, paymentRate);
const effectiveRate = calcEffectiveRate(nominalInterestRate, compoundingFrequency);
const periodicRate = calcMonthlyRate(effectiveRate);

expect(periodicRate).toBe(0.0019405611613942941);
});
});

describe("Tests if the payment rate is resolved correctly", () => {
it("should calculate the payment rate based on monthly payment schedule", () => {
const paymentSchedule = 'monthly';
const paymentRate = resolvePaymentRate(paymentSchedule);

expect(paymentRate).toBe(0.08333333333333333);
});

it("should calculate the payment rate based on bi-weekly payment schedule", () => {
const paymentSchedule = 'bi-weekly';
const paymentRate = resolvePaymentRate(paymentSchedule);

expect(paymentRate).toBe(0.038461538461538464);
});

it("should calculate the payment rate based on accelerated bi-weekly payment schedule", () => {
const paymentSchedule = 'accelerated-bi-weekly';
const paymentRate = resolvePaymentRate(paymentSchedule);

expect(paymentRate).toBe(0.08333333333333333);
});
});
31 changes: 3 additions & 28 deletions src/interestRate.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,15 @@
export function calcEffectiveInterestRate(nominalInterestRate: number, compoundingFrequency: number): number {
export function calcEffectiveRate(nominalInterestRate: number, compoundingFrequency: number): number {
// Formula from: https://www.mikesukmanowsky.com/blog/a-guide-to-canadian-mortgage-calculations
const base = 1 + ((nominalInterestRate / 100) / compoundingFrequency);
const exponent = compoundingFrequency;
const effectiveInterestRate = Math.pow(base, exponent) - 1;

return effectiveInterestRate;
}

export function calcPeriodicInterestRate(effectiveInterestRate: number, paymentFrequency: number): number {
export function calcMonthlyRate(effectiveInterestRate: number): number {
// Formula from: https://www.mikesukmanowsky.com/blog/a-guide-to-canadian-mortgage-calculations
const base = 1 + effectiveInterestRate;
const exponent = paymentFrequency;
const exponent = 1/12;
const monthlyInterestRate = Math.pow(base, exponent) - 1;

return monthlyInterestRate;
}

export function resolvePaymentRate(paymentSchedule: string) : number {
switch (paymentSchedule.toLowerCase()) {
case "monthly":
return 1/12;
case "bi-weekly":
// In the bi-weekly schedule, 26 payments are made in a year
// This is expressed in the formula as 365/26. However, this is a periodic fraction.
// Some will approximate it to 14. And some will use the fraction as I'm using.
// Meaning that the result can be different from other calculators.
// The government's calculator yield a different result: https://itools-ioutils.fcac-acfc.gc.ca/MC-CH/MCCalc-CHCalc-eng.aspx
// While "mortgagecalculator.org" yields the same: https://www.mortgagecalculator.org/calcs/canadian.php
return (365/26)/365;
case "accelerated-bi-weekly":
return 1/12;
default:
throw new Error(JSON.stringify({
message: "The payment schedule is invalid.",
information: `The payment schedule must be one of the following: monthly, bi-weekly, accelerated-bi-weekly`,
code: 400
}));
}
}
2 changes: 1 addition & 1 deletion src/mortgage.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("Tests if the payment per payment schedule is calculated correctly", ()

const payment = calculate(propertyPrice, downPayment, nominalInterestRate, amortization, paymentSchedule);

expect(payment).toBe(970.25);
expect(payment).toBe(971.11);
});

it("should calculate payments over an accelerated bi-weekly schedule", () => {
Expand Down
50 changes: 23 additions & 27 deletions src/mortgage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
calcEffectiveInterestRate,
calcPeriodicInterestRate,
resolvePaymentRate
} from "./interestRate";
import { calcEffectiveRate, calcMonthlyRate } from "./interestRate";
import { validate } from "./validator";

export function calculate(
Expand All @@ -17,46 +13,46 @@ export function calculate(
if (errors.length > 0) return errors;

const compoundingFrequency = 2;
const principalLoanAmount = propertyPrice - downPayment;
const numberOfPayments = paymentSchedule === "bi-weekly" ? amortization * 26 : amortization * 12;
const principal = propertyPrice - downPayment;

const paymentRate = resolvePaymentRate(paymentSchedule);
const effectiveInterestRate = calcEffectiveInterestRate(interestRate, compoundingFrequency);
const periodicInterestRate = calcPeriodicInterestRate(effectiveInterestRate, paymentRate);

const paymentPerSchedule = paymentSchedule === "accelerated-bi-weekly"
? calcPaymentPerSchedule(periodicInterestRate, numberOfPayments, principalLoanAmount) / 2
: calcPaymentPerSchedule(periodicInterestRate, numberOfPayments, principalLoanAmount)
const effectiveRate = calcEffectiveRate(interestRate, compoundingFrequency);
const monthlyRate = calcMonthlyRate(effectiveRate);
const monthlyPayment = calcMonthlyPayment(monthlyRate, principal, amortization)
const paymentPerSchedule = calcPaymentPerSchedule(paymentSchedule, monthlyPayment);

const paymentPerScheduleFormatted = Number.parseFloat(paymentPerSchedule.toFixed(2));
return paymentPerScheduleFormatted;
}

export function calcPaymentPerSchedule(periodicInterestRate: number, numberOfPayments: number, principalLoanAmount: number): number {
function calcMonthlyPayment(monthlyRate: number, principalLoanAmount: number, amortization: number): number {
// Formula from: https://www.bankrate.com/mortgages/mortgage-calculator/#how-mortgage-calculator-help
const numerator = periodicInterestRate * Math.pow(1 + periodicInterestRate, numberOfPayments);
const denominator = Math.pow(1 + periodicInterestRate, numberOfPayments) - 1;
const paymentPerSchedule = (principalLoanAmount * numerator) / denominator;

return paymentPerSchedule;
const amortizationInMonths = amortization * 12;
const numerator = monthlyRate * principalLoanAmount
const denominator = 1 - Math.pow(1 + monthlyRate, -amortizationInMonths);
const monthlyPayment = numerator / denominator;
return monthlyPayment;
}

function calcPaymentPerSchedule(paymentSchedule: string, monthlyPayment: number): number {
// Annualize the payments and divide it by the number of payments per year
if (paymentSchedule === "bi-weekly") return (monthlyPayment * 12) / 26;
if (paymentSchedule === "monthly") return monthlyPayment;
if (paymentSchedule === "accelerated-bi-weekly") return monthlyPayment / 2;
throw new Error("Invalid payment schedule");
}

export function calcMinimumDownPayment(propertyPrice: number): number {
// Data from: https://www.canada.ca/en/financial-consumer-agency/services/mortgages/down-payment.html
let minimumDownPayment = 0;

// 5% of the purchase price
if (propertyPrice <= 500000)
minimumDownPayment = propertyPrice * 0.05;
return propertyPrice * 0.05;

// 5% of the first $500,000 of the purchase price
// 10% for the portion of the purchase price above $500,000
else if (propertyPrice > 500000 && propertyPrice <= 999999)
minimumDownPayment = 25000 + ((propertyPrice - 500000) * 0.1);
return 25000 + ((propertyPrice - 500000) * 0.1);

// 20% of the purchase price
else
minimumDownPayment = propertyPrice * 0.2;

return minimumDownPayment;
return propertyPrice * 0.2;
}

0 comments on commit 7d3b82e

Please sign in to comment.