For many years I've been working for and with software vendors in the brazilian financial industry. The great majority of pricing and risk management aplications running on here were not developed here in Brazil. I think that is the main reason I saw many projects struggling to deliver a glimpse of what had been promissed during the software selection meetings. One thing that is usually said is that brazilian market has its jabuticabas, yeah jabuticaba. It is a sweet fruit which grows in Brazil, vastly. And like the fruit jabuticaba, the brazilian financial market has a bunch of financial instruments with annoying characteristics such as:
- business days rules
- truncation rules, mainly for interest rates
- stupid inflation rules
- the PTAX currency which yields the dirty coupon for the foreign interest rate
- and many others
In this post I want to start a series where will show the steps to validate the pricing rules for a few brazilian financial instruments. I am going to start with the brazilian government floating rate bond called LFT or Letras Financeiras do Tesouro (Treasury Financial Letters). Despite being a Floating Rate Bond, LFT is traded above its face value and a spread, usually quite small and negative, is used to discount it. These calculations seem to be pretty simple but it hides some pitfalls and because of that we are going to go on step by step, paying attention to details.
The instrument
As mentioned before, LFT is a Floating Rate Bond so it accumulates daily interest rate from its issue date up to maturity. The LFT's issue value is R$ 1000,00 then the fair value of a LFT is
$$ LFT_t = 1000 \prod_{n=t_i}^{t-1}(1 + r_{n})^\frac{1}{252} $$
where $t$ refers to the mark-to-market date, $t_i$ the issue date and $t-1$ the business day before mark-to-market date. However, it is usually traded in values higher than the fair value as if a negative spread was applied discounting the fair value, leading to:
$$ LFT_t = \frac{1000 \prod_{n=t_i}^{t-1}(1 + r_{n})^\frac{1}{252}}{(1 + s_t)^{D_T/252}} $$
where $s_t$ is the spread and $D_T$ the number of business days up to maturity.
Notional Value — VNA
The rule to compute the notional value (VNA—Valor Nominal Acumulado) is fairly simple, accumulate the daily interest rate from LFT's issue date up to reference date (mark-to-market date).
What do we need?
- issue date
- reference date
- daily brazilian government interest rate
The LFT's issue date is 2000-07-01 and our reference date is 2014-03-21. Let's start setting the reference date:
ref_date <- as.Date('2014-03-21')
The historical interest rates are downloaded from Quandl and data in properly transformed to suitable formats:
library(Quandl)
selic <- Quandl('BCB/1178', start_date='2000-07-01', end_date='2014-03-21', order='asc')
selic <- transform(selic, Date=as.Date(Date))
str(selic)
## 'data.frame': 3448 obs. of 2 variables:
## $ Date : Date, format: "2000-07-03" "2000-07-04" ...
## $ Value: num 17.3 17.2 17.2 17.3 17.3 ...
Computing VNA
The formula to compute VNA is
$$ VNA = 1000\prod_{n=1}^{T}(1 + r_{n-i})^\frac{1}{252} $$
where T
refers to issue date.
As I said before, the formula is pretty simple but hides inconvenient secrets.
This formula is computed in 3 steps, enumerated below:
- Compute the interest rate factor $(1 + r_i)^\frac{1}{252}$ and round it in 8 digits
- Compute the accumulated factor, using
cumprod
- Compute the VNA multiplying the accumulated factor by 1000 and truncate it in 6 digits
The final value is shifted one day because the accumulated amount refers to the day after the interest rate is released.
The function compute_vna
below execute these steps.
compute_vna <- function(ds) {
ds <- transform(ds, Factor=round((1 + Value/100)^(1/252), 8))
ds <- transform(ds, CumFactor=cumprod(Factor))
ds <- transform(ds, VNA=truncate(1000*CumFactor, 6))
ds$VNA <- with(ds, c(NA, VNA[1:(length(VNA)-1)]))
ds
}
selic <- compute_vna(selic)
head(selic)
## Date Value Factor CumFactor VNA
## 1 2000-07-03 17.26 1.00063204 1.000632040 NA
## 2 2000-07-04 17.22 1.00063068 1.001263119 1000.632040
## 3 2000-07-05 17.23 1.00063102 1.001894936 1001.263118
## 4 2000-07-06 17.28 1.00063272 1.002528855 1001.894935
## 5 2000-07-07 17.28 1.00063272 1.003163175 1002.528854
## 6 2000-07-10 17.01 1.00062356 1.003788707 1003.163174
tail(selic)
## Date Value Factor CumFactor VNA
## 3443 2014-03-14 10.65 1.00040168 6.013481466 6011.066940
## 3444 2014-03-17 10.65 1.00040168 6.015896961 6013.481465
## 3445 2014-03-18 10.65 1.00040168 6.018313427 6015.896961
## 3446 2014-03-19 10.65 1.00040168 6.020730863 6018.313426
## 3447 2014-03-20 10.65 1.00040168 6.023149270 6020.730862
## 3448 2014-03-21 10.65 1.00040168 6.025568648 6023.149269
Observe the VNA for the reference date, we will use this value to compute the theoretical value.
Computing the theoretical value
To compute the LFT's theoretical value we need the terms and conditions of the contracts. That information can be obtained at ANBIMA's web site, there is a link to a text file which contains relevant information to price all brazilian government bonds. Unfortunately, only the last 5 business days are kept available to download. The file used to validate the prices can be found here.
The code below loads and prepares data for pricing.
tit_pub <- read.table('ms140321.txt', skip=2, sep='@', header=TRUE)
lft_quot <- subset(tit_pub, Titulo == 'LFT')
lft_quot <- lft_quot[, c('Data.Referencia', 'Data.Vencimento', 'Tx..Indicativas', 'PU')]
names(lft_quot) <- c('RefDate', 'Maturity', 'Spread', 'SpotPrice')
lft_quot <- transform(lft_quot,
Spread=as.numeric(sub(',', '.', Spread)),
SpotPrice=as.numeric(sub(',', '.', SpotPrice)),
RefDate=as.Date(as.character(RefDate), format='%Y%m%d'),
Maturity=as.Date(as.character(Maturity), format='%Y%m%d'))
lft_quot
## RefDate Maturity Spread SpotPrice
## 16 2014-03-21 2014-09-07 -0.0146 6023.552820
## 17 2014-03-21 2015-03-07 -0.0157 6024.058764
## 18 2014-03-21 2015-09-07 -0.0169 6024.636986
## 19 2014-03-21 2016-03-01 -0.0179 6025.233278
## 20 2014-03-21 2016-09-07 -0.0183 6025.865709
## 21 2014-03-21 2017-03-07 -0.0196 6026.630649
## 22 2014-03-21 2017-09-07 -0.0205 6027.419681
## 23 2014-03-21 2018-03-01 -0.0209 6028.088251
## 24 2014-03-21 2018-09-01 -0.0209 6028.732728
## 25 2014-03-21 2019-03-01 -0.0217 6029.588015
## 26 2014-03-21 2020-03-01 -0.0228 6031.280520
We observe 11 instruments and we have selected all necessary fields to price them properly, now we have a few adjustments. Firstly, we have to move the maturity to the next business day if it is not a business day and after that we have to find out the number of business days between the reference and maturity dates. These tasks can be easily done with the bizdays package, developed by me.
bizdays works with calendars as a list of dates representing non-working days.
Different from other packages which work with algorithmic calendar implementations, bizdays can have any calendar set simply using a list of dates.
It relies on your needs.
I've used the dataset holidaysANBIMA
which is part of bizdays.
This calendar has all financial holidays practiced in Brazil.
The function adjust.next
move the given date to the following if it is not a business day and
the function bizdays
returns the amount of business days between 2 dates.
Look that both functions work vectorized.
library(bizdays)
cal <- Calendar(holidays=holidaysANBIMA, name='ANBIMA', weekdays=c('saturday', 'sunday'))
## Warning in Calendar(holidays = holidaysANBIMA, name = "ANBIMA", weekdays
## = c("saturday", : This function will be deprecated, use create.calendar
## instead.
lft_quot <- transform(lft_quot, MaturityAdj=adjust.next(Maturity, cal))
lft_quot <- transform(lft_quot, BusinessDays=bizdays(ref_date, MaturityAdj, cal))
lft_quot
## RefDate Maturity Spread SpotPrice MaturityAdj BusinessDays
## 16 2014-03-21 2014-09-07 -0.0146 6023.552820 2014-09-08 117
## 17 2014-03-21 2015-03-07 -0.0157 6024.058764 2015-03-09 243
## 18 2014-03-21 2015-09-07 -0.0169 6024.636986 2015-09-08 369
## 19 2014-03-21 2016-03-01 -0.0179 6025.233278 2016-03-01 488
## 20 2014-03-21 2016-09-07 -0.0183 6025.865709 2016-09-08 621
## 21 2014-03-21 2017-03-07 -0.0196 6026.630649 2017-03-07 744
## 22 2014-03-21 2017-09-07 -0.0205 6027.419681 2017-09-08 872
## 23 2014-03-21 2018-03-01 -0.0209 6028.088251 2018-03-01 989
## 24 2014-03-21 2018-09-01 -0.0209 6028.732728 2018-09-03 1118
## 25 2014-03-21 2019-03-01 -0.0217 6029.588015 2019-03-01 1241
## 26 2014-03-21 2020-03-01 -0.0228 6031.280520 2020-03-02 1492
We see that several dates have been correctly adjusted to their following dates and using these new dates the number of business days have been computed.
The code below computes the theoretical price.
Note that the Quotation
is truncated in 4 digits and the theoretical price in 6.
lft_quot <- transform(lft_quot, Quotation=truncate(100/(1 + Spread/100)^(BusinessDays/252), 4))
VNA <- with(selic, VNA[Date == ref_date])
lft_quot <- transform(lft_quot, TheoPrice=truncate(VNA*Quotation/100, 6))
After all those steps we are able to compare theoretical and spot price.
print(with(lft_quot, SpotPrice-TheoPrice))
## [1] 0 0 0 0 0 0 0 0 0 0 0
all(with(lft_quot, SpotPrice-TheoPrice) == 0)
## [1] TRUE
Since the output is TRUE
we all have to agree that these values match.
As I said this is pretty much easy once you know all truncations and roundings that appears along the way.
Another tricky point is that we can't invert that formula analytically, to find out the spread having the spot price of the bond.
Because of the truncations we have to use that algorithm and implement a kind of goal seek mechanism to obtain the spread.