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()
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 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]
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
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)
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 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
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
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 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
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()
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
def get_price_from_instrument(instrument: Payments): if isinstance(instrument, Bond): return instrument.price else: return instrument.npv()
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)
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)
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
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.')