def get_coupons(self) -> Annuity: """ Calculates the coupons and returns them as an Annuity object. :return: The bond coupons. :rtype: Annuity """ if self.is_zero: coupons = None elif self.fr_is_level: if round(self.term % (1 / self.cfreq), 5) == 0: coupons = Annuity(gr=self.gr, amount=self.fr, period=1 / self.cfreq, term=self.term) else: period = 1 / self.cfreq f = self.term - ((self.n_coupons - 1) * period) coupons = Annuity(gr=self.gr, amount=self.fr, n=self.n_coupons, imd='due', deferral=f) else: times = [] amounts = [] base = 0 if isinstance(self.fr, Iterable)\ and isinstance(self.cfreq, Iterable)\ and isinstance(self.coupon_intervals, Iterable): for a, c, t in zip(self.fr, self.cfreq, self.coupon_intervals): n_pmt = t * c amounts += [a[0]] * n_pmt times += [(x + base + 1) / c for x in range(n_pmt)] base += n_pmt else: raise TypeError( "fr, cfreq, and coupon_intervals must be iterable when coupons are nonlevel" ) coupons = Annuity(gr=self.gr, amount=amounts, times=times, term=self.term) return coupons
def balance(self, t: float, n: int = None) -> float: """ Calculates the bond balance at time t, where t must coincide with a payment time. Alternatively, if you'd like \ to know the balance just prior to the n-th coupon payment, you may supply the value to the argument n. :param t: The balance time. :type t: float :param n: The n-th coupon payment. :type n: int :return: The bond balance. :rtype: float """ if t not in self.times: raise ValueError( "Bond balance only available when t occurs on a payment date. For valuations " "between payment dates, use the clean or dirty value formulas." ) if n is not None and t is not None: raise ValueError("Can supply t or n, but not both.") if t is None: if n is not None: t = self.coupons.times[n - 1] else: raise ValueError("You need to supply a time or n-th coupon.") nc = self.n_coupons t0 = floor(t * self.cfreq) g = self.g c = self.red j = self.j if t < self.term: ann = Annuity( gr=j, n=nc - t0, ) bt = c * (g - j) * ann.pv() + c elif t == self.term: bt = self.red else: bt = 0 return bt
def sgr_equiv(self) -> Rate: """ Calculates the sinking fund rate such that would produce a loan payment schedule equivalent to that of an amortized loan. :return: The sinking fund rate. :rtype: Rate """ if self.pmt_is_level: ann = Annuity(period=self.period, term=self.term, gr=self.gr).pv() snn = Annuity(period=self.period, term=self.term, gr=self.sfr).sv() gr = Rate(rate=1 / ann - 1 / snn, pattern="Effective Interest", interval=self.period) else: pmts = Payments(amounts=self.pmt_sched.amounts, times=self.pmt_sched.times, gr=self.sfr) snn = Annuity(period=self.period, term=self.term, gr=self.sfr) i = (pmts.eq_val(t=self.term) - self.amt) / (self.amt * snn.sv()) gr = Rate(rate=i, pattern="Effective Interest", interval=self.period) return gr
def get_payments(self) -> Payments: """ Takes the arguments supplied and creates the payment schedule for the loan. The return type is a \ :class:`.Payments` object, so it contains the payment times and amounts. :return: The payment schedule. :rtype: Payments """ if self.sfr: if self.sfd is not None: interest_due = self.gr.effective_interval( t2=self.period) * self.amt n_payments = ceil(self.term / self.period) final_pmt = self.sf_final() pmts = Payments(amounts=[self.sfd + interest_due] * (n_payments - 1) + [final_pmt], times=[(x + 1) * self.period for x in range(n_payments)]) else: interest_due = self.sfh_gr.effective_interval( t2=self.period) * self.amt n_payments = ceil(self.term / self.period) 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 self.sfd = sfd pmts = Payments(amounts=[amt] * n_payments, times=[(x + 1) * self.period for x in range(n_payments)]) elif self.pp is not None: pmts = self.fixed_principal() else: pmts_dict = get_loan_pmt(loan_amt=self.amt, period=self.period, term=self.term, gr=self.gr.gr, cents=self.cents) pmts = Payments(amounts=pmts_dict['amounts'], times=pmts_dict['times']) return pmts
def sf_final(self, payments: Payments = None) -> float: """ Calculates the final payment required to settle a sinking fund loan. You may supply payments, if they differed \ from the original payment schedule. :param payments: A list of payments, if different from the original payment schedule. :type payments: Payments, optional :return: The final sinking fund payment. :rtype: float """ if self.sfr is None: raise Exception("sf_final only applicable to sinking fund loans.") 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 return final_pmt
def rc_yield(self) -> list: """ Calculates the yield rate based off replacement of capital. :return: The yield rate based off replacement of capital. :rtype: list """ n_payments = ceil(self.term / self.period) if self.pmt_is_level: extra = [self.pmt - self.sfd] * n_payments sv = Annuity(amount=self.sfd, gr=self.sfr, term=self.term, period=self.period).sv() pmts = Payments(amounts=[-self.amt] + extra + [sv], times=[0.0] + [(x + 1) * self.period for x in range(n_payments)] + [self.term], gr=self.sfr) else: sf_deps = self.sinking()['sf_deposit'] extra = [] for amount, sfd in zip(self.pmt_sched.amounts, sf_deps[1:]): extra_i = amount - sfd extra += [extra_i] sv = self.sinking()['sf_bal'][-1] pmts = Payments(amounts=[-self.amt] + extra + [sv], times=[0.0] + [(x + 1) * self.period for x in range(n_payments)] + [self.term], gr=self.sfr) return pmts.irr()
def hybrid_principal(self) -> float: """ Calculates the loan amount based on a hybrid amortized/sinking fund loan. :return: The loan amount. :rtype: float """ ann_am = Annuity(term=self.term, gr=self.gr, period=self.period) ann_sf = Annuity(term=self.term, gr=self.sfr, period=self.period) if self.sfh_gr: sfh = self.sfh_gr.effective_interval(t2=self.period) else: sfh = self.gr.effective_interval(t2=self.period) return self.pmt / (sfh * self.sf_split + self.sf_split / ann_sf.sv() + (1 - self.sf_split) / ann_am.pv())
def __init__(self, gr: Union[float, Rate, TieredTime] = None, pmt: Union[float, int, Payments] = None, period: float = None, term: float = None, amt: float = None, cents: bool = False, sfr: Union[float, Rate, TieredTime] = None, sfd: float = None, sf_split: float = 1.0, sfh_gr: Union[float, Rate, TieredTime] = None, pp: float = None): self.pmt = pmt self.period = period self.term = term if gr is not None: self.gr = standardize_acc(gr) else: self.gr = None self.cents = cents if sfr: self.sfr = standardize_acc(sfr) else: self.sfr = None self.sfd = sfd self.sf_split = sf_split self.pp = pp self.pmt_is_level = None if isinstance(pmt, Payments): self.pmt_sched = pmt if pmt.amounts[1:] == pmt.amounts[:-1]: self.pmt_is_level = True else: self.pmt_is_level = False if amt is None: if sfr is None: ann = Annuity(period=self.period, term=self.term, gr=self.gr, amount=self.pmt).pv() elif sfr and sf_split == 1: ann_snk = 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) ann = self.pmt * (ann_snk / (((sf_i - sf_j) * ann_snk) + 1)) else: ann = self.hybrid_principal() self.amt = ann else: self.amt = amt if sfh_gr: self.sfh_gr = standardize_acc(sfh_gr) elif gr is not None and sfr is not None: self.sfh_gr = standardize_acc(self.sgr_equiv()) else: self.sfh_gr = self.gr if pmt is None: if period and term: self.pmt_sched = self.get_payments() self.pmt = self.pmt_sched.amounts[0] elif pp: self.pmt_sched = self.get_payments() self.pmt = self.pmt_sched.amounts[0] else: self.pmt_sched = Payments(times=[], amounts=[]) self.pmt = None elif pmt and isinstance(pmt, (float, int)) 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] * n_payments) self.pmt_is_level = True else: self.pmt_sched = Payments(times=[], amounts=[]) if sfr is not None and sfd is None and pmt is not None: sv = Annuity(gr=self.sfr, period=self.period, term=self.term).sv() self.sfd = self.amt / sv Payments.__init__(self, amounts=[amt] + [-x for x in self.pmt_sched.amounts], times=[0] + self.pmt_sched.times, gr=self.gr)