예제 #1
0
파일: bond.py 프로젝트: genedan/TmVal
    def yield_j(self, t: float, sale: float) -> list:
        """
        Calculates the yield to the bondholder should they sell the bond at time t.

        :param t: The valuation time.
        :type t: float
        :param sale: The sale price.
        :type sale: float
        :return: The yield to the bondholder.
        :rtype: list
        """

        t0 = max([x for x in self.coupons.times if x <= t])
        ti = self.coupons.times.index(t0)

        amounts = self.coupons.amounts[(ti + 1):]
        times = self.coupons.times[(ti + 1):]
        times = [x - t for x in times]
        red_t = self.term - t

        amounts += [-sale] + [self.red]
        times += [0] + [red_t]

        pmts = Payments(amounts=amounts, times=times)

        return pmts.irr()
예제 #2
0
    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
예제 #3
0
파일: stock.py 프로젝트: genedan/TmVal
    def short(self, st: Stock, deposit=None, t=0):

        sale_amt = st.value
        if deposit is None:
            self.cash += sale_amt
            md = st.value * st.margin_req
            self.cash -= md
        else:
            self.cash += sale_amt + deposit
            md = st.value * st.margin_req
            self.cash -= md

        pmts = Payments(amounts=[-md], times=[t])

        divs = Payments(amounts=[], times=[], gr=self.margin_rate)

        res = {
            'stock': st,
            'ndb': 0,
            'margin_deposit': md,
            'position': 'short',
            'payments': pmts,
            'dividends': divs
        }

        self.portfolio += [res]
예제 #4
0
    def olb_r(self, t: float, payments: Payments = None) -> float:
        """
        Calculates the outstanding loan balance at time t, using the retrospective method. If the actual payments
        differ from the original payment schedule, they may be supplied to the payments argument.

        :param t: The valuation time.
        :type t: float
        :param payments: A list of payments, if they differed from the original payment schedule.
        :type payments: Payments
        :return: The outstanding loan balance.
        :rtype: float
        """

        if payments:
            payments.set_accumulation(gr=self.gr)
            loan_value = self.principal_val(t)
            payments_value = payments.eq_val(t)
            olb = loan_value - payments_value

        else:
            olb = olb_r(loan=self.amt,
                        q=self.pmt,
                        period=self.period,
                        gr=self.gr.gr,
                        t=t)

        return olb
예제 #5
0
파일: option.py 프로젝트: genedan/TmVal
    def yld(self, stp, t, x0=1.05):
        times = [0, t]
        amounts = [-self.c0, (stp - self.k) * self.n]

        pmts = Payments(
            times=times,
            amounts=amounts
        )

        return pmts.irr(x0=x0)
예제 #6
0
    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
예제 #7
0
파일: bond.py 프로젝트: genedan/TmVal
    def yield_c(self, times: list = None, premiums: list = None) -> list:
        """
        Calculates the yields given a list of call times.

        :param times: A list of call times.
        :type times: list
        :param premiums: A list of call premiums.
        :type premiums: list
        :return: Yield rates for the corresponding call times.
        :rtype: list
        """

        if times is None:
            return self.irr()
        else:
            yields = []
            if isinstance(times, (float, int)):
                times = [times]
            else:
                pass

            if premiums is None:
                premiums = [0] * len(times)
            elif isinstance(premiums, (float, int)):
                premiums = [premiums]
            else:
                pass

            for t, p in zip(times, premiums):
                coupons = self.prior_coupons(t=t)
                amounts = coupons.amounts
                coupon_times = coupons.times

                amounts += [-self.price] + [self.red + p]
                coupon_times += [0] + [t]

                pmts = Payments(amounts=amounts, times=coupon_times)

                yields += pmts.irr()

            if len(times) == 1:

                res = yields

            else:

                res = {'times': times, 'yields': yields}

            return res
예제 #8
0
    def fixed_principal(self) -> Payments:
        """
        Calculates the loan payment schedule if a fixed amount of principal is paid each period.

        :return: The payment schedule.
        :rtype: Payments
        """
        # so far, last payment just gets adjusted
        n_payments = ceil(self.amt / self.pp)

        bal = self.amt
        times = [(x + 1) * self.period for x in range(n_payments)]
        amounts = []
        for x in range(n_payments):
            interest_due = bal * self.gr.effective_interval(t2=self.period)
            if bal >= self.pp:
                pmt = self.pp + interest_due
            else:
                pmt = bal + interest_due
            amounts += [pmt]
            bal -= self.pp

        pmts = Payments(times=times, amounts=amounts)

        return pmts
