<jupyter_start><jupyter_text>Brazilian Securities--- <NAME>  Brazilian Securities--- <NAME> Lecture 2- Flat-forward interpolation with COPOM Meetings- Treasury Direct contracts	- Floating: LFT	- Fixed: LTN, NTN-F	- Indexed: NTN-B, Bullet NTN-B- ANBIMA Data- Bootstrap- Nelson-Siegel coeficients ANBIMA- Implied Inflation<jupyter_code>%load_ext autoreload
%autoreload 2
import datetime
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from bcb import currency, sgs
import bizdays

import myfuncs as my<jupyter_output><empty_output><jupyter_text>DI x PRE Curve<jupyter_code>df = pd.read_parquet('data/contracts_202109_202111.parquet').reset_index(drop=True)
di1 = df[(df['Mercadoria'] == 'DI1') &
         (df['PUAtual'] != 100000.0) &
         (df['DataRef'] == pd.to_datetime('2021-11-01'))].copy()
MARKET_CALENDAR = bizdays.Calendar.load('ANBIMA.cal')
di1['Maturity'] = di1['Vencimento'].map(MARKET_CALENDAR.following)
di1['DU'] = di1.apply(lambda x: MARKET_CALENDAR.bizdays(x['DataRef'], x['Maturity']), axis=1)
di1['Rate'] = (100000 / di1['PUAtual'])**(252 / di1['DU']) - 1
di1_curve = di1[['DataRef', 'Maturity', 'DU', 'Rate']]
di1_curve.plot(x='Maturity', y='Rate', figsize=(20,6), style='-o',
               ylabel='Rate', xlabel='Date', title='DI1 Curve - 2021-11-01');<jupyter_output><empty_output><jupyter_text>Completing the curve with daily CDI<jupyter_code>di1_curve_h = di1_curve.reset_index(drop=True).head()
cdi = sgs.get(('CDI', 4389), start_date='2021-11-01', end_date='2021-11-01')
first_term = pd.DataFrame({
    'DataRef': datetime.datetime(2021,11,1),
    'Maturity': ['2021-11-03'],
    'DU': [1],
    'Rate': [cdi.iloc[0, 0]/100]
})
di1_curve_h = pd.concat([first_term, di1_curve_h], axis=0).reset_index(drop=True)
di1_curve_h['DataRef'] = pd.to_datetime(di1_curve_h['DataRef'])
di1_curve_h<jupyter_output><empty_output><jupyter_text>Exponential Flat Forward Interpolation<jupyter_code>log_pu = np.log((1 + di1_curve_h['Rate'])**(di1_curve_h['DU']/252))
terms = di1_curve_h['DU']
i_terms = np.array(range(terms.min(), terms.max()))
pu = np.exp(np.interp(i_terms, terms, log_pu))
i_curve = pd.DataFrame({
    'DU': i_terms,
    'Maturity': MARKET_CALENDAR.vec.offset('2021-11-01', i_terms),
    'Rate': pu ** (252 / i_terms) - 1
})
i_curve['Factor'] = (1 + i_curve['Rate'])**(i_curve['DU']/252)
i_curve['Factor_p'] = i_curve['Factor'].shift(1)
i_curve['FwdRate'] = (i_curve['Factor']/i_curve['Factor_p'])**252 - 1
plt.figure(figsize=(20,6))
plt.xlabel('Rate')
plt.title('DI1 Curve')
plt.plot(i_curve['Maturity'], i_curve['Rate'], '-o')
plt.plot(i_curve['Maturity'], i_curve['FwdRate'], 'o')
plt.plot(pd.to_datetime(di1_curve_h['Maturity']), di1_curve_h['Rate'], 'o', color='red');
plt.legend(['Interpolated', 'Daily Forward Rates', 'Curve'])
plt.show()<jupyter_output><empty_output><jupyter_text>COPOM Meetings<jupyter_code>rates = sgs.get(dict(SELICTARGET=432, CDI=4389), start_date='2021-09-01', end_date='2021-12-31')
plt.figure(figsize=(20,6))
plt.plot(rates[0].index, rates[0]);
plt.plot(rates[1].index, rates[1]);
plt.legend(['SELIC Target', 'CDI']);<jupyter_output><empty_output><jupyter_text>In the same chart:- Historical SELIC Target- Daily Forward Rates<jupyter_code>plt.figure(figsize=(20,6))
plt.xlabel('Rate')
plt.title('DI1 Curve')
plt.plot(rates[0].index, rates[0]/100, 'o')
plt.plot(i_curve['Maturity'], i_curve['FwdRate'], 'o')
plt.plot(pd.to_datetime(di1_curve_h['Maturity']), di1_curve_h['Rate'], 'o', color='red');
plt.legend(['SELICTARGET', 'Daily Forward Rates', 'Curve'])
plt.show()<jupyter_output><empty_output><jupyter_text>COPOM Meetings considerations* Is is a good idea to consider the COPOM Meetings in the interpolation.* Specially the **first meeting** that is more clear and presents a direct result.  Define the rates before the COPOM Meeting* On 2021-11-01, the next COPOM Meeting occurs at 2021-12-08.* Up to the next meeting, the interpolation must consider the current CDI rate available.* The first future matures at 2021-12-01 (Z21), before the first meeting.    * Its price might be slightly different.    * Once we don't have any new information, the CDI must be kept.<jupyter_code>i_curve['COPOM_Rate'] = np.nan
_cdi = cdi.values[0, 0]/100
i_curve.loc[i_curve['Maturity'] <= pd.to_datetime('2021-12-08'), 'COPOM_Rate'] = _cdi
plt.figure(figsize=(20,6))
plt.xlabel('Rate')
plt.title('DI1 Curve')
plt.plot(i_curve['Maturity'], i_curve['FwdRate'], 'o')
plt.plot(i_curve['Maturity'], i_curve['COPOM_Rate'], 'x')
plt.plot(pd.to_datetime(di1_curve_h['Maturity']), di1_curve_h['Rate'], 'o', color='red');
plt.legend(['Daily Forward Rates', 'COPOM Rates', 'Curve'])
plt.show()<jupyter_output><empty_output><jupyter_text>Define the rates after the COPOM Meeting* The second furure expires at 2022-01-01 (F22) is influenced by the decision taken on the this next meeting.* The spot rate $r(t, T)$ reffers to the second future and can be written as$$(1 + r(t,T))^{DU(t,T)/252} = (1 + CDI_t)^{DU(t,C)/252} (1 + CDI_{a})^{DU(C,T)/252}$$where- $C$: COPOM Meeting Date- $CDI_t$: current CDI rate- $CDI_a$: the expected CDI rate after the meeting<jupyter_code>f22_rate = di1.loc[di1['CDVencimento'] == 'F22', 'Rate'].values[0]
f22_DU = di1.loc[di1['CDVencimento'] == 'F22', 'DU'].values[0]
C_DU = MARKET_CALENDAR.bizdays('2021-11-01', '2021-12-08')

