Fortran Finance
Loan Amortization
| Author: | Mitch Richling |
| Updated: | 2025-11-06 12:39:08 |
| Generated: | 2025-11-06 12:39:10 |
Copyright © 2025 Mitch Richling. All rights reserved.
Table of Contents
1. loan_level_payments.f90
The typical approach to solving loan problems is to equate the present value of the principal with the present value of the payment cashflow stream in a single equation and solve for the payment. With only one principal payment occurring at the beginning and a payment stream consisting of a level annuity certain, this is pretty simple. Unfortunately things get much more complex if the payment structure (for principal or repayment) changes from this baseline. These more difficult situations can be easily handled by considering the problem as multiple cashflow streams. In order to illustrate how to approach such problems using a multiple cashflow methodology, this program applies the technique to solve the familiar problem of the simple loan.
We can model a loan as a pair of parallel cashflows:
- Money paid by the lender (i.e. the principal) – a negative cash flow from the lender's perspective
- Money paid to the lender (i.e. loan payments) – a positive cash flow from the lender's perspective
These two cash flows should have equal magnitude PV & FV, but of opposite sign.
Here is the Fortran code (loan_level_payments.f90):
program loan_level_payments use :: mrffl_config, only: rk use :: mrffl_cashflows, only: make_cashflow_vector_delayed_lump, make_cashflow_vector_delayed_level_annuity, cashflow_matrix_cmp use :: mrffl_prt_sets, only: prt_ALL, prt_all_agg_sum use :: mrffl_tvm, only: tvm_lump_sum_solve, tvm_delayed_level_annuity_solve use :: mrffl_var_sets, only: var_fv, var_a implicit none (type, external) integer, parameter :: periods = 10 real(kind=rk) :: n = periods real(kind=rk) :: i = 7 real(kind=rk) :: a_pv, a_fv, a_a, p_pv, p_fv integer :: a_d, a_e, p_d integer :: status real(kind=rk) :: cfm(periods+1,2), afv(periods+1), apv(periods+1), fv(periods+1,2), pv(periods+1,2) ! First, we print out the term and interest. print "(a)", repeat("=", 141) print "(a60,f15.4)", "n: ", n print "(a60,f15.4)", "i: ", i ! Now we compute the PV & FV of the principal cashflow. print "(a)", repeat("=", 141) p_pv = -1000000 p_d = 0 call tvm_lump_sum_solve(n, i, p_pv, p_fv, var_fv, status) print "(a60,i15)", "tvm_lump_sum_solve status: ", status print "(a60,f15.4)", "Loan PV: ", p_pv print "(a60,f15.4)", "Loan FV: ", p_fv print "(a)", repeat("=", 141) ! Now we compute the PV & FV of the repayment cashflow stream. a_pv = -p_pv a_d = 1 a_e = 0 call tvm_delayed_level_annuity_solve(n, i, a_pv, a_fv, a_a, a_d, a_e, var_fv+var_a, status) print "(a60,i15)", "tvm_level_annuity_solve status: ", status print "(a60,f15.4)", "Annuity PV: ", a_pv print "(a60,f15.4)", "Annuity FV: ", a_fv print "(a60,f15.4)", "Annuity A: ", a_a ! Just for fun, let's create cashflow sequences for everything, and print it out. ! First the principal cashflow. Note we use the variables we used with the TVM solver. print "(a)", repeat("=", 141) call make_cashflow_vector_delayed_lump(cfm(:,1), p_pv, p_d, status) print "(a60,i15)", "make_cashflow_vector_delayed_lump status: ", status ! Next the repayment cashflow. Note we use the variables we used with the TVM solver. print "(a)", repeat("=", 141) call make_cashflow_vector_delayed_level_annuity(cfm(:,2), a_a, a_d, a_e, status) print "(a60,i15)", "make_cashflow_vector_delayed_level_annuity status: ", status ! Now we print it out. Notice the PV & FV of the combined cashflow series is zero. print "(a)", repeat("=", 141) call cashflow_matrix_cmp(status, cfm, i, pv_agg_o=apv, fv_agg_o=afv) print "(a60,i15)", "cashflow_matrix_cmp status: ", status print "(a60,f15.4)", "cashflow_matrix_cmp Sum: ", sum(cfm) print "(a60,f15.4)", "cashflow_matrix_cmp PV Sum: ", sum(apv) print "(a60,f15.4)", "cashflow_matrix_cmp FV Sum: ", sum(afv) print "(a)", repeat("=", 141) call cashflow_matrix_cmp(status, cfm, i, pv_o=pv, fv_o=fv, pv_agg_o=apv, fv_agg_o=afv, prt_o=prt_ALL-prt_all_agg_sum) print "(a)", repeat("=", 141) end program loan_level_payments
2. loan_geometric_payments.f90
This program extends the example from loan_level_payments.f90 to geometric payments. Not much changes in the flow except the annuity type.
If you are curious about how such a loan might come about, then consider the following scenario:
A business needs a 1M load. They wish to make annual payments, and to pay down the loan as quickly as possible. At the end of the year they can afford to pay 95K. The business has been experiencing 11% revenue growth for the last 5 years with projections showing that to continue. Based on growth projections, they wish to increase loan payments by 10% per year. We wish to extend them the loan, and make 7%.
Here is the Fortran code (loan_geometric_payments.f90):
program loan_geometric_payments use :: mrffl_config, only: rk use :: mrffl_cashflows, only: make_cashflow_vector_delayed_lump, make_cashflow_vector_delayed_geometric_annuity, cashflow_matrix_cmp use :: mrffl_prt_sets, only: prt_ALL, prt_all_agg_sum use :: mrffl_tvm, only: tvm_delayed_geometric_annuity_solve, tvm_lump_sum_solve use :: mrffl_var_sets, only: var_fv, var_a, var_n implicit none (type, external) real(kind=rk) :: n real(kind=rk) :: i = 7 real(kind=rk) :: a_pv = 1000000 real(kind=rk) :: a_fv real(kind=rk) :: a_g = 10 real(kind=rk) :: a_a = 95000 integer :: a_d = 1 integer :: a_e = 0 real(kind=rk) :: p_pv = -1000000 real(kind=rk) :: p_fv integer :: status real(kind=rk), allocatable :: cfm(:,:), afv(:), apv(:), fv(:,:), pv(:,:) ! First we solve for the number of years and future value of our loan. print "(a)", repeat("=", 141) call tvm_delayed_geometric_annuity_solve(n, i, a_g, a_pv, a_fv, a_a, a_d, a_e, var_n+var_fv, status) print "(a60,i15)", "tvm_level_annuity_solve status: ", status print "(a60,f15.4)", "Annuity n: ", n print "(a60,f15.4)", "Annuity FV: ", a_fv print "(a)", "From this result we know the loan term needs to be just about 10 years." ! Instead of using an odd term, we decide on an even 10 year term. ! So we must copute the initial loan payment, and loan FV print "(a)", repeat("=", 141) n = ceiling(n) call tvm_delayed_geometric_annuity_solve(n, i, a_g, a_pv, a_fv, a_a, a_d, a_e, var_a+var_fv, status) print "(a60,i15)", "tvm_level_annuity_solve status: ", status print "(a60,f15.4)", "Annuity a: ", a_a print "(a60,f15.4)", "Annuity FV: ", a_fv ! Next we check our work by solving for the FV of the lump sum. print "(a)", repeat("=", 141) call tvm_lump_sum_solve(n, i, p_pv, p_fv, var_fv, status) print "(a60,i15)", "tvm_lump_sum_solve status: ", status print "(a60,f15.4)", "Loan FV: ", p_fv ! Allocate space for our cashflow matrix and the aggregate PV/FV vectors. We don't check for allocation errors. ;) allocate(cfm(nint(n)+1,2)) allocate(afv(nint(n)+1)) allocate(apv(nint(n)+1)) ! Now we populate a cashflow matrix with our two cashflows. print "(a)", repeat("=", 141) call make_cashflow_vector_delayed_lump(cfm(:,1), p_pv, 0, status) print "(a60,i15)", "make_cashflow_vector_delayed_lump status: ", status call make_cashflow_vector_delayed_geometric_annuity(cfm(:,2), a_g, a_a, a_d, a_e, status) print "(a60,i15)", "make_cashflow_vector_delayed_level_annuity status: ", status ! We can check our work by making sure our cashflows sum to zero. print "(a)", repeat("=", 141) call cashflow_matrix_cmp(status, cfm, i, pv_agg_o=apv, fv_agg_o=afv) print "(a60,i15)", "cashflow_matrix_cmp status: ", status print "(a60,f15.4)", "cashflow_matrix_cmp PV Sum: ", sum(apv) print "(a60,f15.4)", "cashflow_matrix_cmp FV Sum: ", sum(afv) ! Finally we an print out our cashflows. allocate(pv(nint(n)+1,2)) allocate(fv(nint(n)+1,2)) print "(a)", repeat("=", 141) call cashflow_matrix_cmp(status, cfm, i, pv_o=pv, fv_o=fv, pv_agg_o=apv, fv_agg_o=afv, prt_o=prt_ALL-prt_all_agg_sum) print "(a)", repeat("=", 141) end program loan_geometric_payments
3. loan_up_down_payments.f90
This program extends the examples from loan_level_payments.f90 and loan_geometric_payments.f90 to a unequal, non-standard annuity designed to make payments round cent values.
One way to amortize a loan is to round all payments but the last one up to the nearest penny, and then adjust the last payment lower to accommodate the difference – this last payment is rounded DOWN to the nearest cent. This insures that the lender will not loose more than a fractional cent on the entire transaction, and the borrower don't pay more than the agreed upon rate (this second condition is required by law in many jurisdictions). Note this method is unsuitable for very long term loans as it may shorten the overall term.
Here is the Fortran code (loan_up_down_payments.f90):
program loan_level_payments use :: mrffl_config, only: rk use :: mrffl_cashflows, only: make_cashflow_vector_delayed_lump, make_cashflow_vector_delayed_level_annuity, cashflow_matrix_cmp use :: mrffl_prt_sets, only: prt_ALL, prt_all_agg_sum use :: mrffl_tvm, only: tvm_lump_sum_solve, tvm_delayed_level_annuity_solve use :: mrffl_var_sets, only: var_fv, var_a, var_pv implicit none (type, external) integer, parameter :: periods = 10 real(kind=rk) :: n = periods real(kind=rk) :: i = 7 real(kind=rk) :: a_pv = 1000000 real(kind=rk) :: a_fv real(kind=rk) :: a_a integer :: a_d = 1 integer :: a_e = 0 real(kind=rk) :: p_pv = -1000000 real(kind=rk) :: p_fv integer :: p_d = 0 real(kind=rk) :: a_final integer :: status real(kind=rk) :: cfm(periods+1,2), afv(periods+1), apv(periods+1), fv(periods+1,2), pv(periods+1,2) print "(a)", repeat("=", 141) print "(a60,f15.4)", "n: ", n print "(a60,f15.4)", "i: ", i ! First we find the PV & FV for the principal. print "(a)", repeat("=", 141) call tvm_lump_sum_solve(n, i, p_pv, p_fv, var_fv, status) print "(a60,i15)", "tvm_lump_sum_solve status: ", status print "(a60,f15.4)", "Loan PV: ", p_pv print "(a60,f15.4)", "Loan FV: ", p_fv ! Now we solve for the payment (a in the annuity) and the fv print "(a)", repeat("=", 141) call tvm_delayed_level_annuity_solve(n, i, a_pv, a_fv, a_a, a_d, a_e, var_fv+var_a, status) print "(a60,i15)", "tvm_level_annuity_solve status: ", status print "(a60,f15.4)", "Annuity PV: ", a_pv print "(a60,f15.4)", "Annuity FV: ", a_fv print "(a60,f15.4)", "Annuity A: ", a_a ! Now we round a_a UP to the nearest cent. print "(a)", repeat("=", 141) a_a = ceiling(100*a_a) a_a = a_a / 100 print "(a60,f15.4)", "Rounded Up Annuity A: ", a_a ! Now we find PV & FV for an annuity with the rounded payment that ends one period early print "(a)", repeat("=", 141) call tvm_delayed_level_annuity_solve(n, i, a_pv, a_fv, a_a, a_d, a_e+1, var_fv+var_pv, status) print "(a60,i15)", "tvm_level_annuity_solve status: ", status print "(a60,f15.4)", "Rounded (n-1) Annuity PV: ", a_pv print "(a60,f15.4)", "Rounded (n-1) Annuity FV: ", a_fv ! The final payment needs to be the difference between the principal FV and the rounded annuity FV print "(a)", repeat("=", 141) a_final = floor(-(p_fv+a_fv)*100) a_final = a_final / 100 print "(a60,f15.4)", "Final Payment: ", a_final ! Now we construct the cashflows so we can print a nice table ! We start with the principal print "(a)", repeat("=", 141) call make_cashflow_vector_delayed_lump(cfm(:,1), p_pv, p_d, status) print "(a60,i15)", "make_cashflow_vector_delayed_lump status: ", status ! Next we add the rounded up print "(a)", repeat("=", 141) call make_cashflow_vector_delayed_level_annuity(cfm(:,2), a_a, a_d, a_e+1, status) print "(a60,i15)", "make_cashflow_vector_delayed_level_annuity status: ", status ! Finally we add the last payment cfm(periods+1, 2) = a_final ! Now we print the cashflow call cashflow_matrix_cmp(status, cfm, i, pv_o=pv, fv_o=fv, pv_agg_o=apv, fv_agg_o=afv, prt_o=prt_ALL-prt_all_agg_sum) print "(a)", repeat("=", 141) end program loan_level_payments