예제 #9
0
    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
예제 #10
0
    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()
예제 #11
0
파일: stock.py 프로젝트: genedan/TmVal
    def purchase_stock(self, stock: Stock = None, deposit=None, t=0, idx=None):
        if stock is not None:
            if deposit is None:
                if self.cash >= stock.value:
                    self.cash -= stock.value
                    borrow = 0
                    ndb = 0
                else:
                    borrow = stock.value - self.cash
                    self.cash -= self.cash
                    ndb = borrow
            else:
                if self.cash + deposit >= stock.value:
                    self.cash += deposit - stock.value
                    borrow = 0
                    ndb = 0
                else:
                    borrow = stock.value - deposit - self.get_extra()
                    ndb = borrow

            pmts = Payments(amounts=[-stock.value + borrow], times=[t])

            res = {
                'stock': stock,
                'ndb': ndb,
                'payments': pmts,
                'position': 'long'
            }

            self.portfolio += [res]
        else:
            if self.portfolio[idx]['position'] == 'short':
                self.portfolio[idx]['margin_deposit'] *= (
                    1 + self.margin_rate)**(t - self.age)
                print(self.portfolio[idx]['margin_deposit'])
                repurchase = self.portfolio[idx]['stock'].value

                avail = self.cash + self.portfolio[idx]['margin_deposit']

                div_sv = self.portfolio[idx]['dividends'].eq_val(t)

                due = avail - repurchase - div_sv

                self.portfolio[idx]['margin_deposit'] = 0
                self.portfolio[idx]['stock'].shares = 0
                self.cash = due

                self.portfolio[idx]['payments'].append(amounts=[due],
                                                       times=[t])

        self.age = t
예제 #12
0
파일: bond.py 프로젝트: genedan/TmVal
    def yield_s(self, t: float, sale: float) -> list:
        """
        Calculates the yield from the perspective of the person to whom you are selling the bond.

        :param t: The valuation time.
        :type t: float
        :param sale: The sale price.
        :type sale: float
        :return: The yield to the person to whom you are selling the bond.
        :rtype: list
        """

        t0 = max([x for x in self.coupons.times if x <= t])
        ti = self.coupons.times.index(t0)

        amounts = self.coupons.amounts[:(ti + 1)]
        times = self.coupons.times[:(ti + 1)]

        amounts += [-self.price] + [sale]
        times += [0.0] + [t]

        pmts = Payments(amounts=amounts, times=times)

        return pmts.irr()
예제 #13
0
파일: bond.py 프로젝트: genedan/TmVal
    def prior_coupons(self, t: float) -> Payments:
        """
        Gets the coupons prior to the time t.

        :param t: The valuation time.
        :type t: float
        :return: The coupons prior to the time t.
        :rtype: Payments
        """

        t0 = self.last_coupon_t(t=t)
        ti = self.coupons.times.index(t0)
        amounts = self.coupons.amounts[:ti + 1]
        times = self.coupons.times[:ti + 1]

        pmts = Payments(amounts=amounts, times=times)

        return pmts
예제 #14
0
파일: portfolio.py 프로젝트: genedan/TmVal
def get_price_from_instrument(instrument: Payments):

    if isinstance(instrument, Bond):
        return instrument.price
    else:
        return instrument.npv()
예제 #15
0
    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)
