User Guide#
Where to start?#
Rateslib tends to follow the typical quant architecture:
This means that financial instrument specification, curve and/or surface construction from market data including foreign exchange (FX) will permit pricing metrics and risk sensitivity. These functionalities are interlinked and potentially dependent upon each other. This guide will introduce them in a structured way and give typical examples how they are used in practice.
Note
If you see this icon at any point after a section it will link to a section in the rateslib-excel documentation which may demonstrate the equivalent Python example in Excel.
Let’s start with some fundamental Curve and Instrument constructors.
Trivial derivatives examples#
Rateslib has three fundamental Curve types. All can be constructed independently by providing basic inputs.
A Curve
is discount factor (DF) based and is constructed
by providing DFs on specific node dates. Interpolation between nodes
is
configurable, but the below uses the “log-linear” default.
In [1]: from rateslib import *
In [2]: usd_curve = Curve(
...: nodes={
...: dt(2022, 1, 1): 1.0,
...: dt(2022, 7, 1): 0.98,
...: dt(2023, 1, 1): 0.95
...: },
...: calendar="nyc",
...: id="sofr",
...: )
...:
A LineCurve
is value based and is constructed
by providing curve values on specific node dates. Interpolation between nodes
is
configurable, with the default being “linear” interpolation (hence LineCurve).
In [3]: usd_legacy_3mIBOR = LineCurve(
...: nodes={
...: dt(2022, 1, 1): 2.635,
...: dt(2022, 7, 1): 2.896,
...: dt(2023, 1, 1): 2.989,
...: },
...: calendar="nyc",
...: id="us_ibor_3m",
...: )
...:
An IndexCurve
extends a Curve class by allowing
an index_base
argument and the calculation of index values. It is DF based.
It is required for the pricing of index-linked Instruments.
In [4]: usd_cpi = IndexCurve(
...: nodes={
...: dt(2022, 1, 1): 1.00,
...: dt(2022, 7, 1): 0.97,
...: dt(2023, 1, 1): 0.955,
...: },
...: index_base=308.95,
...: id="us_cpi",
...: )
...:
A Hazard Curve is used by credit Instruments when a default is possible. Hazard Curves
utilise the same Curve
class and the rates reflect overnight
hazard rates and the DFs reflect survival probabilities.
In [5]: pfizer_hazard = Curve(
...: nodes={
...: dt(2022, 1, 1): 1.0,
...: dt(2022, 7, 1): 0.998,
...: dt(2023, 1, 1): 0.995
...: },
...: id="pfizer_hazard",
...: )
...:
Next, we will construct some basic derivative Instruments. These will use some market conventions defined by rateslib through its default argument specifications, although all arguments can be supplied manually if and when required.
Here we create a short dated SOFR RFR interest rate swap (IRS
).
In [6]: irs = IRS(
...: effective=dt(2022, 2, 15),
...: termination="6m",
...: notional=1000000000,
...: fixed_rate=2.0,
...: spec="usd_irs"
...: )
...:
Here we create a SOFR STIR future (STIRFuture
).
In [7]: stir = STIRFuture(
...: effective=get_imm(code="H22"),
...: termination=get_imm(code="M22"),
...: contracts=100,
...: price=97.495,
...: spec="usd_stir",
...: )
...:
A US LIBOR FRA
is an obsolete Instrument, but we can still
create one and these still trade in other currencies, e.g. EUR.
In [8]: fra = FRA(
...: effective=dt(2022, 2, 16),
...: termination="3m",
...: frequency="Q",
...: calendar="nyc",
...: convention="act360",
...: method_param=2,
...: fixed_rate=2.5
...: )
...:
Here we construct a generic US investment grade credit default
swap (CDS
)
In [9]: cds = CDS(
...: effective=dt(2021, 12, 20),
...: termination=dt(2022, 9, 20),
...: notional=15e6,
...: spec="us_ig_cds",
...: )
...:
This constructs a zero-coupon inflation swap
(ZCIS
) with usual
daily index interpolation and 3-month index lag.
In [10]: zcis = ZCIS(
....: effective=dt(2022, 2, 2),
....: termination="9m",
....: notional=-25e6,
....: fixed_rate=3.25,
....: spec="usd_zcis",
....: )
....:
We can combine the Curves and the Instruments to give pricing metrics such as
npv()
,
cashflows()
, and the mid-market
rate()
, as well as others. Without further specification
these values are all expressed in the Instrument’s local USD currency.
In [11]: irs.npv(usd_curve)
Out[11]: 12629097.829705866
In [12]: irs.cashflows(usd_curve)
Out[12]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF Collateral Rate Spread Cashflow NPV FX Rate NPV Ccy
leg1 0 FixedPeriod Stub USD 2022-02-15 2022-08-15 2022-08-17 act360 0.50 1000000000.00 0.97 None 2.00 NaN -10055555.56 -9776494.15 1.00 -9776494.15
leg2 0 FloatPeriod Stub USD 2022-02-15 2022-08-15 2022-08-17 act360 0.50 -1000000000.00 0.97 None 4.58 0.00 23045139.84 22405591.98 1.00 22405591.98
In [13]: stir.npv(usd_curve)
Out[13]: -383423.5359790483
In [14]: stir.rate(usd_curve, metric="price")
Out[14]: 95.96130585608381
In [15]: fra.npv(curves=[usd_legacy_3mIBOR, usd_curve])
Out[15]: 484.85927030340423
In [16]: fra.rate([usd_legacy_3mIBOR, usd_curve])
Out[16]: 2.6984475138121544
In [17]: cds.npv(curves=[pfizer_hazard, usd_curve])
Out[17]: -82275.6055187576
In [18]: cds.rate([pfizer_hazard, usd_curve])
Out[18]: 0.26324952643038907
In [19]: cds.cashflows([pfizer_hazard, usd_curve])
Out[19]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF Collateral Rate Survival Cashflow NPV FX Rate NPV Ccy Recovery
leg1 0 CreditPremiumPeriod Regular USD 2021-12-20 2022-03-20 2022-03-21 act360 0.25 15000000.00 0.99 None 1.00 1.00 -37500.00 -37156.90 1.00 -37156.90 NaN
1 CreditPremiumPeriod Regular USD 2022-03-20 2022-06-20 2022-06-21 act360 0.26 15000000.00 0.98 None 1.00 1.00 -38333.33 -37557.08 1.00 -37557.08 NaN
2 CreditPremiumPeriod Regular USD 2022-06-20 2022-09-20 2022-09-20 act360 0.26 15000000.00 0.97 None 1.00 1.00 -38333.33 -36959.66 1.00 -36959.66 NaN
leg2 0 CreditProtectionPeriod Regular USD 2021-12-20 2022-09-20 2022-09-20 act360 0.76 -15000000.00 0.97 None NaN 1.00 9000000.00 29398.03 1.00 29398.03 0.40
In [20]: zcis.npv([usd_cpi, usd_curve])
Out[20]: -285438.47285240795
In [21]: zcis.rate([usd_cpi, usd_curve])
Out[21]: 4.852119618886364
In [22]: zcis.cashflows([usd_cpi, usd_curve])
Out[22]:
Type Period Ccy Acc Start Acc End Payment Convention DCF Notional DF Rate Spread Cashflow NPV FX Rate NPV Ccy Collateral Real Cashflow Index Base Index Val Index Ratio
leg1 0 ZeroFixedLeg None USD 2022-02-02 2022-11-02 2022-11-02 1+ 0.75 -25000000.00 0.96 3.25 None 606932.34 582461.01 1.00 582461.01 None NaN NaN NaN NaN
leg2 0 ZeroIndexLeg None USD 2022-02-02 2022-11-02 2022-11-02 1 1.00 25000000.00 0.96 100.00 NaN -904363.13 -867899.49 1.00 -867899.49 None -25000000.00 310.64 321.88 1.04
If instead of this trivial, minimalist example you would like to see a real world example replicating a Bloomberg SWPM function SOFR curve please click the link.
Quick look at FX#
Spot rates and conversion#
The above values were all calculated and displayed in USD. That is the default
currency in rateslib and the local currency of those Instruments. We can convert these values
into another currency using the FXRates
class. This is a basic class which is
parametrised by some exchange rates.
In [23]: fxr = FXRates({"eurusd": 1.05, "gbpusd": 1.25})
In [24]: fxr.rates_table()
Out[24]:
usd eur gbp
usd 1.00 0.95 0.80
eur 1.05 1.00 0.84
gbp 1.25 1.19 1.00
We now have a mechanism by which to specify values in other currencies.
In [25]: irs.npv(usd_curve, fx=fxr, base="usd")
Out[25]: 12629097.829705866
In [26]: irs.npv(usd_curve, fx=fxr, base="eur")
Out[26]: <Dual: 12027712.218767, (fx_eurusd), [-11454964.0]>
In [27]: stir.npv(usd_curve, fx=fxr, base="usd")
Out[27]: -383423.5359790483
In [28]: stir.npv(usd_curve, fx=fxr, base="eur")
Out[28]: <Dual: -365165.272361, (fx_eurusd), [347776.4]>
In [29]: fra.npv([usd_legacy_3mIBOR, usd_curve], fx=fxr, base="usd")
Out[29]: 484.85927030340423
In [30]: fra.npv([usd_legacy_3mIBOR, usd_curve], fx=fxr, base="eur")
Out[30]: <Dual: 461.770734, (fx_eurusd), [-439.8]>
In [31]: cds.npv([pfizer_hazard, usd_curve], fx=fxr, base="usd")
Out[31]: -82275.6055187576
In [32]: cds.npv([pfizer_hazard, usd_curve], fx=fxr, base="eur")
Out[32]: <Dual: -78357.719542, (fx_eurusd), [74626.4]>
In [33]: zcis.npv([usd_cpi, usd_curve], fx=fxr, base="usd")
Out[33]: -285438.47285240795
In [34]: zcis.npv([usd_cpi, usd_curve], fx=fxr, base="eur")
Out[34]: <Dual: -271846.164621, (fx_eurusd), [258901.1]>
One observes that the value returned here is not a float but a Dual
which is part of rateslib’s AD framework. This is the first example of capturing a
sensitivity, which here denotes the sensitivity of the EUR NPV relative to the EURUSD FX rate.
One can read more about this particular treatment of FX
here and more generally about the dual AD framework here.
FX forwards#
For multi-currency derivatives we need more than basic, spot exchange rates.
We need an FXForwards
market.
This stores the FX rates and the interest
rates curves that are used for all the FX-interest rate parity derivations. With these
we can calculate forward FX rates and also ad-hoc FX swap rates.
When defining the fx_curves
dict mapping, the key “eurusd” should be interpreted as; the
Curve for EUR cashflows, collateralised in USD, and similarly for other entries.
In [35]: eur_curve = Curve({
....: dt(2022, 1, 1): 1.0,
....: dt(2022, 7, 1): 0.972,
....: dt(2023, 1, 1): 0.98},
....: calendar="tgt",
....: )
....:
In [36]: eurusd_curve = Curve({
....: dt(2022, 1, 1): 1.0,
....: dt(2022, 7, 1): 0.973,
....: dt(2023, 1, 1): 0.981}
....: )
....:
In [37]: fxf = FXForwards(
....: fx_rates=FXRates({"eurusd": 1.05}, settlement=dt(2022, 1, 1)),
....: fx_curves={
....: "usdusd": usd_curve,
....: "eureur": eur_curve,
....: "eurusd": eurusd_curve,
....: }
....: )
....:
In [38]: fxf.rate("eurusd", settlement=dt(2023, 1, 1))
Out[38]: <Dual: 1.084263, (fx_eurusd), [1.0]>
In [39]: fxf.swap("eurusd", settlements=[dt(2022, 2, 1), dt(2022, 5, 2)])
Out[39]: <Dual: -37.314180, (fx_eurusd), [-35.5]>
FXForwards objects are comprehensive and more information regarding all of the FX features is available in this link.
More about instruments#
We’ve seen some single currency derivatives above. A complete guide for all of the Instruments is available in this link. That will also introduce the building blocks; Legs and Periods.
Multi-currency instruments#
Let’s take a look at the multi-currency instruments. Notice how these Instruments
maintain consistent method naming conventions with those above. This makes it possible to plug
any Instruments into a Solver
to calibrate Curves
around target mid-market rates, and generate market risks.
This is an FXSwap
.
In [40]: fxs = FXSwap(
....: effective=dt(2022, 2, 1),
....: termination="3m", # May-1 is a holiday, May-2 is business end date.
....: pair="eurusd",
....: notional=20e6,
....: calendar="tgt|fed",
....: )
....:
In [41]: fxs.rate(curves=[None, eurusd_curve, None, usd_curve], fx=fxf)
Out[41]: <Dual: -37.314180, (fx_eurusd), [-35.5]>
In [42]: fxs.cashflows_table(curves=[None, eurusd_curve, None, usd_curve], fx=fxf)
Out[42]:
local_ccy EUR USD
collateral_ccy usd usd
payment
2022-02-01 20000000.00 -20974233.02
2022-05-02 -20274060.50 21185992.46
An FXExchange
is a forward FX transaction.
In [43]: fxe = FXExchange(
....: settlement=dt(2022, 4, 1),
....: pair="eurusd",
....: notional=10e6,
....: fx_rate=1.035,
....: )
....:
In [44]: fxe.rate(curves=[None, eurusd_curve, None, usd_curve], fx=fxf)
Out[44]: <Dual: 1.046264, (fx_eurusd), [1.0]>
In [45]: fxe.npv(curves=[None, eurusd_curve, None, usd_curve], fx=fxf)
Out[45]: <Dual: 111514.113855, (fx_eurusd), [9864822.1]>
In [46]: fxe.cashflows_table(curves=[None, eurusd_curve, None, usd_curve], fx=fxf)
Out[46]:
local_ccy EUR USD
collateral_ccy usd usd
payment
2022-04-01 10000000.00 -10350000.00
Cross-currency swaps (XCS
) are easily configured and
analysed in rateslib.
In [47]: xcs = XCS(
....: effective=dt(2022, 4, 1),
....: termination="6m",
....: spec="eurusd_xcs",
....: float_spread=-3.0,
....: notional=25e6,
....: )
....:
In [48]: xcs.rate(curves=[eur_curve, eurusd_curve, usd_curve, usd_curve], fx=fxf)
Out[48]: <Dual: -10.421943, (fx_eurusd), [0.0]>
In [49]: xcs.cashflows(curves=[eur_curve, eurusd_curve, usd_curve, usd_curve], fx=fxf)
Out[49]:
Type Period Ccy Payment Notional DF Rate Cashflow NPV FX Rate NPV Ccy Collateral Acc Start Acc End Convention DCF Spread
leg1 0 Cashflow Exchange EUR 2022-04-01 -25000000.00 0.99 NaN 25000000.00 24662055.24 1.05 25895158.01 usd NaT NaT NaN NaN NaN
1 FloatPeriod Regular EUR 2022-07-06 25000000.00 0.97 5.66 -357619.39 -348041.10 1.05 -365443.16 usd 2022-04-01 2022-07-01 act360 0.25 -3.00
2 FloatPeriod Regular EUR 2022-10-05 25000000.00 0.98 -1.63 106426.42 103996.26 1.05 109196.07 usd 2022-07-01 2022-10-03 act360 0.26 -3.00
3 Cashflow Exchange EUR 2022-10-03 25000000.00 0.98 NaN -25000000.00 -24426969.29 1.05 -25648317.76 usd NaT NaT NaN NaN NaN
leg2 0 Cashflow Exchange USD 2022-04-01 26156599.95 0.99 1.05 -26156599.95 -25895158.01 1.00 -25895158.01 usd NaT NaT NaN NaN NaN
1 FloatPeriod Regular USD 2022-07-06 -26156599.95 0.98 4.04 267030.67 261469.06 1.00 <Dual: 261469.060916, (fx_eurusd), [249018.2]> usd 2022-04-01 2022-07-01 act360 0.25 0.00
2 Cashflow Mtm USD 2022-07-01 -94099.95 0.98 1.04 94099.95 92217.95 1.00 92217.95 usd NaT NaT NaN NaN NaN
3 FloatPeriod Regular USD 2022-10-05 -26062500.00 0.96 6.13 417261.77 402336.93 1.00 <Dual: 402336.931939, (fx_eurusd), [383178.0]> usd 2022-07-01 2022-10-03 act360 0.26 0.00
4 Cashflow Exchange USD 2022-10-03 -26062500.00 0.96 1.04 26062500.00 25138777.08 1.00 25138777.08 usd NaT NaT NaN NaN NaN
In [50]: xcs.cashflows_table(curves=[eur_curve, eurusd_curve, usd_curve, usd_curve], fx=fxf)
Out[50]:
local_ccy EUR USD
collateral_ccy usd usd
payment
2022-04-01 25000000.00 -26156599.95
2022-07-01 0.00 94099.95
2022-07-06 -357619.39 267030.67
2022-10-03 -25000000.00 26062500.00
2022-10-05 106426.42 417261.77
Securities and bonds#
A very common instrument in financial investing is a FixedRateBond
.
At time of writing the on-the-run 10Y US treasury was the 3.875% Aug 2033 bond. Here we can
construct this using the street convention and derive the price from yield-to-maturity and
risk calculations.
In [51]: fxb = FixedRateBond(
....: effective=dt(2023, 8, 15),
....: termination=dt(2033, 8, 15),
....: fixed_rate=3.875,
....: spec="ust"
....: )
....:
In [52]: fxb.accrued(settlement=dt(2025, 2, 14))
Out[52]: 1.9269701086956519
In [53]: fxb.price(ytm=4.0, settlement=dt(2025, 2, 14))
Out[53]: 99.10641380057267
In [54]: fxb.duration(ytm=4.0, settlement=dt(2025, 2, 14), metric="duration")
Out[54]: np.float64(7.178560455252011)
In [55]: fxb.duration(ytm=4.0, settlement=dt(2025, 2, 14), metric="modified")
Out[55]: np.float64(7.037804367894129)
In [56]: fxb.duration(ytm=4.0, settlement=dt(2025, 2, 14), metric="risk")
Out[56]: np.float64(7.11053190579773)
There are some interesting Cookbook articles
on BondFuture
and cheapest-to-deliver (CTD) analysis.
Calibrating curves with a solver#
The guide for Constructing Curves introduces the main
curve classes,
Curve
, LineCurve
, and
IndexCurve
. It also touches on some of the more
advanced curves CompositeCurve
,
ProxyCurve
, and MultiCsaCurve
.
Calibrating curves is a very natural thing to do in fixed income. We typically use market prices of commonly traded instruments to set values. FX Volatility Smiles and FX Volatility Surfaces are also calibrated using the exact same optimising algorithms.
Below we demonstrate how to calibrate the Curve
that
we created above in the initial trivial example using SOFR swap market data. First, we
are reminded of the discount factors (DFs) which were manually set on that curve.
In [57]: usd_curve.nodes
Out[57]:
{datetime.datetime(2022, 1, 1, 0, 0): 1.0,
datetime.datetime(2022, 7, 1, 0, 0): 0.98,
datetime.datetime(2023, 1, 1, 0, 0): 0.95}
Now we will instruct a Solver
to recalibrate those value to match
a set of prices, s
. The calibrating Instruments associated with those prices are 6M and 1Y IRSs.
In [58]: solver = Solver(
....: curves=[usd_curve],
....: instruments=[
....: IRS(dt(2022, 1, 1), "6M", spec="usd_irs", curves="sofr"),
....: IRS(dt(2022, 1, 1), "1Y", spec="usd_irs", curves="sofr"),
....: ],
....: s=[4.35, 4.85],
....: instrument_labels=["6M", "1Y"],
....: id="us_rates"
....: )
....:
SUCCESS: `func_tol` reached after 3 iterations (levenberg_marquardt), `f_val`: 3.0486413284865872e-15, `time`: 0.0021s
Solving was a success! Observe that the DFs on the Curve have been updated:
In [59]: usd_curve.nodes
Out[59]:
{datetime.datetime(2022, 1, 1, 0, 0): <Dual: 1.000000, (sofr0, sofr1, sofr2), [1.0, 0.0, 0.0]>,
datetime.datetime(2022, 7, 1, 0, 0): <Dual: 0.978595, (sofr0, sofr1, sofr2), [0.0, 1.0, 0.0]>,
datetime.datetime(2023, 1, 1, 0, 0): <Dual: 0.953176, (sofr0, sofr1, sofr2), [0.0, 0.0, 1.0]>}
We can plot the overnight rates for the calibrated curve. This curve uses ‘log_linear’ interpolation so the overnight forward rates are constant between node dates.
In [60]: usd_curve.plot("1b", labels=["SOFR o/n"])
Out[60]:
(<Figure size 640x480 with 1 Axes>,
<Axes: >,
[<matplotlib.lines.Line2D at 0x7f2e82edbbf0>])
(Source code
, png
, hires.png
, pdf
)
Pricing Mechanisms#
Since rateslib is an object oriented library with object associations we give detailed instructions of the way in which the associations can be constructed in mechanisms.
The key takeway is that when you initialise and create an Instrument you can do one of three things:
Not provide any Curves (or Vol surface) for pricing upfront (
curves=NoInput(0)
).Create an explicit association to pre-existing Python objects, e.g.
curves=my_curve
.Define some reference to a Curves mapping with strings using
curves="my_curve_id"
.
If you do 1) then you must provide Curves at price
time: instrument.npv(curves=my_curve)
.
If you do 2) then you do not need to provide anything further at price time:
instrument.npv()
. But you still can provide Curves directly, like for 1), as an override.
If you do 3) then you can provide a Solver
which contains the Curves and will
resolve the string mapping: instrument.npv(solver=my_solver)
. But you can also provide Curves
directly, like for 1), as an override.
Best practice in rateslib is to use 3). This is the safest and most flexible approach and designed to work best with risk sensitivity calculations also.
Risk Sensitivities#
Rateslib’s can calculate delta and cross-gamma risks relative to the calibrating Instruments of a Solver. Rateslib also unifies these risks against the FX rates used to create an FXForwards market, to provide a fully consistent risk framework expressed in arbitrary currencies. See the risk framework notes.
Performance wise, because rateslib uses dual number AD upto 2nd order, combined with the appropriate analysis, it is shown to calculate a 150x150 Instrument cross-gamma grid (22,500 elements) from a calculated portfolio NPV in approximately 250ms.
Utilities#
Rateslib could not function without some utility libraries. These are often referenced in other guides as they arise and can also be linked to from those sections.
Specifically those utilities are:
Cookbook#
This is a collection of more detailed examples and explanations that don’t necessarily fall into any one category. See the Cookbook index.