reptile7's JavaScript blog
Thursday, December 24, 2015
Adventures in Amortization, Part 4
Blog Entry #356

In the last two entries, we've used the Loan Amount script's CalcPayment( ), CalcLoanAmt( ), and CalcPeriod( ) functions to calculate an amortizing loan's monthly payment (Payment), principal (LoanAmt), and period in months (Period), respectively.

The script also features a corresponding CalcRate( ) function for calculating an amortizing loan's yearly interest rate. The CalcRate( ) function is called by clicking the Rate field's button, and it preliminarily gets and vets an inputted principal, period in months, and monthly payment à la the CalcPayment( )/CalcLoanAmt( )/CalcPeriod( ) functions - so far, so normal. Unfortunately, the amortization formula we've been using

Payment = LoanAmt * Rate * Math.pow(1 + Rate, Period) / (Math.pow(1 + Rate, Period) - 1);

cannot be solved explicitly for the monthly interest Rate; consequently, the CalcRate( ) function determines the Rate via an iterative numerical method that incorporates elements of successive substitution and a binary search.

var TempPayment, Increment; TempPayment = -9999; Rate = 50 / 1200; // Yearly rate of 50% Increment = Rate / 2; with (Math) { while (abs(Payment - TempPayment) > 0.005) { TempPayment = LoanAmt * Rate * pow(1 + Rate, Period) / (pow(1 + Rate, Period) - 1); if (TempPayment > Payment) { Rate -= Increment; } else if (TempPayment < Payment) { Rate += Increment; } Increment /= 2; } } form.Rate.value = (parseInt(Rate * 1200000) / 1000).toString( );

With LoanAmt/Period/Payment data in hand, CalcRate( ) guesses the yearly percentage rate to be 50, which is divided by 1200 to give an initial Rate. It zeroes in on the actual Rate by iteratively
(a) decreasing or increasing the Rate by a shrinking Increment after
(b) comparing the Payment to a transitional TempPayment.
The TempPayment and Increment are initialized to -9999 and Rate / 2, respectively.

A while loop takes us from the initial Rate to the actual Rate. The while condition tests if the | Payment - TempPayment | separation is greater than 0.005; the abs( ) method of the Math object is documented here. If the condition comparison returns true:
(1) The LoanAmt, Rate, and Period are plugged into the amortization formula to give a new TempPayment.
(2) If the new TempPayment is greater than the Payment, then the Rate is too high and is therefore decreased by the Increment; vice versa if the TempPayment is less than the Payment.
(3) The Increment is halved.
Steps (1)-(3) repeat until the condition is false.

The TempPayment initialization is unnecessary if we recast the while loop as a do...while loop:

do { /* ...Loop body statements... */ }
while (abs(Payment - TempPayment) > 0.005)

Lastly, the actual Rate is converted to a yearly rate, specifically,
(i) Rate is multiplied by 1200000,
(ii) the resulting product is parseInt( )ed, and
(iii) the parseInt( ) return is divided by 1000;
the * 1200000 / 1000 scaling causes the yearly rate to have three or fewer post-decimal point digits. The yearly rate is
(iv) unnecessarily toString( )ed and
(v) subsequently displayed in the Rate field.

Zero and below

It can be shown that the Payment approaches LoanAmt / Period as the Rate approaches 0.
(Thanks to mathbff and her "How to Find Any Limit" video - in particular the Open Up Parentheses (Expand Then Simplify) section starting at 12:01 - for help in this regard.)

(1) On the right-hand side (RHS) of the amortization equation, expand the (1 + Rate)Period terms.

Payment = LoanAmt * Rate * (1 + Period*Rate + Period*Rate2 + ... + RatePeriod) / ((1 + Period*Rate + Period*Rate2 + ... + RatePeriod) - 1)

(2) In the RHS denominator, carry out the 1 - 1 subtraction and then factor out the Rate.

Payment = LoanAmt * Rate * (1 + Period*Rate + Period*Rate2 + ... + RatePeriod) / Rate * (Period + Period*Rate + ... + RatePeriod-1)