예제 #16
0
파일: bond.py 프로젝트: genedan/TmVal
    def __init__(self,
                 face: float = None,
                 term: float = None,
                 red: float = None,
                 gr: Union[float, Rate] = None,
                 cgr: Rate = None,
                 alpha: Union[float, list] = None,
                 cfreq: Union[float, list] = None,
                 price: float = None,
                 pd: float = None,
                 k: float = None,
                 fr: float = None):

        self.face = face
        self.term = term
        self.k = k

        if [cgr, alpha].count(None) == 2:
            # if bond is par and priced at par
            if price == red == face and gr is not None:
                alpha = standardize_acc(gr).interest_rate.convert_rate(
                    pattern="Nominal Interest", freq=cfreq).rate
                self.alpha = alpha
                c_args = 1
            else:
                c_args = None
                self.alpha = None
        else:
            c_args = len([cgr, alpha])

        if c_args:

            cgr_dict = parse_cgr(alpha=alpha, cfreq=cfreq, cgr=cgr)
            self.cgr = cgr_dict['cgr']
            self.alpha = cgr_dict['alpha']
            self.cfreq = cgr_dict['cfreq']

            self.fr_is_level = isinstance(self.alpha, (float, int))

            self.fr = self.get_coupon_amt()

            if not self.fr_is_level:
                self.coupon_intervals = self.get_coupon_intervals()

        else:
            self.fr = fr
            self.cfreq = cfreq

        if [red, k].count(None) == 2:
            red_args = None
        else:
            red_args = len([red, k])

        if term is not None:
            self.is_term_floor = self.term_floor()
        else:
            pass

        if [alpha, cfreq, fr, cgr].count(None) == 4:
            self.is_zero = True
        else:
            self.is_zero = False

        if self.is_zero:
            args = [gr, red_args, price, term]
        else:
            args = [gr, red_args, price, term, c_args]

        n_missing = args.count(None)

        if n_missing == 0:
            self.red = red
            self.n_coupons = self.get_n_coupons()
            self.gr = standardize_acc(gr)
            self.coupons = self.get_coupons()
            self.price = price

        elif n_missing == 1:

            if price is None:
                self.n_coupons = self.get_n_coupons()
                self.red = red
                self.gr = standardize_acc(gr)
                self.coupons = self.get_coupons()

            elif gr is None:
                self.n_coupons = self.get_n_coupons()
                self.red = red
                self.price = price

                if self.is_zero:
                    amounts = [-price, red]
                elif self.fr_is_level:
                    amounts = [-price] + [self.fr] * self.n_coupons + [red]
                else:
                    amounts = [-price]
                    cis = self.get_coupon_intervals()

                    for afr, cf, ci in zip(self.fr, self.cfreq, cis):
                        n = cf * ci
                        amounts += [afr[0]] * n
                    amounts += [red]

                times = [0.0] + self.get_coupon_times() + [term]

                pmts = Payments(amounts=amounts, times=times)

                irr = [x for x in pmts.irr() if x > 0]
                self.gr = standardize_acc(min(irr))

                self.coupons = self.get_coupons()

            elif term is None:
                if self.is_zero:
                    self.red = red
                    self.gr = standardize_acc(gr)
                    self.price = price
                    self.term = Amount(gr=gr, k=price).solve_t(fv=red)

            else:
                self.n_coupons = self.get_n_coupons()
                self.red = red
                self.gr = standardize_acc(gr)
                self.price = price
                self.coupons = self.get_coupons()
                self.red = self.get_redemption()

        elif n_missing == 2:
            if price is None and red_args is None:
                if pd is not None:
                    self.n_coupons = self.get_n_coupons()
                    self.gr = standardize_acc(gr)
                    self.coupons = self.get_coupons()

                    self.red = (self.coupons.pv() -
                                pd) / (1 - self.gr.discount_func(t=self.term))

                    self.price = self.red + pd

                else:
                    raise Exception(
                        "Unable to evaluate bond. Too many missing arguments.")

            elif price is None and term is None:
                if k is not None:
                    self.gr = standardize_acc(gr)
                    self.red = red
                    self.g = self.fr / self.red

                    self.price = self.makeham()
                    self.term = self.gr.solve_t(pv=k, fv=self.red)
                    self.n_coupons = self.get_n_coupons()
                    self.coupons = self.get_coupons()
                    self.is_term_floor = self.term_floor()
                else:
                    raise Exception(
                        "Unable to evaluate bond. Too many missing arguments.")

            elif price is None and c_args is None:
                if fr is not None:
                    self.gr = standardize_acc(gr)
                    self.red = red
                    self.price = self.base_amount()
                    self.fr_is_level = True
                    self.fr = fr
                    self.n_coupons = self.get_n_coupons()
                    self.coupons = self.get_coupons()
                    self.alpha = None
                else:
                    raise Exception(
                        "Unable to evaluate bond. Too many missing arguments.")

            else:
                raise Exception(
                    "Unable to evaluate bond. Too many missing arguments.")

        else:
            raise Exception(
                "Unable to evaluate bond. Too many missing arguments.")

        if self.is_zero:
            amounts = [self.red]
            times = [self.term]
        else:
            amounts = self.coupons.amounts + [self.red]
            times = self.coupons.times + [self.term]

        Payments.__init__(self, amounts=amounts, times=times, gr=self.gr)

        if price is None:
            if self.is_term_floor:
                self.price = self.npv()
            else:
                if self.n_coupons == 1:

                    j = self.gr.val(1 / self.cfreq) - 1
                    f = 1 - self.term / (1 / self.cfreq)

                    self.price = (self.red +
                                  self.fr) / (1 + (1 - f) * j) - f * self.fr
                else:
                    self.price = self.clean(t=0)
        else:
            pass

        self.amounts = [-self.price] + self.amounts
        self.times = [0] + self.times

        if self.is_zero:
            pass
        elif self.fr_is_level:
            self.j = self.gr.val(1 / self.cfreq) - 1
            self.base = self.fr / self.j
            self.g = self.fr / self.red

        self.premium = self.price - self.red
        self.discount = self.red - self.price

        self.k = self.gr.discount_func(t=self.term, fv=self.red)
