Understanding and Customising FixedRateBond Conventions#
There are hundreds of bond conventions in use across different sectors and geographies. Rateslib tries to provide a framework general enough to catch the most used conventions by default, but flexible enough to provide a user with the ability to customise it to their own requirements.
Setting up a FixedRateBond
in rateslib can be
done in different ways, with each method becoming more granular depending upon the necessary
level of customisation that is required to properly match the intended convention.
Bonds configured in rateslib have two types of convention parameters;
Regular scheduling parameters, which are similar to swaps and contain arguments like
calendar
,convention
,modifier
,ex_div
, relevant to cashflow and date determination.Calculation modes which are unique to bonds and define certain types of calculation per bond type. For example, how accrued interest is calculated for settlement and during YTM calculations, and how YTM calculations are performed.
Configured defaults#
There are a number of configured default specifications (spec
) that have been setup.
The names of these can be found in the securities section of
User Guide > Defaults.
These are useful because they might be compared against to determine how a specific bond convention might be setup. For example the following parameters are well recognised values for a US Treasury Bond:
convention
: “ActActICMA”,calendar
: “nyc”,modifier
: “none” (coupon dates are not modified according to holiday calendars)payment_lag
: 0 (payment will take place on or immediately after the coupon date).
These are visible directly in the default dict for a US government bond.
In [1]: from rateslib import defaults
In [2]: defaults.spec["us_gb"]
Out[2]:
{'frequency': 's',
'stub': 'shortfront',
'eom': True,
'modifier': 'none',
'calendar': 'nyc',
'payment_lag': 0,
'currency': 'usd',
'convention': 'actacticma',
'payment_lag_exchange': 0,
'settle': 1,
'ex_div': 1,
'calc_mode': 'us_gb'}
One observes that the calc_mode
here is also consistent for a “us_gb”, which we review later.
Regular scheduling parameters#
Sometimes multiple scheduling parameters can result in the same cashflow periods. For example, the below bond does not define any holidays in its calendar so no modifications are made to coupon dates. But that also means payment dates are not adjusted to real business days either.
In [3]: bond = FixedRateBond(dt(2000, 2, 17), "2y", fixed_rate=4.0, frequency="S", calendar="all", convention="actacticma")
In [4]: bond.cashflows()
Out[4]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF Collateral Rate Spread Cashflow NPV FX Rate NPV Ccy
0 FixedPeriod Regular GBP 2000-02-17 2000-08-17 2000-08-17 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
1 FixedPeriod Regular GBP 2000-08-17 2001-02-17 2001-02-17 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
2 FixedPeriod Regular GBP 2001-02-17 2001-08-17 2001-08-17 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
3 FixedPeriod Regular GBP 2001-08-17 2002-02-17 2002-02-17 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
4 Cashflow Exchange GBP NaT NaT 2002-02-17 NaN NaN 1000000.00 None None NaN NaN -1000000.00 None 1.00 None
A better configuration (which is reflected in rateslib defaults) is to directly specify a modifier of “none” but with an appropriate holiday calendar to adjust physical payment dates.
In [5]: bond = FixedRateBond(dt(2000, 2, 17), "2y", fixed_rate=4.0, frequency="S", calendar="nyc", modifier="none", convention="actacticma")
In [6]: bond.cashflows()
Out[6]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF Collateral Rate Spread Cashflow NPV FX Rate NPV Ccy
0 FixedPeriod Regular GBP 2000-02-17 2000-08-17 2000-08-17 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
1 FixedPeriod Regular GBP 2000-08-17 2001-02-17 2001-02-20 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
2 FixedPeriod Regular GBP 2001-02-17 2001-08-17 2001-08-17 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
3 FixedPeriod Regular GBP 2001-08-17 2002-02-17 2002-02-19 actacticma 0.50 1000000.00 None None 4.00 NaN -20000.00 None 1.00 None
4 Cashflow Exchange GBP NaT NaT 2002-02-19 NaN NaN 1000000.00 None None NaN NaN -1000000.00 None 1.00 None
Calculation modes#
The calc_mode
argument is the element that gives more direct control of calculations.
It allows a string input for a
BondCalcMode
that is predefined, or
a user can define their own.
For the above US Treasury Bond the calculation mode is preconfigured and has the following representation:
In [7]: from rateslib.instruments.bonds.conventions import US_GB
In [8]: US_GB.kwargs
Out[8]:
{'settle_accrual': 'linear_days_long_front_split',
'ytm_accrual': 'linear_days_long_front_split',
'v1': 'compounding',
'v2': 'regular',
'v3': 'compounding',
'c1': 'cashflow',
'ci': 'cashflow',
'cn': 'cashflow'}
This differs from another convention, such as for a German Bund, which has the following representation:
In [9]: from rateslib.instruments.bonds.conventions import DE_GB
In [10]: DE_GB.kwargs
Out[10]:
{'settle_accrual': 'linear_days',
'ytm_accrual': 'linear_days',
'v1': 'compounding_final_simple',
'v2': 'regular',
'v3': 'compounding',
'c1': 'cashflow',
'ci': 'cashflow',
'cn': 'cashflow'}
A BondCalcMode
can be directly constructed
and passed as the calc_mode
in the FixedRateBond initialisation.
The relevant properties of the construction are explained on the documentation page for that
object. It contains all of the necessary formulae to achieve the desired results. Importantly
all the functions must be correctly specified, or implemented, such that each element of the YTM
formula (visible in the docs for BondCalcMode
) are calculable.
Example implementation#
Rateslib has not implemented Thai Government Bonds by default, but let’s suppose we want to
construct one. The calculation for these types of bonds were found in a document on the Thai
Bond Market Association website (pdf copy
)
An example (A-3) is given which provides a couple of actionable tests.
The convention
for Thai GBs uses
Act365F and the accrued interest matches this convention with Act365F, so a linear_days
accrual
function will return an accrual fraction that determines the correct accrued interest. Noting,
Since linear_days
is the default, the correct amount of accrued interest should be returned
by default when constructing a bond with an Act365F convention. The official example
gives an accrued interest calculation of 4.86986301. Rateslib gives the following:
In [11]: bond = FixedRateBond(
....: effective=dt(1991, 1, 15),
....: termination=dt(1996, 4, 30),
....: stub="shortback",
....: fixed_rate=11.25,
....: frequency="S",
....: roll=15,
....: convention="act365f",
....: modifier="none",
....: currency="thb",
....: calendar="bus",
....: )
....:
In [12]: bond.accrued(settlement=dt(1994, 12, 20))
Out[12]: 4.86986301369863
The calculations for YTM are not as straightforward, however. The official example gives the clean price for a YTM of 8.75% to be 103.1099263, however, rateslib default calculation mode returns:
In [13]: bond.price(ytm=8.75, settlement=dt(1994, 12, 20))
Out[13]: 103.15940493709401
From the specific Thai YTM formula this is due to a number of things. Firstly, the discount functions, v1 and v3 are handling the days in the stubs differently to Thai conventions. To match, these must be implemented directly.
In [14]: def _v1_thb_gb(
....: obj, # the bond object
....: ytm, # y as defined
....: f, # f as defined
....: settlement, # datetime
....: acc_idx, # the index of the period in which settlement occurs
....: v2, # the numeric value of v2 already calculated
....: accrual, # the ytm_accrual function to return accrual fractions
....: period_idx, # the index of the current period
....: ):
....: """The exponent to the regular discount factor is derived from ACT365F"""
....: r_u = (obj.leg1.schedule.uschedule[acc_idx + 1] - settlement).days
....: return v2 ** (r_u * f / 365)
....:
In [15]: def _v3_thb_gb(obj, ytm, f, settlement, acc_idx, v2, accrual, period_idx):
....: """The exponent to the regular discount function is derived from ACT365F"""
....: r_u = (obj.leg1.schedule.uschedule[-1] - obj.leg1.schedule.uschedule[-2]).days
....: return v2 ** (r_u * f / 365)
....:
Lastly, the Thai YTM formula assumes a standardised coupon payment for the regular flows, whereas
the actual convention of Act365F does not generate the same, standardised coupon payments
each period. This is also amended from default by setting the c1_type
and
ci_type
to be full_coupon
. The back stub remains as cashflow
.
With these modifications to the calc_mode
the bond returns exactly that which aligns with
the official source.
In [16]: from rateslib.instruments import BondCalcMode
In [17]: thb_gb = BondCalcMode(
....: settle_accrual="linear_days",
....: ytm_accrual="linear_days",
....: v1=_v1_thb_gb,
....: v2="regular",
....: v3=_v3_thb_gb,
....: c1="full_coupon",
....: ci="full_coupon",
....: cn="cashflow",
....: )
....:
In [18]: bond = FixedRateBond(
....: effective=dt(1991, 1, 15),
....: termination=dt(1996, 4, 30),
....: stub="shortback",
....: fixed_rate=11.25,
....: frequency="S",
....: roll=15,
....: convention="act365f",
....: modifier="none",
....: currency="thb",
....: calendar="bus",
....: calc_mode=thb_gb
....: )
....:
In [19]: bond.accrued(settlement=dt(1994, 12, 20))
Out[19]: 4.86986301369863
In [20]: bond.price(ytm=8.75, settlement=dt(1994, 12, 20))
Out[20]: 103.10992631215662
These conventions work specifically for this bond because it was identified that it had a back stub, but for the more general case it would be better to implement and pass custom cashflow functions with a name similar to ‘full_coupon_except_cashflow_stub’.