Exemple #1
0
    def effective_interval(self,
                           t2: float,
                           t1: float = 0,
                           annualized: bool = False) -> Rate:
        """
        Calculates the effective interest rate over a time period.

        :param t1: the beginning of the period.
        :type t1: float
        :param t2: the end of the period.
        :type t2: float
        :return: the effective interest rate over the time period.
        :rtype: float
        :param annualized: whether you want the results to be annualized.
        :rtype annualized: bool
        """

        interval = t2 - t1
        effective_rate = (self.val(t=t2) - self.val(t=t1)) / self.val(t=t1)

        effective_rate = Rate(rate=effective_rate,
                              pattern="Effective Interest",
                              interval=interval)

        if annualized:
            effective_rate = effective_rate.standardize()

        return effective_rate
Exemple #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
Exemple #3
0
def read_iym(table: dict, t0: float, t: float) -> Rate:
    """
    Reads a value from a table of interest rates using the investment year method. A table might look something
    like this:

    iym_table = {
    2000: [.06, .065, .0575, .06, .065],
    2001: [.07, .0625, .06, .07, .0675],
    2002: [.06, .06, .0725, .07, .0725],
    2003: [.0775, .08, .08, .0775, .0715]
    }

    :param table: A table of interest rates as a dictionary, with years as the keys.
    :type table: dict
    :param t0: The initial investment time.
    :type t0: float
    :param t: The desired lookup time for which the rate applies.
    :type t: float
    :return: An interest rate applicable to the desired lookup time.
    :rtype: Rate
    """
    duration = t
    n_col = len(table[t0])
    row_d = duration - n_col

    if duration <= n_col:
        rate = table[t0][duration]
    else:
        row = row_d + t0
        rate = table[row][-1]

    rate = Rate(rate)

    return rate
Exemple #4
0
    def get_jump_times(
        self,
        k: float,
    ) -> list:
        """
        Calculates the times at which the interest rate is expected to change for the account, assuming \
        an initial investment of k and no further investments.

        :param k: the principal, or initial investment.
        :type k: float
        :return: a list of times at which the interest rate is expected to change for the account.
        :rtype: list
        """
        # determine jump balances and rates
        jump_balances = [i for i in self.tiers if i > k]
        if len(jump_balances) == 0:
            jump_rates = []
        else:
            jump_rates = self.rates[:len(self.rates) - 1]
            jump_rates = jump_rates[-len(jump_balances):]

        # determine jump times
        jump_times = []
        pv = k
        t_base = 0
        for fv, i in zip(jump_balances, jump_rates):
            jump_increment = compound_solver(pv=pv, fv=fv, gr=Rate(i))
            jump_times.append(t_base + jump_increment)
            t_base = t_base + jump_increment
            pv = fv

        return jump_times
Exemple #5
0
def forward_rates(term, bonds=None, yields=None, alpha=None):
    """
    Given a list of bonds, or yields and coupon rates, returns the forward rates.

    :param term:
    :type term:
    :param bonds:
    :type bonds:
    :param yields:
    :type yields:
    :param alpha:
    :type alpha:
    :return:
    :rtype:
    """
    sr = spot_rates(bonds=bonds, yields=yields, alpha=alpha)

    res = {}
    for t in range(len(sr) + 1 - term):
        if t == 0:
            f = sr[term]
        else:
            f_factor = (1 + sr[t + term])**(t + term) / (1 + sr[t])**t
            f = Rate(f_factor**(1 / term) - 1)
        res.update({(t, t + term): f})

    return res
Exemple #6
0
def spot_rates(bonds: List[Bond] = None, yields=None, alpha=None):
    """
    Solves for the spot rates given a list of bonds.
    :param bonds:
    :type bonds:
    :param yields:
    :type yields:
    :param alpha:
    :type alpha:
    :return:
    :rtype:
    """

    if yields is not None and alpha is not None and bonds is None:
        bonds = [
            Bond(face=100, red=100, alpha=alpha, cfreq=1, term=t + 1, gr=y)
            for t, y in zip(range(len(yields)), yields)
        ]

    # find bond with shortest term
    bond_ts = [b.term for b in bonds]
    min_t = min(bond_ts)
    b0_i = bond_ts.index(min_t)
    b0 = bonds[b0_i]

    r_1 = b0.gr.interest_rate
    rates = [r_1]

    bonds.pop(0)

    t0 = 1

    res = {t0: r_1}

    for b in bonds:
        pmts = b.group_payments()
        del pmts[0]
        times = list(pmts.keys())
        times.sort()
        t_max = times.pop()
        base = b.price
        for r, t in zip(rates, times):
            pv = pmts[t] / ((1 + r)**t)
            base -= pv
        r_t = (pmts[t_max] / base)**(1 / t_max) - 1
        r_t = Rate(r_t)
        rates += [r_t]
        res.update({t_max: r_t})

    return res
