.. _cook-rfr-semi-liquid-doc: .. ipython:: python :suppress: from rateslib import Curve, Solver, IRS, add_tenor, get_calendar, dt, get_imm from datetime import timedelta import matplotlib.pyplot as plt from pandas import DataFrame Less Liquid RFR Curves (MXN, COP) ************************************ Less liquid RFR *Curve* building follows the same principles as demonstrated in :ref:`Standard Liquid RFR Curves `, except data availability might be sparser and obviously the *Instruments* must be chosen to match local conventions. **Key Points** - Standard configuration for *Curve* setup. - Minor modifications for the MXN market. Example Frameworks -------------------- .. tabs:: .. tab:: MXN F-TIIE Due to data availability this example uses standard par tenor MXN F-TIIE OIS quotes as the *Instruuments*. MXN swaps have an unconventional frequency expressed in 28 calendar day periods. *Rateslib* handles this natively as part of its :class:`~rateslib.scheduling.Frequency` framework. .. warning:: In practice, MXN IRS are referred to in *'months'* assuming *'one-month'* is a single 28-day (or 4-week) period. This gives 13 months per year and, for example a 130-month swap would be interpreted as being the 10-year swap. *Rateslib* does **not** tolerate this colloquial definition and requires a more precise tenor to be specified. In this case *130-month* is required to be entered either as *"3640D"* or *"520W"*, and the ``frequency`` is set under the ``spec`` as *"28d"* (equivalent to *"4W"*). **Data** First, load (from source) the market data for each chosen tenor of the *Curve* and establish the relevant dates of construction. The below code also determines the appropriate maturity of each tenor under the market calendar. .. ipython:: python data = DataFrame({ "Term": ["4W", "8W", "12W", "24W", "36W", "52W", "104W", "156W", "208W", "260W", "364W", "520W", "624W", "780W", "1040W", "1560W"], "Rate": [7.02, 7.017, 7.015, 6.99, 6.955, 6.965, 7.155, 7.351, 7.515, 7.645, 7.855, 8.086, 8.21, 8.355, 8.48, 8.44], }) today = dt(2025, 1, 16) spot = get_calendar("mex").lag_bus_days(today, 2, False) data["Termination"] = [add_tenor(spot, _, "F", "mex") for _ in data["Term"]] **Curve Design** In order to create a perfectly solvable curve, the chosen **nodes** will be assigned as the maturity of each *Instrument*. .. ipython:: python ftiie = Curve( id="ftiie", convention="Act360", # <- important to match F-TIIE convention calendar="mex", # <- important to match F-TIIE convention interpolation="spline", nodes={ today: 1.0, # <- this is today's DF, **{_: 1.0 for _ in data["Termination"][:-1]}, # <- every instrument node but the last **{data["Termination"].iloc[-1] + timedelta(days=10): 1.0}, # <- avoid spline end warnings } ) **Calibration with Solver** The final step puts the *Curve* and the *Instruments* together, mutating the *Curve* to match the *rates: s*. .. ipython:: python :okwarning: solver = Solver( curves=[ftiie], instruments=[IRS(spot, _, spec="mxn_irs", curves="ftiie") for _ in data["Termination"]], s=data["Rate"], instrument_labels=data["Term"], id="mx_rates", ) Then plotting either the O/N rates or the zero rates: .. code-block:: python ftiie.plot("1b") ftiie.plot("Z") .. plot:: from rateslib import Curve, Solver, IRS, add_tenor, get_calendar, dt import matplotlib.pyplot as plt from datetime import timedelta from pandas import DataFrame data = DataFrame({ "Term": ["4W", "8W", "12W", "24W", "36W", "52W", "104W", "156W", "208W", "260W", "364W", "520W", "624W", "780W", "1040W", "1560W"], "Rate": [7.02, 7.017, 7.015, 6.99, 6.955, 6.965, 7.155, 7.351, 7.515, 7.645, 7.855, 8.086, 8.21, 8.355, 8.48, 8.44], }) today = dt(2025, 1, 16) spot = get_calendar("mex").lag_bus_days(today, 2, False) data["Termination"] = [add_tenor(spot, _, "F", "mex") for _ in data["Term"]] ftiie = Curve( id="ftiie", convention="Act360", # <- important to match F-TIIE convention calendar="mex", # <- important to match F-TIIE convention interpolation="spline", nodes={ today: 1.0, # <- this is today's DF, **{_: 1.0 for _ in data["Termination"][:-1]}, # <- every instrument node but the last **{data["Termination"].iloc[-1] + timedelta(days=10): 1.0}, # <- avoid spline end warnings } ) solver = Solver( curves=[ftiie], instruments=[IRS(spot, _, spec="mxn_irs", curves="ftiie") for _ in data["Termination"]], s=data["Rate"], instrument_labels=data["Term"], id="mx_rates", ) fig1, ax1, lines = ftiie.plot("z") del fig1, ax1 plt.close() fig, ax, _ = ftiie.plot("1b") ax.plot(lines[0]._x, lines[0]._y) plt.show() plt.close()