(3) Re the RHS numerator/denominator, cancel the outer Rate factors and then plug 0 into the remaining Rate terms, and you're there.

The CalcRate( ) function does not flag Payments that are at or below the LoanAmt / Period threshold: in these cases, the while loop
(a) continues to push the TempPayment toward the Payment and
(b) pushes the Rate closer and closer to 0 as the TempPayment reaches/crosses the threshold.
Another complication arises when converting a near-0 Rate to a yearly rate: parseInt( ) stops flooring an number to 0 and instead extracts the x part thereof if z is 7 or higher, e.g., parseInt(7.673982976852402e-10) returns 7 - this strange result occurs with all of the OS X browsers on my computer.

We can intercept too-low Payments and the erroneous rates they produce via the conditional below:

// Place this code just before the while or do...while loop: var minPayment = LoanAmt / Period; if (Payment <= minPayment) { Payment = minPayment != Math.ceil(minPayment) ? Math.ceil(minPayment) : minPayment + 1; window.alert("Your payment must be at least $" + Payment + " to pay off the loan by the end of the given period."); return; }

We used an analogous snippet to flag CalcPeriod( ) Payments that are at or below LoanAmt * Rate - see the You bet your loan life section of the previous post.

Toward the infinite loop, part 2

If there is too much distance between the initial Rate and the actual Rate, then the Increment eventually becomes too small to push the TempPayment within 0.005 of the Payment, and consequently the loop runs indefinitely.

If we borrow $1000 and repay it in twelve monthly installments, then our minPayment is 83.33. In this case, the original CalcRate( ) function will accept a PaymentAmt.value as low as 55.56, which effectively corresponds to a negative interest rate, but anything lower than that causes the browser to hang; the browser similarly hangs if the Payment is any higher than 135, which corresponds to a 100% yearly interest rate, even though there is in theory no upper limit on what an interest rate can be. (>719,000%, anyone?)
FWIW: I've also found the Loan Amount calculator here, here, and here, and in fairness to Dave and Joe, none of these other guys sorts this problem out.

The Payment <= minPayment conditional puts the kibosh on the Rate0 situation but that still leaves us the high-rate situation to deal with.

I track down a Missing Term Loan Calculator (MTLC) that satisfactorily calculates >100% interest rates. Created by Dan Peterson in 2010, the MTLC - specifically, the cir( ) function thereof - determines an interest rate in much the same way that the Loan Amount script does, namely, it
(a) takes an initial guess at the rate (myDecRate), and then
(b) calculates a temporary payment (myNewPmtAmt) by plugging an inputted principal (myPrin), an inputted number of payments (myNumPmts), and myDecRate into the amortization equation, and then
(c) iteratively advances myDecRate toward the actual rate and myNewPmtAmt toward an inputted payment (myPmtAmt) via a series of shrinking increments.

Significantly, the Δ(myDecRate) increments don't go any lower than 0.000001 / 12 - that's one ten-thousandth of a percent, APR-wise. I go back to the CalcRate( ) function and recast the Increment /= 2; assignment as a

if (Increment > 0.000001 / 12) Increment /= 2;

conditional to see if similarly bounding the Increment staves off the generation of infinite loops as described above. It does!

With the preceding statement in place, the CalcRate( ) function nevertheless requires many more loop iterations to zero in on a >100% interest rate than the cir( ) function does, and it is tempting to import some of the latter into the former. But do we really want to be accommodating such high interest rates in the first place? I like the idea of intercepting them with:

// This would also go just before the loop: var Rate100 = 100 / 1200; var Payment100 = LoanAmt * Rate100 * Math.pow(1 + Rate100, Period) / (Math.pow(1 + Rate100, Period) - 1); if (Payment >= Payment100) { window.alert("Your yearly interest rate is going to be \u2265100%.\nMaybe you should seek out a better lender."); return; }

\u2265 is the Unicode escape sequence for a ≥ symbol.

We'll discuss the script's remaining functions, ShowAmoritazation( ) and ValToMoney( ), in the following entry.

Comments: Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)