f1 = (1 + f22_rate) ** (f22_DU/252)
f2 = (1 + _cdi) ** (C_DU/252)

rate_C = (f1 / f2) ** (252 / (f22_DU - C_DU)) - 1
ix = (pd.to_datetime('2021-12-08') < i_curve['Maturity']) & \
     (i_curve['Maturity'] <= pd.to_datetime('2022-01-03'))
i_curve.loc[ix, 'COPOM_Rate'] = rate_C
plt.figure(figsize=(20,6))
plt.xlabel('Rate')
plt.title('DI1 Curve')
plt.plot(i_curve['Maturity'], i_curve['FwdRate'], 'o')
plt.plot(i_curve['Maturity'], i_curve['COPOM_Rate'], 'x')
plt.plot(pd.to_datetime(di1_curve_h['Maturity']), di1_curve_h['Rate'], 'o', color='red');
plt.legend(['Daily Forward Rates', 'COPOM Rates', 'Curve'])
plt.show()<jupyter_output><empty_output><jupyter_text>COPOM decision implied in the future* By the price of the future, we can see that the decision, that will be taken in the COPOM Meeting, is implied in the future price$$(1 + r(t,T))^{DU(t,T)/252} = (1 + CDI_t)^{DU(t,C)/252} (1 + CDI_{t} + u)^{DU(C,T)/252}$$where* $u$: the change in the spot rate announced in the COPOM Meeting<jupyter_code>f22_rate = di1.loc[di1['CDVencimento'] == 'F22', 'Rate'].values[0]
f22_DU = di1.loc[di1['CDVencimento'] == 'F22', 'DU'].values[0]
C_DU = MARKET_CALENDAR.bizdays('2021-11-01', '2021-12-08')

f1 = (1 + f22_rate) ** (f22_DU/252)
f2 = (1 + _cdi) ** (C_DU/252)

change = (f1 / f2) ** (252 / (f22_DU - C_DU)) - 1 - _cdi