Exemple #7
0
def compound_solver(pv: float = None,
                    fv: float = None,
                    t: float = None,
                    gr: Union[float, Rate] = None):
    """
    Solves for a missing value in the case of compound interest supply 3/4 of: a present value, a future value \
    an interest rate (either APY or APR), and a time. If using an APR interest rate, you need to supply a compounding \
    frequency of m times per period, and you need to set the use_apr parameter to True.

    :param pv: the present value, defaults to None.
    :type pv: float
    :param fv: the future value, defaults to None.
    :type fv: float
    :param t: the time, defaults to None.
    :type t: float
    :type gr: a general growth rate.
    :type gr: Rate
    :return: either a present value, a future value, an interest rate, or a time, depending on which one is missing.
    :rtype: float
    """

    args = [pv, fv, gr, t]

    if args.count(None) > 1:
        raise Exception(
            "You are missing either a present value (pv), future value(fv), "
            "time (t), or growth rate.")

    if gr:
        i = standardize_rate(gr)

    else:
        i = None

    if pv is None:
        res = fv / ((1 + i)**t)
    elif fv is None:
        res = pv * ((1 + i)**t)
    elif gr is None:
        # get the effective rate first
        res = ((fv / pv)**(1 / t)) - 1
        res = Rate(res)
    else:
        res = np.log(fv / pv) / np.log(1 + i)

    return res
Exemple #8
0
    def __init__(
        self,
        tiers: list,
        rates: list,
    ):
        self.tiers = tiers
        rates_std = []
        for x in rates:
            if isinstance(x, Rate):
                pass
            elif isinstance(x, float):
                x = Rate(x)
            else:
                raise TypeError("Invalid type passed to rates, \
                use either a list of floats or Rate objects.")

            rates_std.append(x)

        self.rates = rates_std
Exemple #9
0
    def modified_duration(self, i, m=1, excl_inv=True):

        if excl_inv:
            if m != 1:
                im = Rate(i).convert_rate(
                    pattern="Nominal Interest",
                    freq=m
                )
                return self.macaulay_duration() / (1 + im.rate / m)
            else:
                times = self.times.copy()
                amounts = self.amounts.copy()
                times.pop(0)
                amounts.pop(0)
                pmts = Payments(times=times, amounts=amounts, gr=self.gr)
                return - derivative(pmts.npv, x0=i, dx=1e-6) / pmts.npv(gr=i)
        else:

            return - derivative(self.npv, x0=i, dx=1e-6) / self.npv(gr=i)
Exemple #10
0
def rate_from_earned(iex: Tuple[float, float], iey: Tuple[float,
                                                          float]) -> Rate:
    """
    Given the amount of interest earned in two time periods, calculates the annualized effective interest rate.

    :param iex: Interest earned in period x, provided as (interest earned, period x).
    :type iex: Tuple[float, float]
    :param iey: Interest earned in period y, provided as (interest earned, period y).
    :type iey: Tuple[float, float]
    :return: An annualized effective interest rate.
    :rtype: Rate
    """

    x_amt = iex[0]
    x_t = iex[1]
    y_amt = iey[0]
    y_t = iey[1]

    gr = Rate((y_amt / x_amt)**(1 / ((y_t - 1) - (x_t - 1))) - 1)

    return gr
Exemple #11
0
def simple_solver(
    pv: float = None,
    fv: float = None,
    gr: Union[float, Rate] = None,
    t: float = None,
    rate_res="Simple Interest",
):
    """
    Simple interest solver for when one variable is missing - returns missing value. You need to supply
    three out of the four arguments, and the function will solve for the missing one.

    :param pv: the present value
    :type pv: float
    :param fv: the future value
    :type fv: float
    :param gr: the interest rate
    :type gr: float, Rate
    :param t: the time
    :type t: float
    :param rate_res: if solving for a rate, whether you want simple discount or interest
    :type rate_res: str
    :return: the present value, future value, interest rate, or time - whichever is missing.
    :rtype: float, Rate
    """
    args = [pv, fv, gr, t]
    if args.count(None) > 1:
        raise Exception("Only one argument can be missing.")

    if gr is not None:
        if isinstance(gr, float):
            gr = Rate(s=gr)
        elif isinstance(gr, Rate):
            gr = gr.standardize()
        else:
            raise TypeError("Invalid type passed to s.")

    if pv is None:
        res = fv / (1 + t * gr)

    elif fv is None:

        if gr.pattern == "Simple Interest":
            res = pv * (1 + t * gr)
        elif gr.pattern == "Simple Discount":
            res = pv / (1 - t * gr)
        else:
            raise ValueError(
                "Invalid interest rate pattern passed to argument gr.")

    elif gr is None:
        if rate_res == "Simple Interest":
            res = (fv / pv - 1) / t
            res = Rate(s=res)
        elif rate_res == "Simple Discount":
            res = (1 - pv / fv) / t
            res = Rate(sd=res)
        else:
            raise ValueError("Invalid value passed to rate_res.")
    else:
        res = (fv / pv - 1) / gr

    return res