예제 #17
0
파일: bond.py 프로젝트: genedan/TmVal
    def dirty(self,
              t: float,
              tprac: str = 'theoretical',
              gr: Union[float, Rate] = None) -> float:
        """
        Calculates the dirty value of a bond. It can be toggled to switch between theoretical and practical dirty \
        values.

        :param t: The valuation time.
        :type t: float
        :param tprac: Whether you want the practical or theoretical dirty value. Defaults to 'theoretical'.
        :type tprac: str
        :param gr: The valuation yield, if pricing a bond at a different yield. Defaults to the current bond yield.
        :type gr: float, Rate
        :return: The dirty value.
        :rtype: float
        """
        if t == 0:
            t0 = 0

            ti = -1

            t1 = self.coupons.times[0]

        else:

            t0 = max([x for x in self.coupons.times if x <= t])

            # get next coupon time
            ti = self.coupons.times.index(t0)

            t1 = self.coupons.times[ti + 1]

            # get next coupon amount

        f = (t - t0) / (t1 - t0)

        if gr is None:
            j_factor = (1 + self.gr.effective_interval(t1=t0, t2=t))
            balance = self.balance(t0)
        else:
            jgr = standardize_acc(gr)
            j_factor = (1 + jgr.effective_interval(t1=t0, t2=t))

            amounts = self.coupons.amounts[(ti + 1):]
            times = self.coupons.times[(ti + 1):]
            times = [x - t0 for x in times]
            red_t = self.term - t0

            amounts += [self.red]
            times += [red_t]

            pmts = Payments(amounts=amounts, times=times, gr=jgr)

            balance = pmts.npv()

        if tprac == 'theoretical':

            dt = balance * j_factor

        elif tprac == 'practical':

            dt = self.balance(t0) * (1 + self.j * f)

        else:
            raise ValueError("tprac must be 'theoretical' or 'practical'.")

        return dt