print(change * 1e4, 'change in the spot rate (SELICTARGET) in bps')<jupyter_output>175.20749160745655 change in the spot rate (SELICTARGET) in bps<jupyter_text>Tesouro Direto -- Government Bonds* Government Bonds are daily traded in the Tesouro Direto* Contracts traded in Tesouro Direto    * LFT: Floating Rate Note    * LTN: Fixed Rate Note    * NTN-F: Fixed Rate Note with Coupons    * NTN-B: Inflation Indexed with Coupons    * NTN-B Principal: Inflation Indexed Bullet Bond  LFT* LFT - Letra Financeira do Tesouro is a **Floating Rate Bond**.    * It is a bullet bond, has no coupon payments. Present Value$$LFT = \frac{VNA}{(1 + s)^{DU/252}}$$$$VNA = 1000 \prod_{n=1}^T(1 + SELIC_{n})^{1/252}$$* $s$: small spread that is paid to enter in this contract    * $s$ can be treated as an risk factor, but unfortunately this is not *headgeable*, and also tiny.* $VNA$: Valor Nominal Atualizado - *Notional Accrued*    * $VNA$ is what the investor receives at maturity * ![](images/VNA_LFT_20211101.png)  LTN* LTN - Letra do Tesouro Nacional is a **Fixed Rate Bond**.    * It is a bullet bond, has no coupon payments Present Value$$VP_t = \frac{1000}{(1 + y_t)^T}$$  NTN-F* NTN-F - Nota Financeira do Tesouro Série F is a **Fixed Rate Bond**.    * It has semi annual coupon payments. Present Value$$VP_t = \sum_{k=1}^{N}\frac{1000\cdot[(1.10)^{0.5}-1]}{(1+y_t)^{t_k}} + \frac{1000}{(1 + y_t)^{t_N}}$$  NTN-B* NTN-F - Nota Financeira do Tesouro Série B is a **Fixed Rate Bond**.    * It has semi annual coupon payments.    * It is indexed to IPCA index (inflation index).    * It pays the inflation during a period plus an interest rate.    ![](images/fluxo-de-caixa-ntnb.png) --> Present Value$$VP_t = \sum_{k=1}^{N}\frac{VNA_t\cdot[(1.06)^{0.5}-1]}{(1+y_t)^{t_k}} + \frac{VNA_t}{(1 + y_t)^{t_N}}$$* $VNA_t$: is indexed to IPCA inflation index. * ![](images/VNA_NTN-B_20211101.png)  NTN-B Principal* NTN-B  Nota Financeira do Tesouro Série B is a **Fixed Rate Bond**.    * It is a bullet bond, has no coupon payments.    * This bond is mostly traded in retail, actually it has been striped from NTN-B bonds. Present Value$$VP_t = \frac{VNA_t}{(1 + c_t)^T}$$  Data* * *<jupyter_code>df = pd.read_parquet('data/2021-11-01.parquet')
df<jupyter_output><empty_output><jupyter_text>PREFIXADO Curve* LTN contracts plus SELIC rate are used to build a risk free term structure.<jupyter_code>ltn = df[df['symbol'] == 'LTN'].copy()

ltn['fixing'] = ltn['maturity_date'].map(MARKET_CALENDAR.following)
ltn['DU'] = list(MARKET_CALENDAR.vec.bizdays(ltn['refdate'], ltn['fixing']))

ltn_terms = ltn[['refdate', 'maturity_date', 'DU', 'ref_yield']]

selic = sgs.get({'SELIC': 1178}, start_date='2021-11-01', end_date='2021-11-01')
first_term = pd.DataFrame({
    'refdate': ['2021-11-01'],
    'maturity_date': ['2021-11-03'],
    'DU': [1],
    'ref_yield': [selic.iloc[0, 0]]
})