Exemple #12
0
def time_weighted_yield(
    balance_times: list,
    balance_amounts: list,
    payments: Payments = None,
    payment_times: list = None,
    payment_amounts: list = None,
    annual: bool = False
) -> Rate:
    """
    Given a list of balances and payments, returns the time-weighted yield. If annual is set to True, returns the
    rate as an annual rate. Otherwise, the rate is effective over the investment term.

    You may supply a Payments object, or specify the components separately.

    :param balance_times: A list of balance times.
    :type balance_times: list
    :param balance_amounts: A list of balance amounts, corresponding to the balance times.
    :type balance_amounts: list
    :param payments: A Payments object.
    :type payments: Payments
    :param payment_times: A list of payment times.
    :type payment_times: list
    :param payment_amounts: A list of payment amounts, corresponding to the payment times.
    :type payment_amounts: list
    :param annual: Whether you want the time-weighted yield to be annualized.
    :type annual: bool, defaults to False
    :return: The time-weighted yield.
    :rtype: Rate
    """
    # group payments by time

    if payments:
        payment_times = payments.times
        payment_amounts = payments.amounts
    else:
        payment_times = payment_times
        payment_amounts = payment_amounts

    payments = [[x, y] for x, y in zip(payment_times, payment_amounts)]
    payments.sort()
    payments_grouped = []
    for i, g in groupby(payments, key=lambda x: x[0]):
        payments_grouped.append([i, sum(v[1] for v in g)])

    payments_dict = {x[0]: x[1] for x in payments_grouped}

    balance_zip = zip(balance_times, balance_amounts)
    balance_dict = {x[0]: x[1] for x in balance_zip}

    j_factors = []
    for t_prior, t in pairwise(balance_dict.keys()):

        if t_prior == 0:
            j_factor = balance_dict[t] / balance_dict[t_prior]
        else:
            j_factor = balance_dict[t] / (balance_dict[t_prior] + payments_dict[t_prior])

        j_factors.append(j_factor)

    jtw = functools.reduce(lambda x, y: x*y, j_factors) - 1

    jtw = Rate(
        rate=jtw,
        pattern="Effective Interest",
        interval=max(balance_times)
    )

    if annual:
        jtw = jtw.convert_rate(
            pattern="Effective Interest",
            interval=1
        )

    return jtw
Exemple #13
0
def dollar_weighted_yield(
        payments: Payments = None,
        times: list = None,
        amounts: list = None,
        a: float = None,
        b: float = None,
        i: float = None,
        w_t: float = None,
        k_approx: bool = False,
        annual: bool = False
) -> Rate:
    if [a, b, w_t].count(None) not in [0, 3] and k_approx is False:
        raise Exception("a, b, w_t must all be provided or left none.")

    if payments:
        times = payments.times.copy()
        amounts = payments.amounts.copy()
    elif times and amounts:
        times = times
        amounts = amounts
    elif k_approx:
        pass
    else:
        raise Exception("Must supply a payments object or list of payment times and amounts if not "
                        "using k-approximation.")

    if a is None:
        w_t = times.pop()
        b = amounts.pop()
        a = amounts.pop(0)
        times.pop(0)

    if amounts is not None:
        c = sum(amounts)

    if i is None:
        i = b - a - c

    if k_approx:

        j = (2 * i) / (a + b - i)

    else:
        # normalize times
        max_t = w_t
        t_s = [t / max_t for t in times]
        j = i / (a + sum([ct * (1 - t) for ct, t in zip(amounts, t_s)]))

    j = Rate(
        rate=j,
        pattern="Effective Interest",
        interval=w_t
    )

    if annual:
        j = j.convert_rate(
            pattern="Effective Interest",
            interval=1
        )

    return j
