Multi Currency Swap Pricing with SwapPricer
I have released a new version of the package SwapPricer on GitHub here.
As we are now at version 1.0.1 the toolbox is able to price using just a one-curve framework but is able to price multiple currencies (ie. CHF, EUR, GBP, JPY and USD) and any convention in terms of coupon frequency, day count convention.
We are working to introduce OIS Discounting in the next releases
SwapPricer: instructions for use
To run a multi-currency swap portfolio valuation you need the following three “ingredients”:
A table with the characteristics of the swap, like the following one
Table 1: Swap portfolio ID currency notional start.date maturity.date strike type standard time.unit.pay time.unit.receive dcc.pay dcc.receive Swap 25y EUR 10,000,000 19/01/2007 19/01/2032 0.06% receiver TRUE NA NA NA NA Swap 30y GBP 1,000,000 24/04/2012 24/04/2042 1.00% payer TRUE NA NA NA NA Swap 10y USD 2,000,000 21/02/2012 21/02/2022 0.25% receiver TRUE NA NA NA NA Swap 2y16y GBP 7,500,000 14/04/2021 14/04/2037 1.50% receiver TRUE NA NA NA NA Swap non standard EUR 15,000,000 26/05/2014 26/05/2039 2.00% payer FALSE 12 3 act/365 act/365 Swap 10y semi fixed EUR 10,000,000 26/05/2014 26/05/2024 0.10% payer FALSE 6 6 act/365 act/365 Swap 30y quarter floating GBP 1,000,000 24/04/2012 24/04/2042 2.00% receiver FALSE 3 12 act/360 act/365 Swap 10y irregular USD 2,000,000 21/02/2012 21/02/2022 0.25% receiver FALSE 6 12 act/365 act/365 Swap 2y16y EUR 7,500,000 14/04/2021 14/04/2037 1.50% payer FALSE 12 12 act/365 act/360 These are the columns that have to be in the
- ID (character) is a custom ID for the swap
- currency (character) as per the ISO 4217 standard
- notional (numeric)
- start.date (Date)
- maturity.date (Date)
- strike (numeric)
- type (string) either payer or receiver. It is not case-sensitive.
- standard (logical)
Only if the field standard is set to FALSE then we need four additional fields.
An example of portfolio is provided with the package and is called swap.basket.
- time.unit.pay and time.unit.receive (integer) are the number of months for the frequency of the leg (ie. monthly would have a time.unit of 1, quarterly of 3, semiannual of 6 and annual of 12)
- dcc.pay and dc..receive (character) as per the fmdates helper
The date at which the swaps are being priced
As many interest rate lists as per the currencies in the swap portfolio. The list is made of a string with the code of the currency and a with a tibble with the discounting factor curve with two columns: Dates and Discount Factors (df). Here is the structure of interest rate object:
str(SwapPricer::EUR.curves)
## List of 2 ## $ currency: chr "EUR" ## $ discount:Classes 'tbl_df', 'tbl' and 'data.frame': 26 obs. of 2 variables: ## ..$ Date: Date[1:26], format: "2019-04-15" ... ## ..$ df : num [1:26] 1 1 1 1 1 ...
Where the discount tibble of the list has the same characteristics of the
df.table
object described in this post For your reference, I provided three curve objects with the package: EUR.curves, GBP.curves and USD.curves
Now we can price our test multi-currency and non-standard swap portfolio by simply running SwapPortfolioPricing
function with the three items in the order of description and appending as many curves as the number of currencies in the book.
A sample code is shown here:
today <- lubridate::ymd(20190414)
results <- SwapPricer::SwapPortfolioPricing(SwapPricer::swap.basket,
today,
SwapPricer::EUR.curves,
SwapPricer::GBP.curves,
SwapPricer::USD.curves)
and these are the results in table format:
swap.id | currency | clean.mv | dirty.mv | accrual.pay | accrual.receive | par | pv01 |
---|---|---|---|---|---|---|---|
Swap 25y | EUR | -881,814.61 | -874,994.31 | 5,441.11 | 1,379.18 | 0.77% | -12,393.65 |
Swap 30y | GBP | 105,100.47 | 104,668.39 | -4,712.33 | 4,280.26 | 1.54% | 1,948.54 |
Swap 10y | USD | -119,319.88 | -126,214.04 | -7,630.28 | 736.11 | 2.43% | -548.15 |
Swap 2y16y | GBP | -94,850.43 | -94,850.43 | -0.00 | 0.00 | 1.59% | -10,393.05 |
Swap non standard | EUR | -2,591,763.24 | -2,861,567.21 | -263,835.62 | -5,968.36 | 1.07% | 27,917.04 |
Swap 10y semi fixed | EUR | -16,340.53 | -29,897.79 | -3,808.22 | -9,749.04 | 0.07% | 5,132.42 |
Swap 30y quarter floating | GBP | 88,255.22 | 105,650.31 | -2,056.96 | 19,452.05 | 1.55% | -1,941.05 |
Swap 10y irregular | USD | -119,595.14 | -126,677.82 | -7,795.01 | 712.33 | 2.44% | -546.46 |
Swap 2y16y | EUR | -361,098.10 | -361,098.10 | -0.00 | 0.00 | 1.18% | 11,170.90 |
So long RQuantLib
Apart from the new definition of the curve object, most of the changes have happened “behind the scenes”. One thing that I had to do to make the deployment easier on all the OSs was to remove any reference to Quantlib. I am a huge fan of the RQuantLib package, but I realised I was using it just for adjusting dates for holidays. So I decided to take a different approach:
- I downloaded all the holidays and saved it as internal objects in my package
- I created a very simple routine to adjust for holidays, as in the following piece of code:
AdvanceDate <- function(dates, currency, eom.check) {
holiday <- holidays[[currency]]
check <- TRUE %in% ((dates %in% holiday) |
(weekdays(dates) %in% "Saturday") |
(weekdays(dates) %in% "Sunday"))
if (check) {
repeat {
if (eom.check) {
dates <- dplyr::case_when((dates %in% holiday) ~ dates - 1,
(weekdays(dates) %in% "Saturday") ~ dates - 1,
(weekdays(dates) %in% "Sunday") ~ dates - 2,
TRUE ~ dates)
} else {
dates <- dplyr::case_when((dates %in% holiday) ~ dates + 1,
(weekdays(dates) %in% "Saturday") ~ dates + 2,
(weekdays(dates) %in% "Sunday") ~ dates + 1,
TRUE ~ dates)
}
check <- TRUE %in% ((dates %in% holiday) |
(weekdays(dates) %in% "Saturday") |
(weekdays(dates) %in% "Sunday"))
if (!check) {
return(dates)
}
}
} else {
return(dates)
}
}
You can see that adjusting for holidays is a recursive exercise: let’s assume we have Easter Sunday in the cashflow schedule. It automatically gets adjusted to Monday, but isince Easter Monday is also an holiday, the function will run a second time to further advance to Tuesday.
The performance of the function is guaranteed by the fact that the dplyr::case_when function is vectorised. In fact, the overall procedure takes almost the same time to run as with the RQuantLib function.