Calculate Bond Portfolio Cashflows
The code for this workbook is available on my GitHub account at the URL:
https://github.com/kaybenleroll/data_workshops
The materials are in the directory talk_sai_bondcashflow_202202.
1 Load Data
We first want to load our datasets and prepare them for some simple association rules mining.
1.1 Load Portfolio Data
We load a first example of our portfolio, containing the basic information we need to properly account for cashflows.
portfolio_cols <- cols(
issuer = col_character(),
currency = col_character(),
coupon = col_number(),
issued_date = col_date(format = "%Y-%m-%d"),
maturity_date = col_date(format = "%Y-%m-%d"),
price = col_number(),
position = col_integer()
)
bond_portfolio_tbl <- read_csv(
file = "data/sample_bond_portfolio.csv",
na = c("N/A"),
col_types = portfolio_cols
) %>%
mutate(
ticker_symbol = str_replace(issuer, "^(\\S+) .*", "\\1"),
mkt_value = position * price * 0.01 * 1000
)
bond_portfolio_tbl %>% glimpse()## Rows: 250
## Columns: 9
## $ issuer [3m[38;5;246m<chr>[39m[23m "ECOM 5.875 10/14/2022", "PSBQ 6.5 06/25/2020", "ALYA 9.…
## $ currency [3m[38;5;246m<chr>[39m[23m "USD", "USD", "CAD", "USD", "USD", "USD", "USD", "USD", …
## $ position [3m[38;5;246m<int>[39m[23m 172, 234, 164, 207, 308, 201, 138, 215, 236, 155, 223, 1…
## $ price [3m[38;5;246m<dbl>[39m[23m 91.73, 96.57, 97.75, 106.31, 104.39, 99.59, 93.93, 101.2…
## $ coupon [3m[38;5;246m<dbl>[39m[23m 5.875, 6.500, 9.625, 7.000, 9.125, 9.000, 5.625, 5.125, …
## $ issued_date [3m[38;5;246m<date>[39m[23m 2017-10-13, 2016-06-24, 2017-12-27, 2017-05-17, 2015-04…
## $ maturity_date [3m[38;5;246m<date>[39m[23m 2022-10-14, 2020-06-25, 2021-12-28, 2023-05-18, 2023-04…
## $ ticker_symbol [3m[38;5;246m<chr>[39m[23m "ECOM", "PSBQ", "ALYA", "BSRSF", "MTX", "LOGM", "DLR", "…
## $ mkt_value [3m[38;5;246m<dbl>[39m[23m 157775.6, 225973.8, 160310.0, 220061.7, 321521.2, 200175…
1.2 Load Market Capitalisation Data
We will want to segment our bond portfolio by market capitalisation and so we load up the market cap data. It is provided by SEC CIK number, so we will need to link by this.
mktcap_cols <- cols(
cik_number = col_character(),
year = col_number(),
mktcap = col_double()
)
mktcap_tbl <- read_csv(
file = "data/company_mktcap.csv",
col_types = mktcap_cols
)
mktcap_tbl %>% glimpse()## Rows: 5,802
## Columns: 3
## $ year [3m[38;5;246m<dbl>[39m[23m 2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018,…
## $ cik_number [3m[38;5;246m<chr>[39m[23m "0000878334", "0001081862", "0000868822", "0000749098", "00…
## $ mktcap [3m[38;5;246m<dbl>[39m[23m 3162687931, 16810662751, 8001655, 15196648088, 35505321530,…
1.3 Load Company Information
We now load the various data we have on each individual company to help with the analysis of the portfolio.
company_cols <- cols(
cik_number = col_character(),
company_name = col_character(),
main_exchange = col_character(),
country_code = col_character(),
ticker_symbol = col_character(),
gics_code = col_character()
)
company_info_tbl <- read_csv(
file = "data/company_information.csv",
na = c("N/A"),
col_types = company_cols
)
company_info_tbl %>% glimpse()## Rows: 262
## Columns: 6
## $ cik_number [3m[38;5;246m<chr>[39m[23m "0001018724", "0000732712", "0000051143", "0000858877", …
## $ company_name [3m[38;5;246m<chr>[39m[23m "AMAZON.COM, INC.", "VERIZON COMMUNICATIONS INC", "INTER…
## $ main_exchange [3m[38;5;246m<chr>[39m[23m "NASDAQ National Market", "New York Stock Exchange (NYSE…
## $ country_code [3m[38;5;246m<chr>[39m[23m "US", "US", "US", "US", "US", "US", "US", "US", "US", "U…
## $ ticker_symbol [3m[38;5;246m<chr>[39m[23m "AMZN", "VZ", "IBM", "CSCO", "QCOM", "TJX", "PM", "HPE",…
## $ gics_code [3m[38;5;246m<chr>[39m[23m "25502020", "50101020", "45102010", "45201020", "4530102…
The sector information is provided in this data via the GICS code, so we need
to load the GICS information which is availabe via the GICS package.
data(standards)
gics_tbl <- standards %>%
as_tibble() %>%
transmute(
gics_code = `subindustry id`,
gics_sector_code = `sector id`,
gics_sector_name = `sector name`,
gics_industrygroup_code = `industry group id`,
gics_industrygroup_name = `industry group name`,
gics_industry_code = `industry id`,
gics_industry_name = `industry name`,
gics_subindustrygroup_code = `subindustry id`,
gics_subindustrygroup_name = `subindustry name`
) %>%
mutate(
across(everything(), as.character)
)
gics_tbl %>% glimpse()## Rows: 157
## Columns: 9
## $ gics_code [3m[38;5;246m<chr>[39m[23m "10101010", "10101020", "10102010", "101020…
## $ gics_sector_code [3m[38;5;246m<chr>[39m[23m "10", "10", "10", "10", "10", "10", "10", "…
## $ gics_sector_name [3m[38;5;246m<chr>[39m[23m "Energy", "Energy", "Energy", "Energy", "En…
## $ gics_industrygroup_code [3m[38;5;246m<chr>[39m[23m "1010", "1010", "1010", "1010", "1010", "10…
## $ gics_industrygroup_name [3m[38;5;246m<chr>[39m[23m "Energy", "Energy", "Energy", "Energy", "En…
## $ gics_industry_code [3m[38;5;246m<chr>[39m[23m "101010", "101010", "101020", "101020", "10…
## $ gics_industry_name [3m[38;5;246m<chr>[39m[23m "Energy Equipment & Services", "Energy Equi…
## $ gics_subindustrygroup_code [3m[38;5;246m<chr>[39m[23m "10101010", "10101020", "10102010", "101020…
## $ gics_subindustrygroup_name [3m[38;5;246m<chr>[39m[23m "Oil & Gas Drilling", "Oil & Gas Equipment …
1.4 Retrieve Frech-Fama Breakpoints
if(file_exists("data/ME_Breakpoints_CSV.zip")) {
me_breakpoint_url <- "http://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/ME_Breakpoints_CSV.zip"
curl_download(me_breakpoint_url, destfile = "data/ME_Breakpoints_CSV.zip")
}We now need to process this breakpoint data.
read_lines("data/ME_Breakpoints_CSV.zip") %>%
enframe(name = "id", value = "line_str") %>%
mutate(char_count = map_int(line_str, nchar), .after = id) %>%
filter(char_count == 232) %>%
pull(line_str) %>%
write_lines("data/breakpoints_parsed.csv")
csv_colnames <- c(
"datestr", "data_count",
"ME00", "ME05", "ME10", "ME15", "ME20",
"ME25", "ME30", "ME35", "ME40", "ME45",
"ME50", "ME55", "ME60", "ME65", "ME70",
"ME75", "ME80", "ME85", "ME90", "ME95"
)
csv_cols <- cols(
datestr = col_character(),
data_count = col_integer()
)
me_breakpoint_tbl <- read_csv(
file = "data/breakpoints_parsed.csv",
col_names = csv_colnames,
col_types = csv_cols
)
me_breakpoint_tbl %>% glimpse()## Rows: 1,153
## Columns: 22
## $ datestr [3m[38;5;246m<chr>[39m[23m "192512", "192601", "192602", "192603", "192604", "192605",…
## $ data_count [3m[38;5;246m<int>[39m[23m 487, 491, 499, 503, 505, 509, 510, 506, 517, 514, 521, 520,…
## $ ME00 [3m[38;5;246m<dbl>[39m[23m 1.40, 1.38, 1.29, 1.12, 1.31, 1.14, 1.08, 1.08, 1.07, 1.06,…
## $ ME05 [3m[38;5;246m<dbl>[39m[23m 2.38, 2.54, 2.34, 2.05, 2.35, 2.25, 2.20, 2.04, 2.35, 2.29,…
## $ ME10 [3m[38;5;246m<dbl>[39m[23m 3.37, 3.76, 3.57, 3.08, 3.32, 3.15, 3.44, 3.15, 3.34, 3.12,…
## $ ME15 [3m[38;5;246m<dbl>[39m[23m 4.95, 4.84, 4.69, 4.15, 4.37, 4.20, 4.33, 4.25, 4.27, 4.23,…
## $ ME20 [3m[38;5;246m<dbl>[39m[23m 5.95, 5.94, 5.61, 5.01, 5.03, 5.10, 5.20, 5.28, 5.38, 5.18,…
## $ ME25 [3m[38;5;246m<dbl>[39m[23m 7.40, 7.47, 7.15, 6.01, 6.63, 6.56, 6.87, 6.90, 6.81, 6.64,…
## $ ME30 [3m[38;5;246m<dbl>[39m[23m 8.89, 9.28, 8.45, 7.39, 8.13, 7.96, 8.25, 8.52, 8.78, 8.75,…
## $ ME35 [3m[38;5;246m<dbl>[39m[23m 10.81, 10.94, 10.53, 9.00, 9.60, 9.68, 9.79, 10.07, 10.31, …
## $ ME40 [3m[38;5;246m<dbl>[39m[23m 13.01, 12.71, 12.15, 10.68, 11.66, 11.55, 12.00, 12.04, 12.…
## $ ME45 [3m[38;5;246m<dbl>[39m[23m 15.61, 15.90, 14.62, 13.20, 13.75, 13.27, 13.60, 13.80, 14.…
## $ ME50 [3m[38;5;246m<dbl>[39m[23m 18.55, 17.75, 17.03, 15.65, 15.81, 15.62, 15.50, 16.12, 16.…
## $ ME55 [3m[38;5;246m<dbl>[39m[23m 22.88, 23.45, 21.18, 19.04, 19.17, 18.87, 19.50, 19.67, 20.…
## $ ME60 [3m[38;5;246m<dbl>[39m[23m 29.92, 30.40, 27.62, 24.80, 25.60, 25.65, 26.52, 27.10, 26.…
## $ ME65 [3m[38;5;246m<dbl>[39m[23m 38.41, 39.00, 36.55, 31.90, 33.10, 33.77, 34.67, 34.88, 34.…
## $ ME70 [3m[38;5;246m<dbl>[39m[23m 49.93, 50.85, 46.49, 44.40, 45.00, 45.60, 47.32, 49.09, 47.…
## $ ME75 [3m[38;5;246m<dbl>[39m[23m 65.75, 66.82, 61.43, 58.24, 57.73, 57.81, 59.40, 65.49, 60.…
## $ ME80 [3m[38;5;246m<dbl>[39m[23m 92.72, 94.69, 87.51, 81.92, 83.63, 85.27, 84.30, 88.43, 87.…
## $ ME85 [3m[38;5;246m<dbl>[39m[23m 142.48, 139.18, 138.04, 130.12, 133.50, 125.90, 135.34, 136…
## $ ME90 [3m[38;5;246m<dbl>[39m[23m 238.78, 235.20, 223.02, 205.20, 211.20, 209.25, 219.45, 225…
## $ ME95 [3m[38;5;246m<dbl>[39m[23m 1319.00, 1331.71, 1366.39, 1322.46, 1350.21, 1382.58, 1510.…
1.5 Futures Expiration Dates
We also want to load the expiration dates for futures to assist the hedging of currency risks.
expiration_date_tbl <- read_csv(
file = "data/expiration_date.csv",
col_types = cols(expiration_date = col_date())
)
expiration_date_tbl %>% glimpse()## Rows: 23
## Columns: 1
## $ expiration_date [3m[38;5;246m<date>[39m[23m 2019-01-18, 2019-02-15, 2019-03-15, 2019-04-19, 2019-…
1.6 Process Bond and Company Data
We start by using the French and Fama breakpoints to categorise the size of the companies we have data for into distinct groupings.
mktcap_breaks_tbl <- me_breakpoint_tbl %>%
filter(datestr == "201812") %>%
select(datestr, ME20, ME40, ME60, ME80) %>%
pivot_longer(
!datestr,
names_to = "label",
values_to = "breakpoint"
)
mktcap_lookup_tbl <- mktcap_tbl %>%
transmute(
cik_number, mktcap,
mktcap_qn = cut(
mktcap,
breaks = c(0, mktcap_breaks_tbl$breakpoint * 1e6, Inf),
labels = c("QN1", "QN2", "QN3", "QN4", "QN5")
)
)
mktcap_lookup_tbl %>% glimpse()## Rows: 5,802
## Columns: 3
## $ cik_number [3m[38;5;246m<chr>[39m[23m "0000878334", "0001081862", "0000868822", "0000749098", "00…
## $ mktcap [3m[38;5;246m<dbl>[39m[23m 3162687931, 16810662751, 8001655, 15196648088, 35505321530,…
## $ mktcap_qn [3m[38;5;246m<fct>[39m[23m QN3, QN5, QN1, QN4, QN5, QN4, QN5, QN4, QN2, QN3, QN1, QN4,…
We then construct a lookup table with all the company information we need to segment our bond portfolio. The data comes in a number of pieces and we join the data together by whatever common keys we have.
company_lookup_tbl <- company_info_tbl %>%
left_join(gics_tbl, by = "gics_code") %>%
left_join(mktcap_lookup_tbl, by = "cik_number") %>%
transmute(
ticker_symbol, company_name, main_exchange, country_code,
currency = str_c(country_code, "D"),
cik_number, gics_sector_name, gics_industrygroup_name, mktcap, mktcap_qn
)
company_lookup_tbl %>% glimpse()## Rows: 262
## Columns: 10
## $ ticker_symbol [3m[38;5;246m<chr>[39m[23m "AMZN", "VZ", "IBM", "CSCO", "QCOM", "TJX", "P…
## $ company_name [3m[38;5;246m<chr>[39m[23m "AMAZON.COM, INC.", "VERIZON COMMUNICATIONS IN…
## $ main_exchange [3m[38;5;246m<chr>[39m[23m "NASDAQ National Market", "New York Stock Exch…
## $ country_code [3m[38;5;246m<chr>[39m[23m "US", "US", "US", "US", "US", "US", "US", "US"…
## $ currency [3m[38;5;246m<chr>[39m[23m "USD", "USD", "USD", "USD", "USD", "USD", "USD…
## $ cik_number [3m[38;5;246m<chr>[39m[23m "0001018724", "0000732712", "0000051143", "000…
## $ gics_sector_name [3m[38;5;246m<chr>[39m[23m "Consumer Discretionary", "Telecommunication S…
## $ gics_industrygroup_name [3m[38;5;246m<chr>[39m[23m "Retailing", "Telecommunication Services", "So…
## $ mktcap [3m[38;5;246m<dbl>[39m[23m 734416195872, 232301894022, 103302587000, 1988…
## $ mktcap_qn [3m[38;5;246m<fct>[39m[23m QN5, QN5, QN5, QN5, QN5, QN5, QN5, QN5, QN5, Q…
2 Calculate Aggregate Positions
We want to be able to divide the portfolio by other factors, and so we join that data onto the both the main portfolio data and the cashflow data.
portfolio_full_tbl <- bond_portfolio_tbl %>%
left_join(company_lookup_tbl, by = c("ticker_symbol", "currency"))
portfolio_full_tbl %>% filter(are_na(gics_sector_name))## # A tibble: 0 × 17
## # … with 17 variables: issuer <chr>, currency <chr>, position <int>,
## # price <dbl>, coupon <dbl>, issued_date <date>, maturity_date <date>,
## # ticker_symbol <chr>, mkt_value <dbl>, company_name <chr>,
## # main_exchange <chr>, country_code <chr>, cik_number <chr>,
## # gics_sector_name <chr>, gics_industrygroup_name <chr>, mktcap <dbl>,
## # mktcap_qn <fct>
portfolio_full_tbl %>% glimpse()## Rows: 250
## Columns: 17
## $ issuer <chr> "ECOM 5.875 10/14/2022", "PSBQ 6.5 06/25/2020"…
## $ currency <chr> "USD", "USD", "CAD", "USD", "USD", "USD", "USD…
## $ position <int> 172, 234, 164, 207, 308, 201, 138, 215, 236, 1…
## $ price <dbl> 91.73, 96.57, 97.75, 106.31, 104.39, 99.59, 93…
## $ coupon <dbl> 5.875, 6.500, 9.625, 7.000, 9.125, 9.000, 5.62…
## $ issued_date <date> 2017-10-13, 2016-06-24, 2017-12-27, 2017-05-1…
## $ maturity_date <date> 2022-10-14, 2020-06-25, 2021-12-28, 2023-05-1…
## $ ticker_symbol <chr> "ECOM", "PSBQ", "ALYA", "BSRSF", "MTX", "LOGM"…
## $ mkt_value <dbl> 157775.6, 225973.8, 160310.0, 220061.7, 321521…
## $ company_name <chr> "CHANNELADVISOR CORPORATION", "PSB HOLDINGS, I…
## $ main_exchange <chr> "New York Stock Exchange (NYSE)", "OTC Bulleti…
## $ country_code <chr> "US", "US", "CA", "US", "US", "US", "US", "US"…
## $ cik_number <chr> "0001169652", "0000948368", "0001734520", "000…
## $ gics_sector_name <chr> "Information Technology", "Financials", "Infor…
## $ gics_industrygroup_name <chr> "Software & Services", "Banks", "Software & Se…
## $ mktcap <dbl> 309994264, 100977637, 152256205, 709538, 18103…
## $ mktcap_qn <fct> QN1, QN1, QN1, QN1, QN2, QN3, QN5, QN2, QN1, Q…
We can now calculate some summarised aggregates of our portfolio, such as the total value of the portfolio by segment.
portfolio_full_tbl %>%
count(currency, gics_sector_name, wt = mkt_value, name = "total_value") %>%
mutate(total_value = round(total_value, 2)) %>%
pivot_wider(
id_cols = "gics_sector_name",
names_from = currency,
values_from = total_value,
values_fill = 0
) %>%
arrange(gics_sector_name) %>%
datatable()We now want to show the market value of the segmented portfolio by market cap quintile.
portfolio_full_tbl %>%
count(currency, mktcap_qn, wt = mkt_value, name = "total_value") %>%
mutate(total_value = round(total_value, 2)) %>%
pivot_wider(
id_cols = "mktcap_qn",
names_from = currency,
values_from = total_value,
values_fill = 0
) %>%
arrange(mktcap_qn) %>%
datatable()3 Construct Cashflow Data
To build out the cashflow calculation, we need to use a number of utility and helper functions to do this.
3.1 Create Helper Functions
To start, we need a function that determines each of the coupon dates for a bond - which is just a sequence of dates six months apart from the date of issue of a bond till the bond matures and pas back the par value of the bond.
create_coupon_payment_dates <- function(init_date, final_date) {
max_year <- difftime(final_date, init_date, units = "days") %>%
as.numeric() %>%
divide_by(183) %>%
ceiling()
coupon_date_tbl <- (init_date %m+% months(6 * 1:max_year)) %>%
enframe(name = NULL, value = "cashflow_date") %>%
filter(cashflow_date <= final_date)
return(coupon_date_tbl)
}We also need a function that uses the above function to create the coupon dates for the bond and then calculates the coupon payment based on the number of owned bonds, the coupon rate and the contract size (typically $1,000 of the currency).
calculate_cashflow_data <- function(init_date, final_date, position, coupon,
contract_size = 1000) {
par_value <- position * contract_size
coupon_payment <- par_value * coupon * 0.01 * 0.5
coupon_data_tbl <- create_coupon_payment_dates(init_date, final_date) %>%
mutate(
cashflow_amt = coupon_payment
)
final_tbl <- tibble(
cashflow_date = final_date,
cashflow_amt = par_value
)
cashflow_tbl <- list(coupon_data_tbl, final_tbl) %>%
bind_rows()
return(cashflow_tbl)
}We now combine all the above functions that we can use along with a table of the bond positions that comprise the portfolio and produce cashflows.
We then rearrange the cashflows by date and we now have a function that takes a portfolio table as an input, along with a ‘start date’, and it produces a sequence of cashflows from that portfolio.
calculate_portfolio_cashflow_data <- function(portfolio_tbl, current_date,
id_cols = c("issuer")) {
cashflow_tbl <- portfolio_tbl %>%
mutate(
cash_data = pmap(
list(
init_date = issued_date,
final_date = maturity_date,
position = position,
coupon = coupon
),
calculate_cashflow_data,
contract_size = 1000
)
) %>%
select(any_of(id_cols), cash_data) %>%
unnest(cash_data) %>%
filter(cashflow_date >= current_date) %>%
arrange(cashflow_date)
return(cashflow_tbl)
}3.2 Calculate and Aggregate Cashflows
We now use this function to produce the cashflows based on our portfolio.
bond_cashflow_tbl <- bond_portfolio_tbl %>%
calculate_portfolio_cashflow_data(
current_date = as.Date("2019-01-01"),
id_cols = c("issuer", "ticker_symbol", "currency", "mkt_value")
)
bond_cashflow_tbl %>% glimpse()## Rows: 1,848
## Columns: 6
## $ issuer <chr> "GIL 7 01/02/2023", "SMD 9.375 07/04/2020", "PCM 6.125 0…
## $ ticker_symbol <chr> "GIL", "SMD", "PCM", "PM", "GENC", "HYSR", "SLGD", "TRKK…
## $ currency <chr> "CAD", "CAD", "USD", "USD", "USD", "USD", "USD", "USD", …
## $ mkt_value <dbl> 241566.3, 185307.5, 217705.8, 165780.0, 248928.9, 207359…
## $ cashflow_date <date> 2019-01-01, 2019-01-03, 2019-01-04, 2019-01-09, 2019-01…
## $ cashflow_amt <dbl> 8505.000, 8203.125, 6921.250, 9000.000, 8133.750, 8883.7…
As before, we append this portfolio data to the cashflow data.
cashflow_full_tbl <- bond_cashflow_tbl %>%
left_join(company_lookup_tbl, by = c("ticker_symbol", "currency"))
cashflow_full_tbl %>% filter(are_na(gics_sector_name))## # A tibble: 0 × 14
## # … with 14 variables: issuer <chr>, ticker_symbol <chr>, currency <chr>,
## # mkt_value <dbl>, cashflow_date <date>, cashflow_amt <dbl>,
## # company_name <chr>, main_exchange <chr>, country_code <chr>,
## # cik_number <chr>, gics_sector_name <chr>, gics_industrygroup_name <chr>,
## # mktcap <dbl>, mktcap_qn <fct>
cashflow_full_tbl %>% glimpse()## Rows: 1,848
## Columns: 14
## $ issuer <chr> "GIL 7 01/02/2023", "SMD 9.375 07/04/2020", "P…
## $ ticker_symbol <chr> "GIL", "SMD", "PCM", "PM", "GENC", "HYSR", "SL…
## $ currency <chr> "CAD", "CAD", "USD", "USD", "USD", "USD", "USD…
## $ mkt_value <dbl> 241566.3, 185307.5, 217705.8, 165780.0, 248928…
## $ cashflow_date <date> 2019-01-01, 2019-01-03, 2019-01-04, 2019-01-0…
## $ cashflow_amt <dbl> 8505.000, 8203.125, 6921.250, 9000.000, 8133.7…
## $ company_name <chr> "GILDAN ACTIVEWEAR INC", "STRATEGIC METALS LTD…
## $ main_exchange <chr> "Toronto Stock Exchange", "TSX Venture Exchang…
## $ country_code <chr> "CA", "CA", "US", "US", "US", "US", "US", "US"…
## $ cik_number <chr> "0001061894", "0001267561", "0000908187", "000…
## $ gics_sector_name <chr> "Consumer Discretionary", "Materials", "Financ…
## $ gics_industrygroup_name <chr> "Consumer Durables & Apparel", "Materials", "D…
## $ mktcap <dbl> 6280267712, 21305750, 132524794, 103780019000,…
## $ mktcap_qn <fct> QN4, QN1, QN1, QN5, QN1, QN1, QN1, QN1, QN1, Q…
We can now use this data to calculate cashflow-based measures.
We now want to aggregate all these cashflows on a weekly basis to help assess the FX risk due to holding bonds in both currencies.
friday_dates <- seq(as.Date("2019-01-01"), as.Date("2022-01-01"), by = "day") %>%
enframe(name = NULL, value = "date") %>%
mutate(
day_of_week = format(date, "%a")
) %>%
filter(day_of_week == "Fri") %>%
pull(date)
aggregate_cashflows_tbl <- bond_cashflow_tbl %>%
mutate(
agg_date = map(cashflow_date, ~ friday_dates[friday_dates >= .x][1]) %>% reduce(c)) %>%
group_by(currency, agg_date) %>%
summarise(
.groups = "drop",
agg_cashflow = sum(cashflow_amt)
) %>%
arrange(agg_date, currency)
aggregate_cashflows_tbl %>% glimpse()## Rows: 256
## Columns: 3
## $ currency <chr> "CAD", "USD", "USD", "CAD", "USD", "CAD", "USD", "CAD", "…
## $ agg_date <date> 2019-01-04, 2019-01-04, 2019-01-11, 2019-01-18, 2019-01-…
## $ agg_cashflow <dbl> 16708.125, 6921.250, 43522.500, 9628.125, 47450.625, 1496…
3.3 Visualise Monthly Cashflow Data
An important part of this work is to construct helpful visualisations of the data to help check for mistakes.
We aggregate our data by currency and month and then plot them.
plot_tbl <- bond_cashflow_tbl %>%
mutate(
cashflow_month = zoo::as.yearmon(cashflow_date)
) %>%
count(currency, cashflow_month, wt = cashflow_amt, name = "cashflow_total")
ggplot(plot_tbl) +
geom_line(aes(x = cashflow_month, y = cashflow_total)) +
expand_limits(y = 0) +
facet_wrap(vars(currency), nrow = 2, scales = "free_y") +
scale_y_continuous(labels = label_comma()) +
labs(
x = "Cashflow Month",
y = "Cashflow Amount",
title = "Lineplot of Monthly Cashflows"
)3.4 Calculate Futures Delivery Expirations
We now want to repeat this exercise, aggregating by FX futures delivery dates to allow the asset manager to reduce the cost of hedging the FX risk by using futures rather than dealing with forwards.
expry_dates <- expiration_date_tbl %>% pull(expiration_date)
futures_exposures_tbl <- bond_cashflow_tbl %>%
count(currency, cashflow_date, wt = cashflow_amt, name = "date_total") %>%
mutate(
exposure_date = map(cashflow_date, ~ expry_dates[expry_dates >= .x][1]) %>% reduce(c)
) %>%
group_by(currency, exposure_date) %>%
mutate(
total_exposure = cumsum(date_total) %>% round(2)
) %>%
ungroup()
futures_exposures_tbl %>% glimpse()## Rows: 1,229
## Columns: 5
## $ currency <chr> "CAD", "CAD", "CAD", "CAD", "CAD", "CAD", "CAD", "CAD",…
## $ cashflow_date <date> 2019-01-01, 2019-01-03, 2019-01-16, 2019-01-18, 2019-0…
## $ date_total <dbl> 8505.000, 8203.125, 4753.125, 4875.000, 5865.000, 9100.…
## $ exposure_date <date> 2019-01-18, 2019-01-18, 2019-01-18, 2019-01-18, 2019-0…
## $ total_exposure <dbl> 8505.00, 16708.12, 21461.25, 26336.25, 5865.00, 14965.0…
We now show this table using the DT package
futures_exposures_tbl %>%
pivot_wider(
id_cols = c(currency, cashflow_date, exposure_date),
names_from = currency,
values_from = total_exposure,
values_fill = 0
) %>%
datatable()We also want to plot the cumulative currency exposures by delivery date and we represent the delivery dates by a vertical line.
ggplot(futures_exposures_tbl) +
geom_line(aes(x = cashflow_date, y = total_exposure)) +
geom_vline(aes(xintercept = exposure_date), colour = "red") +
facet_wrap(vars(currency), nrow = 2, scales = "free_y") +
scale_y_continuous(labels = label_comma(scale = 1e-6)) +
labs(
x = "Date",
y = "Total Exposure (millions)",
title = "Visualisation of FX Exposure Aggregated to Delivery Date"
)The above exposure is a bit messy and so we take an alternative approach to this where we display the delivery dates as points rather than vertical lines.
exposure_data_tbl <- futures_exposures_tbl %>%
group_by(currency, exposure_date) %>%
summarise(
.groups = "drop",
max_exposure = max(total_exposure)
)
ggplot() +
geom_line(aes(x = cashflow_date, y = total_exposure),
data = futures_exposures_tbl) +
geom_point(aes(x = exposure_date, y = max_exposure),
data = exposure_data_tbl, colour = "red") +
facet_wrap(vars(currency), nrow = 2, scales = "free_y") +
scale_y_continuous(labels = label_comma(scale = 1e-6)) +
labs(
x = "Date",
y = "Total Exposure (millions)",
title = "Visualisation of FX Exposure Aggregated to Delivery Date"
)3.5 Calculate Portfolio Duration
We want to calculate a quantity known as the Macauley Duration - a way to estimate the interest rate risk in the portfolio. This duration is a commonly calculated quantity, and is useful for risk management of the portfolio.
calculate_cashflow_duration <- function(data_tbl, duration_date) {
cashflow_duration <- data_tbl %>%
filter(cashflow_date > duration_date) %>%
mutate(
time_diff = difftime(cashflow_date, duration_date, units = "days") %>%
divide_by(365.25) %>%
as.numeric()
) %>%
summarise(
.groups = "drop",
cashflow_duration = sum(time_diff * cashflow_amt) / sum(cashflow_amt)
) %>%
pull(cashflow_duration)
return(cashflow_duration)
}We start by calculating the overall duration of the portfolio, split by currency.
bond_cashflow_tbl %>%
group_nest(currency) %>%
mutate(
segment_duration = map_dbl(
data, calculate_cashflow_duration,
duration_date = as.Date("2019-01-01")
)
) %>%
transmute(currency, duration = round(segment_duration, 3)) %>%
datatable()3.5.1 Calculate Duration by Portfolio Segment
We can now calculate the portfolio durations by the various factors we may want such as GICS Sector or market cap quintile.
cashflow_full_tbl %>%
group_nest(currency, gics_sector_name) %>%
mutate(
segment_duration = map_dbl(
data, calculate_cashflow_duration,
duration_date = as.Date("2019-01-01")
)
) %>%
transmute(gics_sector_name, currency, duration = round(segment_duration, 3)) %>%
pivot_wider(
id_cols = c(gics_sector_name),
names_from = currency,
values_from = duration,
values_fill = 0
) %>%
arrange(gics_sector_name) %>%
datatable()And something similar gives us a segmentation by Market Cap.
cashflow_full_tbl %>%
group_nest(currency, mktcap_qn) %>%
mutate(
segment_duration = map_dbl(
data, calculate_cashflow_duration,
duration_date = as.Date("2019-01-01")
)
) %>%
transmute(mktcap_qn, currency, duration = round(segment_duration, 3)) %>%
pivot_wider(
id_cols = c(mktcap_qn),
names_from = currency,
values_from = duration,
values_fill = 0
) %>%
arrange(mktcap_qn) %>%
datatable()3.6 Calculate Yield to Maturity
We can take a similar approach to calculating yield-to-maturity, the discount rate at which the net present value of the cashflows is zero.
This quantity is usually available for each bond in the portfolio as part of the pricing, but we can also calculate it for any subset of our portfolio.
calculate_cashflow_yield_maturity <- function(cashflow_tbl, curr_cost,
curr_date) {
discount_tbl <- cashflow_tbl %>%
mutate(
tdiff = difftime(cashflow_date, curr_date, units = "days") %>%
as.numeric() %>%
divide_by(365.25)
)
calculate_npv <- function(int_rate) {
npv <- discount_tbl %>%
mutate(
discounted = cashflow_amt * exp(-int_rate * tdiff)
) %>%
summarise(
disc_cashflow = sum(discounted)
) %>%
pull(disc_cashflow) %>%
subtract(curr_cost)
return(npv)
}
optim_val <- optimise(function(x) abs(calculate_npv(x)), interval = c(0, 1))
return(optim_val$minimum)
}We now want to calculate the yield to maturity for each of the bonds.
bond_portfolio_ytm_tbl <- cashflow_full_tbl %>%
select(
issuer, ticker_symbol, currency, mkt_value, gics_sector_name, mktcap_qn,
cashflow_date, cashflow_amt
) %>%
group_nest(
issuer, ticker_symbol, currency, mkt_value, gics_sector_name, mktcap_qn
) %>%
mutate(
ytm = map2_dbl(
data, mkt_value,
calculate_cashflow_yield_maturity,
curr_date = as.Date("2019-01-01")
)
) %>%
select(
issuer, ticker_symbol, currency, mkt_value, gics_sector_name, mktcap_qn,
ytm
)
bond_portfolio_ytm_tbl %>% glimpse()## Rows: 250
## Columns: 7
## $ issuer <chr> "ACAN 8.375 06/07/2021", "ACFN 6.375 12/18/2020", "AC…
## $ ticker_symbol <chr> "ACAN", "ACFN", "ACIW", "ADC", "ADVM", "AFTM", "AKCA"…
## $ currency <chr> "USD", "USD", "USD", "USD", "USD", "USD", "USD", "USD…
## $ mkt_value <dbl> 141312.6, 342931.2, 181945.2, 209013.0, 253520.8, 281…
## $ gics_sector_name <chr> "Health Care", "Information Technology", "Information…
## $ mktcap_qn <fct> QN1, QN1, QN3, QN2, QN1, QN1, QN3, QN4, QN1, QN1, QN2…
## $ ytm <dbl> 0.08988547, 0.05467070, 0.08075142, 0.07231494, 0.085…
Now that we have calculated the yield to maturity for each of the bonds in the portfolio we now want to visualise this data so we can get a sense of the dispersion.
ggplot(bond_portfolio_ytm_tbl) +
geom_point(aes(x = gics_sector_name, y = ytm)) +
expand_limits(y = 0) +
labs(
x = "GICS Sector",
y = "Yield to Maturity",
title = "Yield to Maturity by Sector"
) +
theme(axis.text.x = element_text(angle = 20, vjust = 0.5))We now want to calculate the segment yield to maturity on the book.
sector_ytm_tbl <- cashflow_full_tbl %>%
nest(data = c(cashflow_date, cashflow_amt)) %>%
group_by(currency, gics_sector_name) %>%
mutate(segment_value = sum(mkt_value)) %>%
ungroup() %>%
unnest(data) %>%
group_nest(currency, gics_sector_name, segment_value) %>%
mutate(
ytm = map2_dbl(
data, segment_value,
calculate_cashflow_yield_maturity,
curr_date = as.Date("2019-01-01")
)
)
sector_ytm_tbl %>%
mutate(ytm = round(ytm, 4)) %>%
pivot_wider(
id_cols = gics_sector_name,
names_from = currency,
values_from = ytm,
values_fill = 0
) %>%
datatable()Finally, we repeat the process with the market cap quintiles.
mktcap_ytm_tbl <- cashflow_full_tbl %>%
nest(data = c(cashflow_date, cashflow_amt)) %>%
group_by(currency, mktcap_qn) %>%
mutate(segment_value = sum(mkt_value)) %>%
ungroup() %>%
unnest(data) %>%
group_nest(currency, mktcap_qn, segment_value) %>%
mutate(
ytm = map2_dbl(
data, segment_value,
calculate_cashflow_yield_maturity,
curr_date = as.Date("2019-01-01")
)
)
mktcap_ytm_tbl %>%
mutate(ytm = round(ytm, 4)) %>%
pivot_wider(
id_cols = mktcap_qn,
names_from = currency,
values_from = ytm,
values_fill = 0
) %>%
datatable()4 R Environment
sessioninfo::session_info()## ─ Session info ───────────────────────────────────────────────────────────────
## setting value
## version R version 4.1.1 (2021-08-10)
## os Ubuntu 20.04.3 LTS
## system x86_64, linux-gnu
## ui RStudio
## language (EN)
## collate en_US.UTF-8
## ctype en_US.UTF-8
## tz Etc/UTC
## date 2022-02-03
##
## ─ Packages ───────────────────────────────────────────────────────────────────
## package * version date lib source
## assertthat 0.2.1 2019-03-21 [1] RSPM (R 4.1.0)
## backports 1.3.0 2021-10-27 [1] RSPM (R 4.1.0)
## bit 4.0.4 2020-08-04 [1] RSPM (R 4.1.0)
## bit64 4.0.5 2020-08-30 [1] RSPM (R 4.1.0)
## bookdown 0.24 2021-09-02 [1] RSPM (R 4.1.0)
## broom 0.7.9 2021-07-27 [1] RSPM (R 4.1.0)
## bslib 0.3.1 2021-10-06 [1] RSPM (R 4.1.0)
## cachem 1.0.6 2021-08-19 [1] RSPM (R 4.1.0)
## cellranger 1.1.0 2016-07-27 [1] RSPM (R 4.1.0)
## cli 3.1.0 2021-10-27 [1] RSPM (R 4.1.0)
## codetools 0.2-18 2020-11-04 [2] CRAN (R 4.1.1)
## colorspace 2.0-2 2021-06-24 [1] RSPM (R 4.1.0)
## conflicted * 1.0.4 2019-06-21 [1] RSPM (R 4.1.0)
## cowplot * 1.1.1 2020-12-30 [1] RSPM (R 4.1.0)
## crayon 1.4.1 2021-02-08 [1] RSPM (R 4.1.0)
## crosstalk 1.1.1 2021-01-12 [1] RSPM (R 4.1.0)
## curl * 4.3.2 2021-06-23 [1] RSPM (R 4.1.0)
## data.table 1.14.2 2021-09-27 [1] RSPM (R 4.1.0)
## DBI 1.1.1 2021-01-15 [1] RSPM (R 4.1.0)
## dbplyr 2.1.1 2021-04-06 [1] RSPM (R 4.1.0)
## digest 0.6.28 2021-09-23 [1] RSPM (R 4.1.0)
## dplyr * 1.0.7 2021-06-18 [1] RSPM (R 4.1.0)
## DT * 0.19 2021-09-02 [1] RSPM (R 4.1.0)
## ellipsis 0.3.2 2021-04-29 [1] RSPM (R 4.1.0)
## evaluate 0.14 2019-05-28 [1] RSPM (R 4.1.0)
## fansi 0.5.0 2021-05-25 [1] RSPM (R 4.1.0)
## farver 2.1.0 2021-02-28 [1] RSPM (R 4.1.0)
## fastmap 1.1.0 2021-01-25 [1] RSPM (R 4.1.0)
## forcats * 0.5.1 2021-01-27 [1] RSPM (R 4.1.0)
## fs * 1.5.0 2020-07-31 [1] RSPM (R 4.1.0)
## furrr * 0.2.3 2021-06-25 [1] RSPM (R 4.1.0)
## future * 1.22.1 2021-08-25 [1] RSPM (R 4.1.0)
## generics 0.1.1 2021-10-25 [1] RSPM (R 4.1.0)
## ggplot2 * 3.3.5 2021-06-25 [1] RSPM (R 4.1.0)
## GICS * 0.0.1.0 2022-01-17 [1] Github (bautheac/GICS@0c2b0e4)
## globals 0.14.0 2020-11-22 [1] RSPM (R 4.1.0)
## glue * 1.4.2 2020-08-27 [1] RSPM (R 4.1.0)
## gtable 0.3.0 2019-03-25 [1] RSPM (R 4.1.0)
## haven 2.4.3 2021-08-04 [1] RSPM (R 4.1.0)
## highr 0.9 2021-04-16 [1] RSPM (R 4.1.0)
## hms 1.1.1 2021-09-26 [1] RSPM (R 4.1.0)
## htmltools 0.5.2 2021-08-25 [1] RSPM (R 4.1.0)
## htmlwidgets 1.5.4 2021-09-08 [1] RSPM (R 4.1.0)
## httr 1.4.2 2020-07-20 [1] RSPM (R 4.1.0)
## jquerylib 0.1.4 2021-04-26 [1] RSPM (R 4.1.0)
## jsonlite 1.7.2 2020-12-09 [1] RSPM (R 4.1.0)
## knitr 1.36 2021-09-29 [1] RSPM (R 4.1.0)
## labeling 0.4.2 2020-10-20 [1] RSPM (R 4.1.0)
## lattice 0.20-44 2021-05-02 [2] CRAN (R 4.1.1)
## lifecycle 1.0.1 2021-09-24 [1] RSPM (R 4.1.0)
## listenv 0.8.0 2019-12-05 [1] RSPM (R 4.1.0)
## lubridate * 1.8.0 2021-10-07 [1] RSPM (R 4.1.0)
## magrittr * 2.0.1 2020-11-17 [1] RSPM (R 4.1.0)
## modelr 0.1.8 2020-05-19 [1] RSPM (R 4.1.0)
## munsell 0.5.0 2018-06-12 [1] RSPM (R 4.1.0)
## parallelly 1.28.1 2021-09-09 [1] RSPM (R 4.1.0)
## pillar 1.6.4 2021-10-18 [1] RSPM (R 4.1.0)
## pkgconfig 2.0.3 2019-09-22 [1] RSPM (R 4.1.0)
## purrr * 0.3.4 2020-04-17 [1] RSPM (R 4.1.0)
## R6 2.5.1 2021-08-19 [1] RSPM (R 4.1.0)
## Rcpp 1.0.7 2021-07-07 [1] RSPM (R 4.1.0)
## readr * 2.0.2 2021-09-27 [1] RSPM (R 4.1.0)
## readxl 1.3.1 2019-03-13 [1] RSPM (R 4.1.0)
## reprex 2.0.1 2021-08-05 [1] RSPM (R 4.1.0)
## rlang * 0.4.12 2021-10-18 [1] RSPM (R 4.1.0)
## rmarkdown 2.11 2021-09-14 [1] RSPM (R 4.1.0)
## rmdformats 1.0.3 2021-10-06 [1] RSPM (R 4.1.0)
## rstudioapi 0.13 2020-11-12 [1] RSPM (R 4.1.0)
## rvest 1.0.2 2021-10-16 [1] RSPM (R 4.1.0)
## sass 0.4.0 2021-05-12 [1] RSPM (R 4.1.0)
## scales * 1.1.1 2020-05-11 [1] RSPM (R 4.1.0)
## sessioninfo 1.1.1 2018-11-05 [1] RSPM (R 4.1.0)
## stringi 1.7.5 2021-10-04 [1] RSPM (R 4.1.0)
## stringr * 1.4.0 2019-02-10 [1] RSPM (R 4.1.0)
## tibble * 3.1.5 2021-09-30 [1] RSPM (R 4.1.0)
## tidyr * 1.1.4 2021-09-27 [1] RSPM (R 4.1.0)
## tidyselect 1.1.1 2021-04-30 [1] RSPM (R 4.1.0)
## tidyverse * 1.3.1 2021-04-15 [1] RSPM (R 4.1.0)
## tzdb 0.2.0 2021-10-27 [1] RSPM (R 4.1.0)
## utf8 1.2.2 2021-07-24 [1] RSPM (R 4.1.0)
## vctrs 0.3.8 2021-04-29 [1] RSPM (R 4.1.0)
## vroom 1.5.5 2021-09-14 [1] RSPM (R 4.1.0)
## withr 2.4.2 2021-04-18 [1] RSPM (R 4.1.0)
## xfun 0.27 2021-10-18 [1] RSPM (R 4.1.0)
## xml2 1.3.2 2020-04-23 [1] RSPM (R 4.1.0)
## yaml 2.2.1 2020-02-01 [1] RSPM (R 4.1.0)
## zoo 1.8-9 2021-03-09 [1] RSPM (R 4.1.0)
##
## [1] /usr/local/lib/R/site-library
## [2] /usr/local/lib/R/library