Exemple #14
0
    def dw_approx(
        self,
        a: float = None,
        b: float = None,
        w_t: float = None,
        k_approx: bool = False,
        k: float = .5,
        annual: bool = False
    ) -> Rate:

        """
        Calculates the approximate dollar-weighted yield rate by standardizing the investment time to 1:

        .. math::

           j \\approx \\frac{I}{A + \\sum_{t \\in (0, 1)} C_t(1-t)}

        Where A is the beginning balance, I is interest earned, and the Cs are the contributions. When k_approx is
        set to true, k is assumed to be a fixed constant within the investment window:

        .. math::

           j \\approx \\frac{I}{A + C(1-k)}

        The default value for k is 1/2, simplifying the above expression to:

           j \\approx \\frac{2I}{A + B - I}

        Where B is the withdrawal balance. When arguments a, b, and w_t (corresponding to A, B, and the \
        the withdrawal time) are not provided, a is assumed to be the first payment in the parent object, and b is \
        calculated to be the last.

        :param a: The initial balance.
        :type a: float
        :param b: The withdrawal balance.
        :type b: float
        :param w_t: The withdrawal time.
        :type w_t: float
        :param k_approx: Whether you want to use the k-approximation formula.
        :type k_approx: bool
        :param k: The value for k in the k-approximation formula, defaults to .5.
        :type k: float
        :param annual: Whether you want the results annualized.
        :type annual: bool
        :return: The approximate dollar-weighted yield rate.
        :rtype: Rate
        """

        if [a, b, w_t].count(None) not in [0, 3]:
            raise Exception("a, b, w_t must all be provided or left none.")

        times = self.times.copy()
        amounts = self.amounts.copy()

        if a is None:
            w_t = times.pop()
            b = - amounts.pop()
            a = amounts.pop(0)
            times.pop(0)

        c = sum(amounts)
        i = b - a - c

        if k_approx:

            j = i / (k * a + (1 - k) * b - (1 - k) * i)

        else:
            # normalize times
            max_t = w_t
            t_s = [t / max_t for t in times]
            j = i / (a + sum([ct * (1-t) for ct, t in zip(amounts, t_s)]))

        j = Rate(
            rate=j,
            pattern="Effective Interest",
            interval=w_t
        )

        if annual:
            j = j.convert_rate(
                pattern="Effective Interest",
                interval=1
            )

        return j
Exemple #15
0
def parse_cgr(alpha: Union[float, list] = None,
              cfreq: Union[float, list] = None,
              cgr: Union[Rate, TieredTime] = None) -> dict:
    """
    Parses the coupon-related arguments provided to the Bond class. It returns a dictionary containing items that
    are used to calculate other features of the bond - alpha, cfreq, and cgr. This is used to improve the flexibility
    of the arguments one may supply to specify coupons.

    This function can for example, extract the rate and frequency components out of the cgr argument and supply them
    to alpha and cfreq, respectively. Or, it can take the alpha and cfreq values to calculate cgr.

    It also handles the case where alpha and cfreq are not constant throughout the term of the bond, in which case
    cgr is calculated to be a TieredTime and vice versa.

    :param alpha: When supplied as a float, a nominal coupon rate. When supplied as a list of tuples of the form [(x1, \
    y_1), (x2, y2), ...], represents a series of coupon rates x supplied during periods beginning at time y. For \
    example alpha=[(.05, 0), (.04, 2)] means a 5% coupon rate the first two years and a 4% rate after that.
    :type alpha: float, list
    :param cfreq: The coupon frequency, or a list of coupon frequencies. When supplied as a list, alpha must also \
    be supplied as a list and each element in cfreq needs to correspond to a tuple in alpha.
    :type cfreq: float, list
    :param cgr: A nominal rate representing the coupon rate and frequency.
    :type cgr: Rate, TieredTime
    :return: A dictionary containing the alpha, cfreq, and cgr values.
    :rtype: dict
    """

    if alpha is not None and cfreq is not None:

        if isinstance(alpha, (float, int)) \
                and isinstance(cfreq, (float, int)):

            gr = Rate(rate=alpha, pattern="Nominal Interest", freq=cfreq)

        elif isinstance(alpha, list) \
                and isinstance(cfreq, list):

            tiers = []
            rates = []
            for a, c in zip(alpha, cfreq):
                rates += [Rate(rate=a[0], pattern="Nominal Interest", freq=c)]

                tiers += [a[1]]

            gr = TieredTime(tiers=tiers, rates=rates)

        else:
            raise TypeError("Invalid type passed to alpha or cfreq")

    elif cgr is not None:

        gr = cgr

    elif (alpha is not None or cfreq is not None) and cgr is not None:
        raise ValueError(
            "When using cgr to specify coupon rate, leave alpha and cfreq blank."
        )
    else:
        raise Exception("Cannot determine coupon rate.")

    if isinstance(gr, Rate):
        alphas = gr.rate
        cfreqs = gr.freq
    elif isinstance(gr, TieredTime):
        alphas = [(x.rate, y) for x, y in zip(gr.rates, gr.tiers)]
        cfreqs = [x.freq for x in gr.rates]
    else:
        raise Exception("cgr parsing failed.")

    res = {'cgr': gr, 'alpha': alphas, 'cfreq': cfreqs}

    return res