Skip to content
215 changes: 192 additions & 23 deletions finmath/brazilian_bonds/government_bonds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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.


Expand Down Expand Up @@ -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
Expand All @@ -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.)

Expand All @@ -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