Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NTN-B Class #49

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
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