QuantLib icon indicating copy to clipboard operation
QuantLib copied to clipboard

MtMCrossCurrencyBasisSwapRateHelper Produces Unrealistic Zero Rates When Using OIS Indices (Ester/SOFR)

Open ahabre opened this issue 1 month ago • 5 comments

Hi,

When building a mark-to-market (MtM) cross-currency basis curve between EUR (Ester) and USD (SOFR), the MtMCrossCurrencyBasisSwapRateHelper gives unrealistic zero rates and discount factors for the long end of the curve. The short end, calibrated using FX forwards, behaves as expected. The issue appears when extending the curve using MtMCrossCurrencyBasisSwapRateHelper.

I’m trying to build a EUR–USD MTM xccy basis curve where both indices are overnight (OIS).

For maturities up to 1Y, I use FX forwards, which produce correct and stable results. Beyond 1Y, using MtMCrossCurrencyBasisSwapRateHelper, the resulting zero rates explode (see data below).

helper = ql.MtMCrossCurrencyBasisSwapRateHelper( basis=ql.QuoteHandle(basisQuote), tenor=ql.Period("2Y"), fixingDays=2, calendar=ql.JointCalendar(ql.TARGET(), ql.UnitedStates()), convention=ql.ModifiedFollowing, endOfMonth=False, baseCurrencyIndex=ql.Estr(), quoteCurrencyIndex=ql.Sofr(), collateralCurve=ql.YieldTermStructureHandle(collateralCurve), isFxBaseCurrencyCollateralCurrency=False, isBasisOnFxBaseCurrencyLeg=True, isFxBaseCurrencyLegResettable=True, paymentFrequency=4, )

Date TimeFromRef ZeroRate Discount
27/11/2026 1.025 1969.78 1.7e-09
26/11/2027 2.036 1025.54 8.5e-10
27/11/2028 3.056 705.92 4.3e-10
26/11/2030 5.081 451.33 1.1e-10
26/11/2035 10.153 254.89 5.8e-12

I noticed the constructor for MtMCrossCurrencyBasisSwapRateHelper expects IborIndex objects for the floating legs — not OISIndex (like Ester or Sofr).

Could this mismatch be the cause of the issue when both indices are OIS? or is it an issue with my inputs?

ahabre avatar Nov 23 '25 14:11 ahabre

Hi!

Overnight indices should be handled properly - based on this https://github.com/lballabio/QuantLib/issues/1973.

How are you inserting the basis? Note that the constructor expects it in decimal format, e.g. 20 bps should be passed as 0.002. Perhaps, this should be made clearer in the documentation of the class.

marcin-rybacki avatar Nov 24 '25 12:11 marcin-rybacki

Basis is input in actual terms meaning if 10bps it is 0.001

ahabre avatar Nov 24 '25 12:11 ahabre

May you post the rest of your script so we can run it? Thanks.

lballabio avatar Nov 24 '25 12:11 lballabio

Hi, attached is a self contained example that shows the curve building.

Thanks for the help.

example to quantlib.py

ahabre avatar Nov 25 '25 18:11 ahabre

Hi, thanks for sending over the example. By the first look, it seems that you are linking to the same handle 1) the basis curve you are bootstrapping 2) ESTR projection curve used by the index. ESTR should be linked to a separate ESTR curve, built out of ESTR swaps - basis curve should not be used to project ESTR fixings. Second, very minor thing, is that you set FX base leg (EUR in this case) as resettable, which I believe for these instruments is the USD leg.

marcin-rybacki avatar Nov 25 '25 19:11 marcin-rybacki