0
$\begingroup$

I'm trying to bootstrap to get discount curve based on cross currency basis swap using ConstNotionalCrossCurrencyBasisSwapRateHelper in QuantLib. As a test, I tried to bootstrap the discount curve from a single cross currency basis swap but notice that the resulted discount curve is different from what I expected.

What I have done are:

  1. Create a sample SOFR curve as the usd ois curve
  2. Create a sample Estr curve as the eur ois curve
  3. Create a cross currency basis swap quote for EUR USD at 3M.
  4. Bootstrap a eur usd curve using ConstNotionalCrossCurrencyBasisSwapRateHelper

The tests are

  1. Create a 3 month usd_leg that is based on usd ois curve with notional payments. The spread is zero in this curve.
  2. Price the usd_leg using usd ois curve. As expected, the value is zero. Everything is good so far.
  3. Create a 3 month eur_leg that is based on eur ois curve with notional payments. The spread equals to the cross currency basis swap quote
  4. Price the eur_leg using the eur usd curve. I expect the value is zero. However, the result is not zero.

Can someone help?

I paste my tests code below.

# This example is based on https://www.implementingquantlib.com/2023/09/cross-currency-swaps.html
# I simplified the cross currency swaps to demonstrate the question
# I also changed the swaps to OIS based

import QuantLib as ql
import pandas as pd

today = ql.Date(27, ql.October, 2021)
ql.Settings.instance().evaluationDate = today

bps = 1e-4

# Sample data
sample_rates = pd.DataFrame(
    [
        (ql.Date(27, 10, 2021), 0.0229, 0.0893, -0.5490, -0.4869),
        (ql.Date(27, 1, 2022), 0.0645, 0.1059, -0.5584, -0.5057),
        (ql.Date(27, 4, 2022), 0.0414, 0.1602, -0.5480, -0.5236),
        (ql.Date(27, 10, 2022), 0.1630, 0.2601, -0.5656, -0.5030),
        (ql.Date(27, 10, 2023), 0.4639, 0.6281, -0.4365, -0.3468),
        (ql.Date(27, 10, 2024), 0.7187, 0.9270, -0.3500, -0.2490),
        (ql.Date(27, 10, 2025), 0.9056, 1.1257, -0.3041, -0.1590),
        (ql.Date(27, 10, 2026), 1.0673, 1.2821, -0.2340, -0.0732),
        (ql.Date(27, 10, 2027), 1.1615, 1.3978, -0.1690, -0.0331),
        (ql.Date(27, 10, 2028), 1.2326, 1.4643, -0.1041, 0.0346),
        (ql.Date(27, 10, 2029), 1.3050, 1.5589, -0.0070, 0.1263),
        (ql.Date(27, 10, 2030), 1.3584, 1.5986, 0.0272, 0.1832),
        (ql.Date(27, 10, 2031), 1.4023, 1.6488, 0.0744, 0.2599),
        (ql.Date(27, 10, 2036), 1.5657, 1.8136, 0.3011, 0.4406),
        (ql.Date(27, 10, 2041), 1.6191, 1.8749, 0.3882, 0.5331),
        (ql.Date(27, 10, 2046), 1.6199, 1.8701, 0.3762, 0.5225),
        (ql.Date(27, 10, 2051), 1.6208, 1.8496, 0.3401, 0.4926),
    ],
    columns=["date", "SOFR", "USDLibor3M", "EUR-USD-discount", "Estr"],
)

def sample_curve(tag):
    curve = ql.ZeroCurve(
        sample_rates["date"], sample_rates[tag] / 100, ql.Actual365Fixed()
    )
    return ql.YieldTermStructureHandle(curve)

# Construct basis curves
eur_ois_curve = sample_curve("Estr")
eur_ois = ql.Estr(eur_ois_curve)

usd_ois_curve = sample_curve("SOFR")
usd_ois = ql.Sofr(usd_ois_curve)

# Construct usd_leg for a simple 3 month cross currency swap.
# Spread for the usd_leg is set to zero so I expect the NPV of the usd_leg
# is 0 if use usdlibor3M_curve to discount.
notional = 1_000_000
fx_0 = 0.85

calendar = ql.UnitedStates(ql.UnitedStates.FederalReserve)
start_date = calendar.advance(today, ql.Period(2, ql.Days))
end_date = calendar.advance(start_date, ql.Period(3, ql.Months))
tenor = ql.Period(3, ql.Months)
rule = ql.DateGeneration.Forward
convention = ql.Following
end_of_month = False

schedule = ql.Schedule(
    start_date,
    end_date,
    tenor,
    calendar,
    convention,
    convention,
    rule,
    end_of_month,
)

usd_leg = (
    (ql.SimpleCashFlow(-notional, schedule[0]),)
    + ql.OvernightLeg(
        nominals=[notional],
        schedule=schedule,
        index=usd_ois,
    )
    + (ql.SimpleCashFlow(notional, schedule[-1]),)
)

usd_npv = ql.CashFlows.npv(usd_leg, usd_ois_curve, True)
print(usd_npv)
# usd_npv = 1.1641532182693481e-10

# usd_npv is zero. Everyting is good so far.

# Now I create the EUR-USD-discount using the ConstNotionalCrossCurrencyBasisSwapRateHelper
# For simplicity, I only include one ccbs quote that the base currency is USD and quote currency is EUR
# Collateral currency is USD and the quote is on EUR
quote = -0.03
ccbs_quotes = {'3M': quote}
day_counter = eur_ois.dayCounter()

ccbs_helpers = [ql.ConstNotionalCrossCurrencyBasisSwapRateHelper(
                        ql.QuoteHandle(ql.SimpleQuote(value)),
                        ql.Period(period),
                        2,
                        calendar,
                        convention,
                        end_of_month,
                        usd_ois,
                        eur_ois,
                        usd_ois_curve,
                        True,
                        False)
                for period, value in ccbs_quotes.items()]

eur_usd_curve = ql.YieldTermStructureHandle(
                    ql.PiecewiseLogLinearDiscount(2, calendar, ccbs_helpers, day_counter))

# Construct eur_leg with spread = ccbs quote. I expect the NPV of eur_leg zero using eur_usd_curve to discount
eur_leg = (
    (ql.SimpleCashFlow(-notional * fx_0, schedule[0]),)
    + ql.OvernightLeg(
        nominals=[notional * fx_0],
        schedule=schedule,
        index=eur_ois,
        spreads=[quote]
    )
    + (ql.SimpleCashFlow(notional * fx_0, schedule[-1]),)
)

eur_npv = ql.CashFlows.npv(eur_leg, eur_usd_curve, True, start_date)
print(eur_npv)
# eur_npv = -35.281226538005285

# eur_npv is not zero. What is wrong in the example above?
$\endgroup$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.