.. _cook-ndirs-doc: .. ipython:: python :suppress: from rateslib import dt, Solver, Curve, FXRates, FXForwards, IRS, NDXCS, defaults import matplotlib.pyplot as plt Non-Deliverable Offshore IRS and XCS (INR) ******************************************* *Rateslib* v2.5 introduced non-deliverable *IRS* and *XCS*. This page exemplifies how to use these objects to calibrate *Curves* in those markets. **Key Points** - A USD `Curve` is established in the normal way using US instruments. - An `FXForwards` market is proposed (uncalibrated) between USD and the desired EM currency. - The suite of non-deliverable *Instruments* are constructed and used for calibration. Example Frameworks -------------------- .. tabs:: .. tab:: INR Offshore Here we will use ND-IRS and NDXCS to calibrate offshore Indian Rupee *Curves*. **The Deliverable US Market** First, we need to establish the baseline US market and the SOFR curve. This is no different to the :ref:`standard tutorial ` on the matter, so we quickly do this with some of the following SOFR swap data. Just for some variety, this *Curve* will be interpolated with a log-cubic DF spline. .. ipython:: python usd = Curve( nodes={ dt(2025, 12, 29): 1.0, dt(2026, 12, 29): 1.0, dt(2027, 12, 29): 1.0, dt(2028, 12, 29): 1.0, dt(2029, 12, 29): 1.0, dt(2031, 1, 7): 1.0, }, convention="Act360", calendar="nyc", interpolation="spline", id="sofr" ) us_solver = Solver( curves=[usd], instruments=[ IRS(dt(2025, 12, 31), _, spec="usd_irs", curves="sofr") for _ in ["1Y", "2Y", "3Y", "4Y", "5Y"] ], s=[3.434, 3.302, 3.314, 3.359, 3.416], ) **The Components for the ``FXForwards``** We are going to use just the 1Y through 5Y instruments, as demonstration, to calibrate the INR market. So our local FBIL Overnight Mumbai Interbank Outright Rate (FBIL-O/N MIBOR) is the following: .. ipython:: python inr = Curve( nodes={ dt(2025, 12, 29): 1.0, dt(2026, 12, 29): 1.0, dt(2027, 12, 29): 1.0, dt(2028, 12, 29): 1.0, dt(2029, 12, 29): 1.0, dt(2031, 1, 7): 1.0, }, convention="Act365F", calendar="mum", id="mibor-ois", ) In order to introduce the necessary degrees of freedom to satisfy the cross-currency market and supply and demand we establish the basis curve: .. ipython:: python inrusd = Curve( nodes={ dt(2025, 12, 29): 1.0, dt(2026, 12, 29): 1.0, dt(2027, 12, 29): 1.0, dt(2028, 12, 29): 1.0, dt(2029, 12, 29): 1.0, dt(2031, 1, 7): 1.0, }, convention="Act365F", calendar="all", # <- no holiday calendar necessary for a cross-currency discount curve. id="inrusd", ) Finally we put all of the elements together to create the USDINR FXForwards market, note that we have also input the spot USDINR FX rate here as well: .. ipython:: python fxf = FXForwards( fx_rates=FXRates({"usdinr": 89.9812}, settlement=dt(2025, 12, 31)), fx_curves={"usdusd": usd, "inrinr": inr, "inrusd": inrusd}, ) This object can now be used to forecast any USDINR rate, **but it won't be accurate** becuase we haven't calibrated anything yet! The INR rates are currently all zero on the 2 INR *Curves*. .. ipython:: python fxf.rate("usdinr", settlement=dt(2026, 12, 31)) fxf.swap("usdinr", [dt(2025, 12, 31), dt(2026, 12, 31)]) **Calibrating the Curves** So, we have now reached the point where we can calibrate the INR curves. We have 10 parameters / degrees of freedom and will therefore require 10 *Instruments*. We will use 5 *NDIRS*, which will calibrate local currency interest rates (the ``inr`` *Curve*) [Bloomberg Moniker IRSWNI1 Curncy], and 5 *NDXCS* [Bloomberg Moniker IRUSON1 Curncy] which will effectively calibrate the cross-currency basis. *Rateslib* has added some ``spec`` defaults for the purpose of this article, but the keyword arguments used can be directly observed below: .. ipython:: python defaults.spec["inr_ndirs"] defaults.spec["inrusd_ndxcs"] To calibrate we must include the previous US :class:`~rateslib.solver.Solver`, which contains the mapping to the constructed US SOFR *Curve*, and we specify the *Instruments* and the live market data rates. .. ipython:: python inr_solver = Solver( pre_solvers=[us_solver], curves=[inr, inrusd], instruments=[ *[IRS(dt(2025, 12, 30), _, spec="inr_ndirs", curves=["mibor-ois", "sofr"]) for _ in ["1Y", "2Y", "3Y", "4Y", "5Y"]], *[NDXCS(dt(2025, 12, 31), _, spec="inrusd_ndxcs", curves=[None, "sofr", "sofr", "sofr"]) for _ in ["1Y", "2Y", "3Y", "4Y", "5Y"]], ], s=[ 5.47, 5.5525, 5.715, 5.835, 5.925, # <- IRS rates 6.375, 6.335, 6.415, 6.535, 6.595 # <- XCS rates ], fx=fxf, ) What is interesting to note about this particular *Solver* configuration is that nowhere does the *'inrusd'* discount *Curve* enter any *Instrument* specification. Since these *Instruments* have non-deliverable cashflows every discount *Curve* is the USD SOFR *Curve*. The key pricing component here is the ``fx=fxf`` object, which is a **pricing** parameter that *is* needed and is passed to all *Instruments*, and of course it derives forward FX rates using the *'inrusd'* *Curve* so everything is calibrated accurately. The datasource (**DS**) for these prices also gives (wide) financial bid/ask for FX swaps and FX forwards. We can compare these with the :class:`~rateslib.fx.FXForwards` we have constructed through *rateslib* (**RL**) calibration. .. ipython:: python :suppress: from pandas import DataFrame from rateslib.dual.utils import _dual_float df = DataFrame({ "tenor": ["1y", "2y", "3y", "4y", "5y"], "DS forward": [92.5112, 95.4512, 98.3212, 101.3112, 104.7912], "DS swap": [25300, 54700, 83400, 113300, 148100], "RL forward": [ _dual_float(fxf.rate("usdinr", dt(2026, 12, 31))), _dual_float(fxf.rate("usdinr", dt(2027, 12, 31))), _dual_float(fxf.rate("usdinr", dt(2028, 12, 29))), _dual_float(fxf.rate("usdinr", dt(2029, 12, 31))), _dual_float(fxf.rate("usdinr", dt(2030, 12, 31))), ], "RL swap": [ _dual_float(fxf.swap("usdinr", [dt(2025, 12, 31), dt(2026, 12, 31)])), _dual_float(fxf.swap("usdinr", [dt(2025, 12, 31), dt(2027, 12, 31)])), _dual_float(fxf.swap("usdinr", [dt(2025, 12, 31), dt(2028, 12, 29)])), _dual_float(fxf.swap("usdinr", [dt(2025, 12, 31), dt(2029, 12, 31)])), _dual_float(fxf.swap("usdinr", [dt(2025, 12, 31), dt(2030, 12, 31)])), ] }) .. ipython:: python df Lets have a look at the calibrate *Curves* thus far: .. ipython:: python usd.plot("1b", comparators=[inr, inrusd], labels=["SOFR", "ON/MIBOR", "ON/MIBOR+Basis"]) .. plot:: from rateslib import dt, Solver, Curve, FXRates, FXForwards, IRS, NDXCS import matplotlib.pyplot as plt usd = Curve( nodes={ dt(2025, 12, 29): 1.0, dt(2026, 12, 29): 1.0, dt(2027, 12, 29): 1.0, dt(2028, 12, 29): 1.0, dt(2029, 12, 29): 1.0, dt(2031, 1, 7): 1.0, }, convention="Act360", calendar="nyc", interpolation="spline", id="sofr" ) us_solver = Solver( curves=[usd], instruments=[ IRS(dt(2025, 12, 31), "1y", spec="usd_irs", curves="sofr"), IRS(dt(2025, 12, 31), "2y", spec="usd_irs", curves="sofr"), IRS(dt(2025, 12, 31), "3y", spec="usd_irs", curves="sofr"), IRS(dt(2025, 12, 31), "4y", spec="usd_irs", curves="sofr"), IRS(dt(2025, 12, 31), "5y", spec="usd_irs", curves="sofr"), ], s=[3.434, 3.302, 3.314, 3.359, 3.416], ) inr = Curve( nodes={ dt(2025, 12, 29): 1.0, dt(2026, 12, 29): 1.0, dt(2027, 12, 29): 1.0, dt(2028, 12, 29): 1.0, dt(2029, 12, 29): 1.0, dt(2031, 1, 7): 1.0, }, convention="Act365F", calendar="mum", id="mibor-ois" ) inrusd = Curve( nodes={ dt(2025, 12, 29): 1.0, dt(2026, 12, 29): 1.0, dt(2027, 12, 29): 1.0, dt(2028, 12, 29): 1.0, dt(2029, 12, 29): 1.0, dt(2031, 1, 7): 1.0, }, convention="Act365F", calendar="all", # <- no holiday calendar necessary for a cross-currency discount curve. id="inrusd" ) fxf = FXForwards( fx_rates=FXRates({"usdinr": 89.9812}, settlement=dt(2025, 12, 31)), fx_curves={"usdusd": usd, "inrinr": inr, "inrusd": inrusd}, ) inr_solver = Solver( pre_solvers=[us_solver], curves=[inr, inrusd], instruments=[ *[IRS(dt(2025, 12, 30), _, spec="inr_ndirs", curves=["mibor-ois", "sofr"]) for _ in ["1Y", "2Y", "3Y", "4Y", "5Y"]], *[NDXCS(dt(2025, 12, 31), _, spec="inrusd_ndxcs", curves=[None, "sofr", "sofr", "sofr"]) for _ in ["1Y", "2Y", "3Y", "4Y", "5Y"]], ], s=[5.47, 5.5525, 5.715, 5.835, 5.925, 6.375, 6.335, 6.415, 6.535, 6.595], fx=fxf, ) fig, ax, line = usd.plot("1b", comparators=[inr, inrusd], labels=["SOFR", "ON/MIBOR", "ON/MIBOR+Basis"]) plt.show() plt.close()