From 5f9020016a5f1be28b7673ddc2d5f3db8c68ee3f Mon Sep 17 00:00:00 2001 From: thetedmunds Date: Mon, 15 Apr 2019 14:31:23 -0700 Subject: [PATCH] Amended commit to address pull-request comments. --- libgnucash/app-utils/fin.scm | 168 +++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/libgnucash/app-utils/fin.scm b/libgnucash/app-utils/fin.scm index be56e543fd..c3ee43ce61 100644 --- a/libgnucash/app-utils/fin.scm +++ b/libgnucash/app-utils/fin.scm @@ -185,3 +185,171 @@ ) ) ) + +;; Further options to match what some (several? many?) lenders do (at +;; least in Canada): +;; The posted interest rate is an annual rate that has a specified +;; compounding frequency per year (2 for mortgages in Canada). +;; A payment frequency and amortization length are selected (e.g. +;; monthly payments for 25 years). +;; The posted nominal rate is converted from the specified compounding +;; frequency to the equivalent rate at the payment frequency. +;; The required payment is calculated. +;; The payment is rounded up to the next dollar (or $10 dollars, +;; or whatever...) +;; Each payment period, interest is calculated on the outstanding +;; balance. +;; The interest is rounded to the nearest cent and added to the +;; balance. +;; The payment is subtracted from the balance. +;; The final payment will be smaller because all the other payments +;; were rounded up. +;; +;; For the purpose of creating scheduled transactions that properly +;; debit a source account while crediting the loan account and the +;; interest expense account, the first part (the calculation of the +;; required payment) doesn't really matter. You have agreed +;; with the lender what the payment terms (interest rate, payment +;; frequency, payment amount) will be; you keep paying until the +;; balance is zero. +;; +;; To create the scheduled transactions, we need to build an +;; amortization table. +;; If it weren't for the rounding of the interest to the nearest cent +;; each period, we could calculate the ith row of the amortization +;; table directly from the general annuity equation (as is done by +;; gnc:ipmt and gnc:ppmt). But to deal with the intermediate +;; rounding, the amortization table has to be constructed iteratively +;; (as is done by the AMORT worksheet on the TI BA II Plus +;; financial calculator). +;; +;; ================================= +;; EXAMPLE: +;; Say you borrow $100,000 at 5%/yr, compounded semi-annually. +;; You amortize the loan over 2 years with 24 monthly payments. +;; This calls for payments of $4,384.8418 at the end of each month. +;; The lender rounds this up to $4,385. +;; +;; If you calculate the balance at each period directly using the annuity +;; formula (like calc-principal does), and then use the those values to calculate +;; the principal and interest paid, the first 10 rows of the amortization table +;; look like this (the values are rounded to the nearest cent for _display_, but +;; not for calculating the next period): +;; +;; PERIOD | Open | Interest | Principal | End +;; 1 |$100,000.00 | $412.39 | $3,972.61 | $96,027.39 +;; 2 | $96,027.39 | $396.01 | $3,988.99 | $92,038.40 +;; 3 | $92,038.40 | $379.56 | $4,005.44 | $88,032.96 +;; 4 | $88,032.96 | $363.04 | $4,021.96 | $84,011.00 +;; 5 | $84,011.00 | $346.45 | $4,038.55 | $79,972.45 +;; 6 | $79,972.45 | $329.80 | $4,055.20 | $75,917.25 +;; 7 | $75,917.25 | $313.08 | $4,071.92 | $71,845.33 +;; 8 | $71,845.33 | $296.28 | $4,088.72 | $67,756.61 +;; 9 | $67,756.61 | $279.43 | $4,105.57 | $63,651.04 +;; 10 | $63,651.04 | $262.49 | $4,122.51 | $59,528.53 +;; +;; If you calculate each period sequentially (rounding the interest and balance +;; at each step), you get: +;; +;; PERIOD | Open | Interest | Principal | End +;; 1 |$100,000.00 | $412.39 | $3,972.61 | $96,027.39 +;; 2 | $96,027.39 | $396.01 | $3,988.99 | $92,038.40 +;; 3 | $92,038.40 | $379.56 | $4,005.44 | $88,032.96 +;; 4 | $88,032.96 | $363.04 | $4,021.96 | $84,011.00 +;; 5 | $84,011.00 | $346.45 | $4,038.55 | $79,972.45 +;; 6 | $79,972.45 | $329.80 | $4,055.20 | $75,917.25 +;; 7 | $75,917.25 | $313.08 | $4,071.92 | $71,845.33 +;; 8 | $71,845.33 | $296.28 | $4,088.72 | $67,756.61 +;; 9 | $67,756.61 | $279.42 | $4,105.58 | $63,651.03 <- Different +;; 10 | $63,651.03 | $262.49 | $4,122.51 | $59,528.52 <- still $0.01 off +;; +;; ================================= +;; +;; For the following functions the argument names are: +;; py: payment frequency (number of payments per year) +;; cy: compounding frequency of the nominal rate (per year) +;; iy: nominal annual interest rate +;; pv: the present value (opening balance) +;; pmt: the size of the periodic payment +;; n: the payment period we are asking about (the first payment is n=1) +;; places: number of decimal places to round the interest amount to +;; at each payment (999 does no rounding) +;; +;; Note: only ordinary annuities are supported (payments at the end of +;; each period, not at the beginning of each period) +;; +;; Unlike the AMORT worksheet on the BA II Plus, these methods will +;; handle the smaller payment (bringing the balance to zero, then +;; zeroing future payments) +;; +;; The present value (pv) must be non-negative. If not, the balance will be +;; treated as 0. +;; The payment (pmt) can be positive (paying interest, and hopefully +;; reducing the balance each payment), or negative (increasing the balance +; each payment). +;; The payment number (n) must be positive for amort_pmt, amort_ppmt, and +;; amort_ipmt. I.e., the first payment is payment 1. +;; The payment number (n) must be non-negative for amort_balance. (In this +;; case, payment zero is at the _beginning_ of the first period, so +;; amort_balance will just be the initial balance.) +;; If the above conditions on n are violated, the functions return -1 (#f is +;; not used, because it causes gnucash to crash). +;; +;; A negative interest rate works (if you can find a lender who charges +;; negative rates), but negative compounding frequency, or negative payment +;; frequency is a bad idea. + +;; Calculate the balance remaining after the nth payment +;; (n must be greater than or equal to zero) +(define (gnc:amort_pmt py cy iy pv pmt n places) + (if (< n 1) -1 ;; Returning #f here causes gnucash to crash on startup + (let* ((prevBal (gnc:amort_balance py cy iy pv pmt (- n 1) places)) + (balBeforePayment + (amort_balanceAfterInterest prevBal py cy iy places)) + (balAfterPayment (amort_balanceAfterPayment balBeforePayment pmt))) + (- balBeforePayment balAfterPayment)))) + +;; Calculate the amount of the nth payment that is principal +;; (n must be greater than zero) +(define (gnc:amort_ppmt py cy iy pv pmt n places) + (if (< n 1) -1 + (let* ((prevBal (gnc:amort_balance py cy iy pv pmt (- n 1) places)) + (bal-after-int (amort_balanceAfterInterest prevBal py cy iy places)) + (newBal (amort_balanceAfterPayment bal-after-int pmt))) + (- prevBal newBal)))) + +;; Calculate the amount of the nth payment that is interest +;; (n must be greater than zero) +(define (gnc:amort_ipmt py cy iy pv pmt n places) + (if (< n 1) -1 + (let* ((prevBal(gnc:amort_balance py cy iy pv pmt (- n 1) places))) + (amort_interest prevBal py cy iy places)))) + +;; "Private" helper functions: + +;; Calculate the amount of interest on the current balance, +;; rounded to the specified number of decimal places +(define (amort_interest balance py cy iy places) + (roundToPlaces (* balance (gnc:periodic_rate iy py cy)) places) +) + +;; Calculate the new balance after applying the interest, but before +;; applying the payment +(define (amort_balanceAfterInterest prevBalance py cy iy places) + (+ prevBalance (amort_interest prevBalance py cy iy places)) +) + +;; Apply the payment to the balance (after the interest has been +;; added), without letting the balance go below zero. +(define (amort_balanceAfterPayment balanceBeforePmt pmt) + (max 0 (- balanceBeforePmt pmt)) +) + +;; Round the value to the specified number of decimal places. +;; 999 places means no rounding (#f is not used, because only numbers can be +;; entered in the scheduled transaction editor) +(define (roundToPlaces value places) + (if (= places 999) value + (/ (round (* value (expt 10 places))) (expt 10 places)) + ) +)