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:
- Create a sample SOFR curve as the usd ois curve
- Create a sample Estr curve as the eur ois curve
- Create a cross currency basis swap quote for EUR USD at 3M.
- Bootstrap a eur usd curve using ConstNotionalCrossCurrencyBasisSwapRateHelper
The tests are
- Create a 3 month usd_leg that is based on usd ois curve with notional payments. The spread is zero in this curve.
- Price the usd_leg using usd ois curve. As expected, the value is zero. Everything is good so far.
- 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
- 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?