diff --git a/finmath/brazilian_bonds/government_bonds.py b/finmath/brazilian_bonds/government_bonds.py index a816156..6e33839 100644 --- a/finmath/brazilian_bonds/government_bonds.py +++ b/finmath/brazilian_bonds/government_bonds.py @@ -12,6 +12,18 @@ dc = DayCounts('bus/252', calendar='cdr_anbima') +def truncate(number, decimals=0): + """Returns a value truncated to a specific number of decimal places""" + if not isinstance(decimals, int): + raise TypeError("decimal places must be an integer.") + elif decimals < 0: + raise ValueError("decimal places has to be 0 or more.") + elif decimals == 0: + return np.trunc(number) + factor = 10.0 ** decimals + return np.trunc(number * factor) / factor + + class LTN(object): def __init__(self, @@ -57,16 +69,30 @@ def __init__(self, self.dv01 = (self.mod_duration / 100.) * self.price self.convexity = self.ytm * (1. + self.ytm) / (1. + self.rate) ** 2 - @staticmethod - def price_from_rate(principal: float = 1e6, + def price_from_rate(self, + principal: Optional[float] = None, rate: Optional[float] = None, - ytm: Optional[float] = None): - return principal / (1. + rate) ** ytm + ytm: Optional[float] = None, + truncate_price: bool = True): - @staticmethod - def rate_from_price(principal: float = 1e6, + principal = self.principal if principal is None else principal + rate = self.rate if rate is None else rate + ytm = self.ytm if ytm is None else ytm + # Adjusting according the Anbima specifications + pu = 10*np.round(100/((1 + rate)**ytm), 10) + if truncate_price: + pu = truncate(pu, 6) + + return principal/1000 * pu + + def rate_from_price(self, + principal: Optional[float] = None, price: Optional[float] = None, ytm: Optional[float] = None): + + principal = self.principal if principal is None else principal + price = self.price if price is None else price + ytm = self.ytm if ytm is None else ytm return (principal / price) ** (1. / ytm) - 1. @@ -95,24 +121,25 @@ def __init__(self, self.expiry = pd.to_datetime(expiry).date() self.ref_date = pd.to_datetime(ref_date).date() + self.principal = principal - interest = ((1. + coupon_rate) ** (1. / 2.) - 1.) * principal + interest = ((1. + coupon_rate) ** (1. / 2.) - 1.) * self.principal cash_flows = pd.Series(index=self.payment_dates(), data=interest).sort_index() - cash_flows.iloc[-1] += principal + cash_flows.iloc[-1] += self.principal self.cash_flows = cash_flows if rate is not None and price is None: - self.rate: float = float(rate) - self.price = self.price_from_rate() + self.rate = float(rate) + self.price = self.price_from_rate(principal=self.principal, rate=self.rate) elif rate is None and price is not None: self.price = float(price) - self.rate = self.rate_from_price() + self.rate = self.rate_from_price(price=self.price) else: - pt = self.price_from_rate() - if np.abs(pt - float(price)) / principal > 0.1: + pt = self.price_from_rate(principal=self.principal, rate=rate) + if np.abs(pt - float(price)) / self.principal > 0.1: msg = 'Given price and rate are incompatible!' warnings.warn(msg) self.rate = rate @@ -132,19 +159,32 @@ def payment_dates(self): return sorted(payd) - def price_from_rate(self) -> float: + def price_from_rate(self, + principal: Optional[float] = None, + rate: Optional[float] = None, + truncate_price: bool = True) -> float: pv = 0. + principal = self.principal if principal is None else principal + rate = self.rate if rate is None else rate for d, p in self.cash_flows.items(): - cf = LTN(d, rate=self.rate, principal=p, - ref_date=self.ref_date) - pv += cf.price - return float(pv) + # Adjusting according the Anbima specifications + p = np.round(100 * p / principal, 6) + pv += LTN(d, ref_date=self.ref_date, price=p).price_from_rate(p, rate, None, False) - def rate_from_price(self): - theor_p = lambda x: sum([LTN(d, rate=x, principal=p, - ref_date=self.ref_date).price - for d, p in self.cash_flows.items()]) - error = lambda x: (self.price - float(theor_p(x))) + if truncate_price: + pv = truncate(10*pv, 6) + # Adjusting back according to the intended principal + return pv * principal / 1000 + + def rate_from_price(self, + price: Optional[float] = None): + + price = self.price if price is None else price + theor_p = lambda x: sum([ + LTN(d, ref_date=self.ref_date, price=p).price_from_rate(p, x, None, False) + for d, p in self.cash_flows.items() + ]) + error = lambda x: (price - float(theor_p(x))) return optimize.brentq(error, 0., 1.) @@ -162,3 +202,132 @@ def calculate_risk(self): convexity = (convexity / self.price) / (1. + self.rate) ** 2 return mod_duration, convexity + + +class NTNB(object): + + def __init__(self, + expiry: Date, + rate: Optional[float] = None, + price: Optional[float] = None, + coupon_rate: float = 0.06, + vna: Optional[float] = None, + ref_date: Date = TODAY): + """ + Class constructor. + This is a Brazilian government bond that pays coupons every six months + :param expiry: bond expiry date + :param rate: bond yield + :param price: bond price + :param coupon_rate: bond coupon rate + :param vna: the inflation index used for the price calculation + :param ref_date: reference date for price or rate calculation + """ + + msg = 'Parameters rate and price cannot be both None!' + assert rate is not None or price is not None, msg + + if rate is not None: + msg_2 = 'Parameters price and vna cannot be both None!' + assert price is not None or vna is not None, msg_2 + + if price is not None: + msg_3 = 'Parameters rate and vna cannot be both None!' + assert rate is not None or vna is not None, msg_3 + + self.expiry = pd.to_datetime(expiry).date() + self.ref_date = pd.to_datetime(ref_date).date() + + interest = ((1. + coupon_rate) ** (1. / 2.) - 1.) * 100 + cash_flows = pd.Series(index=self.payment_dates(), + data=interest).sort_index() + cash_flows.iloc[-1] += 100 + + self.cash_flows = cash_flows + + if price is not None and rate is not None and vna is None: + self.price = float(price) + self.rate = float(rate) + base_price = self.price_from_rate(rate=self.rate, vna=1000) + self.vna = np.round(self.price / base_price * 1000, 6) + elif rate is not None and price is None and vna is not None: + self.vna = float(vna) + self.rate = float(rate) + self.price = self.price_from_rate(rate=self.rate, vna=self.vna) + elif rate is None and price is not None and vna is not None: + self.vna = float(vna) + self.price = float(price) + self.rate = self.rate_from_price(price=self.price, vna=self.vna) + + else: + pt = self.price_from_rate(rate=rate, vna=vna) + if np.abs(pt - float(price)) > 0.1: + msg = 'Given price and rate are incompatible!' + warnings.warn(msg) + self.rate = rate + self.price = price + self.vna = float(vna) + + self.mod_duration, self.convexity = self.calculate_risk + self.macaulay = self.mod_duration * (1. + self.rate) + self.dv01 = (self.mod_duration / 100.) * self.price + + def payment_dates(self): + + payd = [dc.following(self.expiry)] + d = dc.workday(dc.eom(dc.following(self.expiry), offset=-7) + pd.DateOffset(days=14), 1) + while d > dc.following(self.ref_date): + payd += [d] + d = dc.workday(dc.eom(d, offset=-7)+pd.DateOffset(days=14), 1) + + return sorted(payd) + + def price_from_rate(self, + rate: Optional[float] = None, + vna: Optional[float] = None, + truncate_price: bool = True) -> float: + pv = 0. + rate = self.rate if rate is None else rate + vna = self.vna if vna is None else vna + + for d, p in self.cash_flows.items(): + # Adjusting according the Anbima specifications + p = np.round(p , 6) + pv += LTN(d, ref_date=self.ref_date, price=p).price_from_rate(p, rate, None, False) + + pv = truncate(pv, 4) * np.round(vna, 6) / 100 + if truncate_price: + pv = truncate(pv, 6) + return pv + + def rate_from_price(self, + price: Optional[float] = None, + vna: Optional[float] = None): + + price = self.price if price is None else price + vna = self.vna if vna is None else vna + price /= vna + price *= 100 + + theor_p = lambda x: sum([ + LTN(d, ref_date=self.ref_date, price=p).price_from_rate(p, x, None, False) + for d, p in self.cash_flows.items() + ]) + error = lambda x: (price - float(theor_p(x))) + + return optimize.brentq(error, -0.99, 1.) + + @property + def calculate_risk(self): + macaulay = 0. + convexity = 0. + for d, p in self.cash_flows.items(): + pv = p / (1. + self.rate) ** dc.tf(self.ref_date, d) + t = dc.tf(self.ref_date, d) + macaulay += t * pv + convexity += t * (1 + t) * pv + macaulay = macaulay / self.price + mod_duration = macaulay / (1. + self.rate) + convexity = (convexity / self.price) / (1. + self.rate) ** 2 + + return mod_duration, convexity