From 6c3c59b1a29c2270ba71457aed2bdb83253b5447 Mon Sep 17 00:00:00 2001 From: genedan Date: Sun, 30 Aug 2020 20:03:43 -0500 Subject: [PATCH] continue work on adding sinking fund features --- tmval/loan.py | 174 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 146 insertions(+), 28 deletions(-) diff --git a/tmval/loan.py b/tmval/loan.py index 6d699b5..fc1878c 100644 --- a/tmval/loan.py +++ b/tmval/loan.py @@ -1,4 +1,4 @@ -from math import ceil, floor +from math import ceil from typing import Union from tmval.annuity import Annuity, olb_r, olb_p, get_loan_pmt @@ -24,32 +24,77 @@ def __init__( self.term = term self.gr = standardize_acc(gr) self.cents = cents - self.sfr = sfr + if sfr: + self.sfr = standardize_acc(sfr) + else: + self.sfr = None self.sfd = sfd if amt is None: - - ann = Annuity( - period=self.period, - term=self.term, - gr=self.gr, - amount=self.pmt - ) - - self.amt = ann.pv() + if sfr is None: + ann = Annuity( + period=self.period, + term=self.term, + gr=self.gr, + amount=self.pmt + ).pv() + else: + a_n = Annuity( + period=self.period, + term=self.term, + gr=self.sfr, + amount=1 + ).pv() + + sf_i = self.gr.effective_interval(t2=self.period) + sf_j = self.sfr.effective_interval(t2=self.period) + print(a_n) + print(sf_i) + print(sf_j) + ann = self.pmt * (a_n / (((sf_i - sf_j) * a_n) + 1)) + + self.amt = ann else: self.amt = amt if pmt is None: - self.pmt_sched = self.get_payments() - self.pmt = self.pmt_sched.amounts[0] + if period and term: + self.pmt_sched = self.get_payments() + self.pmt = self.pmt_sched.amounts[0] + + if pmt and period and term: + n_payments = ceil(term / period) + self.pmt_sched = Payments( + times=[(x + 1) * period for x in range(n_payments)], + amounts=[pmt for x in range(n_payments)] + ) def get_payments(self): + interest_due = self.gr.effective_rate(self.period) * self.amt + n_payments = ceil(self.term / self.period) if self.sfr: - n_payments = ceil(self.term / self.period) - amt = self.gr.effective_rate(self.period) * self.amt - pmts = Payments(amounts=[amt] * n_payments, times=[(x + 1) * self.period for x in range(n_payments)]) + if self.sfd is not None: + + final_pmt = self.sf_final() + pmts = Payments( + amounts=[self.sfd + interest_due for x in range(n_payments - 1)] + [final_pmt], + times=[(x + 1) * self.period for x in range(n_payments)] + ) + + else: + + sv_ann = Annuity( + gr=self.sfr.effective_rate(self.period), + period=self.period, + term=self.term + ).sv() + sfd = self.amt / sv_ann + amt = interest_due + sfd + pmts = Payments( + amounts=[amt] * n_payments, + times=[(x + 1) * self.period for x in range(n_payments)] + ) else: @@ -143,6 +188,10 @@ def amortize_payments(self, payments: Payments) -> dict: res['principal_paid'] += [principal_paid] res['remaining_balance'] += [principal] + if self.cents: + for k, v in res.items(): + res[k] = [round(x, 2) if isinstance(x, float) else x for x in v] + return res def principal_paid(self, t2: float, t1: float = 0): @@ -173,24 +222,93 @@ def amortization(self): res = self.amortize_payments(payments=self.pmt_sched) - if self.cents: - for k, v in res.items(): - res[k] = [round(x, 2) if isinstance(x, float) else x for x in v] - return res - def sf_final(self): + def sf_final(self, payments: Payments = None) -> float: if self.sfr is None: raise Exception("sf_final only applicable to sinking fund loans.") - sv = Annuity( - amount=self.sfd, - gr=self.sfr, - period=self.period, - term=self.term - self.period - ).eq_val(self.term) + if payments: + bal = self.amt + t0 = 0 + sf_amounts = [] + sf_times = [] + for amount, time in zip(payments.amounts, payments.times): + interest_due = bal * self.gr.effective_interval(t1=t0, t2=time) + if amount >= interest_due: + sf_deposit = amount - interest_due + else: + sf_deposit = 0 + bal += interest_due - amount + sf_amounts += [sf_deposit] + sf_times += [time] + t0 = time + sf_payments = Payments(amounts=sf_amounts, times=sf_times, gr=self.sfr) + + sv = sf_payments.eq_val(self.term) + + final_pmt = bal * (1 + self.gr.effective_interval(t1=t0, t2=self.term)) - sv + else: + sv = Annuity( + amount=self.sfd, + gr=self.sfr, + period=self.period, + term=self.term - self.period + ).eq_val(self.term) - final_pmt = self.amt - sv + final_pmt = self.amt - sv return final_pmt + + def sink_payments(self, payments: Payments) -> dict: + + res = { + 'time': [], + 'interest_due': [], + 'sf_deposit': [], + 'sf_interest': [], + 'sf_bal': [], + 'loan_balance': [] + } + + # initial row + res['time'] += [0] + res['interest_due'] += [0] + res['sf_deposit'] += [0] + res['sf_interest'] += [0] + res['sf_bal'] += [0] + res['loan_balance'] += [self.amt] + + bal = self.amt + sf_bal = 0 + t0 = 0 + for amount, time in zip(payments.amounts, payments.times): + interest_due = bal * self.gr.effective_interval(t1=t0, t2=time) + if amount >= interest_due: + sf_deposit = amount - interest_due + else: + sf_deposit = 0 + bal += interest_due - amount + + sf_interest = sf_bal * self.sfr.effective_interval(t1=t0, t2=time) + sf_bal += (sf_deposit + sf_interest) + net_bal = bal - sf_bal + res['time'] += [time] + res['interest_due'] += [interest_due] + res['sf_deposit'] += [sf_deposit] + res['sf_interest'] += [sf_interest] + res['sf_bal'] += [sf_bal] + res['loan_balance'] += [net_bal] + t0 = time + + if self.cents: + for k, v in res.items(): + res[k] = [round(x, 2) if isinstance(x, float) else x for x in v] + + return res + + def sinking(self): + res = self.sink_payments(payments=self.pmt_sched) + + return res