ltn_curve = pd.concat([first_term, ltn_terms], axis=0).reset_index(drop=True)
ltn_curve
plt.figure(figsize=(20,6))
plt.xlabel('Rate')
plt.title('SELIC Curve')
plt.plot(ltn_curve['maturity_date'], ltn_curve['ref_yield'], '-o')
plt.show()<jupyter_output><empty_output><jupyter_text>Divergences with NTN-F contracts* NTN-F and LTN are Fixed Rate Bonds, in order that they rely on the same risk structure and must be priced with the same curve.* Instead, once NTN-F bonds are priced with LTN rates, it presents small divergences.<jupyter_code>ntnfs = df[df['symbol'] == 'NTN-F'].copy()
ntnfs
ltn
class FixedRateBond:
    def __init__(self, **kwargs):
        self.calendar = kwargs['calendar']
        self.issue_date = kwargs['issue_date']
        self.maturity_date = kwargs['maturity_date']
        self._yield = kwargs['yield']
        self.notional = kwargs['notional']
        self.coupon_rate = kwargs['coupon_rate']
        self.coupon_frequency = kwargs['coupon_frequency']
        self.spot_price = kwargs['price']
        self.cashflow = self._cashflow()
        self.market_cashflow = self._market_cashflow()

    def _cashflow(self):
        if self.coupon_frequency:
            freq = '-{}MS'.format(int(self.coupon_frequency))
            dates = pd.date_range(start=self.maturity_date, end=self.issue_date, freq=freq, closed=None).sort_values()
            dates = dates.map(lambda dt: dt.replace(day=self.maturity_date.day))
            x = pd.DataFrame({
                'dates': dates,
                'coupons': self.notional * ((1 + self.coupon_rate)**(self.coupon_frequency/12) - 1),
                'amortizations': 0
            })
        else:
            dates = self.maturity_date
            x = pd.DataFrame({
                'dates': [dates],
                'coupons': [self.notional * ((1 + self.coupon_rate)**(self.coupon_frequency/12) - 1)],
                'amortizations': [0]
            })
        with pd.option_context('mode.chained_assignment', None):
            x['amortizations'].iloc[-1] = self.notional
        x['fixings'] = x['dates'].map(self.calendar.following)
        return x

    def _market_cashflow(self):
        global REFDATE
        cf = self.cashflow.copy()
        mcf = cf[cf['dates'] >= REFDATE].copy()
        mcf['DU'] = list(self.calendar.vec.bizdays(REFDATE, mcf['fixings']))
        mcf['payments'] = mcf['coupons'] + mcf['amortizations']
        return mcf

    def theoretical_value(self, discount_curve=None):
        mcf = self.market_cashflow
        if discount_curve is None:
            discount = 1 / ((1 + self._yield) ** (mcf['DU']/252))
        else:
            discount = discount_curve(mcf['DU'])
        return np.sum(mcf['payments'] * discount)


class NTNF(FixedRateBond):
    pass


class LTN(FixedRateBond):
    pass


class NTNB(FixedRateBond):
    pass<jupyter_output><empty_output><jupyter_text>Let's use the discount curve obtained from LTN bonds to a price NTN-F bond<jupyter_code>ntnf = ntnfs.iloc[0].to_dict()
REFDATE = datetime.datetime.strptime(ntnf['refdate'], '%Y-%m-%d')
ntnf['maturity_date'] = datetime.datetime.strptime(ntnf['maturity_date'], '%Y-%m-%d')
ntnf['issue_date'] = datetime.datetime.strptime(ntnf['issue_date'], '%Y-%m-%d')
ntnf['calendar'] = MARKET_CALENDAR
ntnf['notional'] = 1000
ntnf['yield'] = ntnf['ref_yield']/100
ntnf['coupon_rate'] = 0.1
ntnf['coupon_frequency'] = 6
bond = NTNF(**ntnf)
bond.market_cashflow<jupyter_output><empty_output><jupyter_text>* All these cashflows match with LTN maturities  Let's build a discount curve with LTN rates<jupyter_code>discount_curve_selic = my.ff_discount_curve(ltn_curve['DU'], ltn_curve['ref_yield']/100)
bond.spot_price
bond.theoretical_value(discount_curve=discount_curve_selic)<jupyter_output><empty_output><jupyter_text>ANBIMA Curves* ANBIMA uses Nelson Siegel Svensson term structure model to build a curve which attends both LTN and NTN-F bonds* The spot rate is given by$$r(t) = \beta_1 +\beta_2 \left( \frac{1 - e^{-\lambda_1 t}}{\lambda_1 t} \right) +\beta_3 \left( \frac{1 - e^{-\lambda_1 t}}{\lambda_1 t} -e^{-\lambda_1 t} \right) +\beta_4 \left( \frac{1 - e^{-\lambda_2 t}}{\lambda_2 t} -e^{-\lambda_2 t} \right)$$* The parameters are daily delivered in this link:    *  * We will load all LTN and NTN-F bonds and price these bonds with the term structure built with ANBIMA's parameters<jupyter_code>bonds = []

for ntnf in ntnfs.to_dict('records'):
    REFDATE = datetime.datetime.strptime(ntnf['refdate'], '%Y-%m-%d')
    ntnf['maturity_date'] = datetime.datetime.strptime(ntnf['maturity_date'], '%Y-%m-%d')
    ntnf['issue_date'] = datetime.datetime.strptime(ntnf['issue_date'], '%Y-%m-%d')
    ntnf['calendar'] = MARKET_CALENDAR
    ntnf['notional'] = 1000
    ntnf['yield'] = ntnf['ref_yield']/100
    ntnf['coupon_rate'] = 0.1
    ntnf['coupon_frequency'] = 6
    
    bond = NTNF(**ntnf)
    bonds.append(bond)