예제 #18
0
    def __init__(self,
                 gr: Union[Accumulation, Callable, float, Rate],
                 amount: Union[float, int, list, Callable] = 1.0,
                 period: float = 1,
                 term: float = None,
                 n: float = None,
                 gprog: float = 0.0,
                 aprog: Union[float, int, tuple] = 0.0,
                 times: list = None,
                 reinv: [Accumulation, Callable, float, Rate] = None,
                 deferral: float = 0.0,
                 imd: str = 'immediate',
                 loan: float = None,
                 drb: str = None):
        self.term = term
        self.amount = amount
        self.period = period
        self.imd = imd
        self.gprog = gprog

        if isinstance(aprog, (float, int)):
            self.aprog = aprog
            self.mprog = 1
        elif isinstance(aprog, tuple):
            self.aprog = aprog[0]
            self.mprog = aprog[1] * period
        else:
            raise ValueError("Invalid value provided to aprog.")

        imd_ind = 1 if imd == 'immediate' else 0
        self.is_level_pmt = None
        self.reinv = reinv
        self.deferral = deferral
        self.loan = loan
        self.n_payments = n
        self.drb_pmt = None

        if term is None:
            if self.n_payments:
                self.term = self.n_payments * self.period
                if loan is None:
                    r = self.n_payments
                else:
                    r = max(self.get_r_pmt(gr), self.n_payments)
            elif times:
                self.term = max(times)
                r = len(times)
                self.n_payments = len(times)
            elif period == 0:
                dt = standardize_acc(gr).interest_rate.convert_rate(
                    pattern="Force of Interest")
                r = np.Inf
                self.term = np.log(1 - loan / amount * dt) / (-dt)
            else:
                r = self.get_r_pmt(gr)
                r = ceil(r) if drb == 'drop' else floor(r)
                self.term = r * self.period
        else:
            if self.period == 0:
                r = np.Inf
            else:
                r = self.term / self.period
        # perpetuity
        if self.term == np.inf or self.n_payments == np.Inf:

            def perp_series(t):
                start = amount
                factor = 1 + gprog
                curr = 0
                while curr < t:
                    yield start * factor**curr
                    curr += 1

            def perp_times(t):
                curr = 0 + 1 if imd == 'immediate' else 0
                while curr < t:
                    yield curr + 1

            amounts = perp_series
            times = perp_times
            self._ann_perp = 'perpetuity'
            self.n_payments = np.inf

            if gprog == 0:
                self.is_level_pmt = True
            else:
                self.is_level_pmt = False
        # continuously paying
        elif self.period == 0:
            amounts = []
            times = []
            self.n_payments = np.inf
            self._ann_perp = 'annuity'

            if isinstance(amount, Callable):
                if round(amount(0) * self.term,
                         3) == round(quad(amount, 0, self.term)[0], 3):
                    Warning(
                        "Level continuously paying annuity detected. It's better to supply a constant to the "
                        "amount argument to speed up computation.")
                    self.is_level_pmt = True
                else:
                    self.is_level_pmt = False
            else:
                self.is_level_pmt = True
        else:
            if self.n_payments is None and term is not None:
                r_payments = self.term / period
                n_payments = floor(r_payments)
                self.n_payments = n_payments
                f = 0
            elif self.n_payments is None and term is None:
                r_payments = self.get_r_pmt(gr)
                n_payments = floor(r_payments)
                f = r_payments - n_payments
                self.n_payments = n_payments

            elif r > self.n_payments:
                self.n_payments -= 1
                f = r - self.n_payments
            else:
                f = 0

            if isinstance(amount, (int, float)) or (isinstance(amount, list)
                                                    and len(amount)) == 1:
                amounts = [
                    self.amount * (1 + self.gprog)**x +
                    self.aprog * floor(x * self.mprog)
                    for x in range(self.n_payments)
                ]
                times = [
                    period * (x + imd_ind) for x in range(self.n_payments)
                ]
            else:
                amounts = amount
                times = times
                times.sort()
                intervals = list(np.diff(times))
                intervals = [round(x, 7) for x in intervals]
                if intervals[1:] != intervals[:-1]:
                    raise Exception(
                        "Non-level intervals detected, use payments class instead."
                    )
                else:
                    self.period = intervals[0]

                if min(times) == 0:
                    self.imd = 'due'
                    self.term = max(times) + self.period
                else:
                    self.imd = 'immediate'
                    self.term = max(times)

            if deferral > 0:
                times = [x + deferral for x in times]

            if 0 < f < 1:

                if drb == "balloon":
                    self.drb_pmt = self.get_balloon(gr)
                    amounts[-1] = self.drb_pmt
                elif drb == "drop":
                    self.drb_pmt = self.get_drop(gr)
                    amounts.append(self.drb_pmt)
                    times.append(self.term)
                    self.n_payments += 1

                self.is_level_pmt = False

            elif 1 <= f:
                self.drb_pmt = self.amount + olb_r(loan=self.loan,
                                                   q=self.amount,
                                                   period=self.period,
                                                   gr=gr,
                                                   t=self.term)

                amounts.append(self.drb_pmt)
                times.append(self.term)
                self.is_level_pmt = False

            else:
                pass

            if (isinstance(amount, (int, float)) or (isinstance(amount, list) and
                len(amount))) == 1 and \
                    gprog == 0 and \
                    aprog == 0 and self.drb_pmt is None:

                self.is_level_pmt = True

            elif gprog != 0 or aprog != 0:

                self.is_level_pmt = False

            elif amounts[1:] == amounts[:-1]:

                self.is_level_pmt = True

            else:

                self.is_level_pmt = False

            self._ann_perp = 'annuity'

        Payments.__init__(self, amounts=amounts, times=times, gr=gr)

        self.pattern = self._ann_perp + '-' + imd

        if imd not in ['immediate', 'due']:
            raise ValueError('imd can either be immediate or due.')