for _ltn in ltn.to_dict('records'):
    REFDATE = datetime.datetime.strptime(_ltn['refdate'], '%Y-%m-%d')
    _ltn['maturity_date'] = datetime.datetime.strptime(_ltn['maturity_date'], '%Y-%m-%d')
    _ltn['issue_date'] = datetime.datetime.strptime(_ltn['issue_date'], '%Y-%m-%d')
    _ltn['calendar'] = MARKET_CALENDAR
    _ltn['notional'] = 1000
    _ltn['yield'] = _ltn['ref_yield']/100
    _ltn['coupon_rate'] = 0
    _ltn['coupon_frequency'] = 0
    
    bond = LTN(**_ltn)
    bonds.append(bond)
# ANBIMA's parameters
par = (0.1229, -0.0606, 0.1004, -0.0175, 1.9174, 1.1923)

pre_discount_curve = my.nss_discount_curve(par)
errors = [100 * (b.theoretical_value(discount_curve=pre_discount_curve) - b.spot_price) / b.spot_price for b in bonds]
for e in errors:
    print(f'{e: 1.4f}')<jupyter_output>0.0680
-0.1007
 0.0675
 0.3222
-0.2989
 0.0125
-0.0589
 0.0031
 0.0146
 0.0121
-0.0425
-0.0702
 0.0021
 0.0656<jupyter_text>* We can see this curve<jupyter_code>pre_curve = my.nss_curve(par)

t = np.arange(252, 2520)
r = pre_curve(t)

plt.figure(figsize=(20, 6))
plt.plot(t, r)
plt.show()<jupyter_output><empty_output><jupyter_text>Real Interest Rate from NTN-B bonds* Once NTN-B is indexed to inflation, the interest rate paid additionally is the **real interest rate**.<jupyter_code>ntnbs = df[df['symbol'] == 'NTN-B'].copy()

bonds = []

for _bond in ntnbs.to_dict('records'):
    REFDATE = datetime.datetime.strptime(_bond['refdate'], '%Y-%m-%d')
    _bond['maturity_date'] = datetime.datetime.strptime(_bond['maturity_date'], '%Y-%m-%d')
    _bond['issue_date'] = datetime.datetime.strptime(_bond['issue_date'], '%Y-%m-%d')
    _bond['calendar'] = MARKET_CALENDAR
    _bond['notional'] = 3702.299176
    _bond['yield'] = _bond['ref_yield']/100
    _bond['coupon_rate'] = 0.06
    _bond['coupon_frequency'] = 6
    
    bond = NTNB(**_bond)
    bonds.append(bond)
# ANBIMA's parameters for real interest rates
par = (0.0557, -0.0766, 0.1308, -0.0326, 2.0284, 1.0558)

ipca_discount_curve = my.nss_discount_curve(par)
errors = [100 * (b.theoretical_value(discount_curve=ipca_discount_curve) - b.spot_price) / b.spot_price for b in bonds]
for e in errors:
    print(f'{e: 1.4f}')
ipca_curve = my.nss_curve(par)

t = np.arange(126, 2520)
r = ipca_curve(t)

plt.figure(figsize=(20, 6))
plt.plot(t, r)
plt.show()<jupyter_output><empty_output><jupyter_text>Expected Inflation* Having real and nominal rates, the future inflation can be estimated according to the following relation.$$(1 + c)(1 + i) = (1 + r)$$where* $r$ is the nominal interest rate obtained from LTN and NTN-F bonds* $c$ is the real interest rate obtained from NTN-B contracts* $i$ is the resulting inflation<jupyter_code>t = np.arange(126, 2520)
j = ipca_curve(t)
r = pre_curve(t)

f_r = (1 + r) ** (t/252)
f_j = (1 + j) ** (t/252)
i = (f_r/f_j) ** (252/t) - 1

plt.figure(figsize=(20, 6))
plt.plot(t, i)
plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1721150514034885, 'fraction_numerical': 0.04568557236918101, 'mean_word_length': 4.71075197889182, 'pattern_counts': {'":': 0, '<': 76, '<?xml version=': 0, '>': 75, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '18', 'dir': 'jupyter-structured-clean-dedup', 'id': '539006', 'license': 'MIT', 'n_tokens_mistral': 7324, 'n_tokens_neox': 6619, 'n_words': 1432, 'path': 'Brazilian Securities 2.ipynb', 'repo_name': 'wilsonfreitas/brazilian-securities-lectures', 'size': '17330'}