Choosing Spread Legs for Euribor Basis Swaps in QuantLib
Summary
The document discusses fitting a EUR interest rate curve from overnight, outright Euribor swap, and inter-tenor basis quotes. The questioner models each basis instrument as two vanilla swaps and adds the quoted spread to the shorter-tenor floating leg, but reports a negative fitted zero-rate spread for a six-month versus twelve-month basis despite matching the market quote. The quoted examples cover one-month versus six-month and six-month versus twelve-month structures.
The answer suggests that the spread is being optimized on the wrong leg: its sign depends on which tenor receives the spread, with the cited example indicating a negative spread on the twelve-month leg for the six-month versus twelve-month structure. It also points to QuantLib’s FloatFloatSwap class as an alternative to composing two swaps. The guidance is specific to leg orientation and implementation; it does not provide a complete curve-fitting procedure or independently verify the questioner’s setup.
Key ideas
- Basis swap quotes require a consistent convention for which floating leg carries the spread.
- The sign of a basis spread depends on the leg receiving it and the direction of the swap.
- A negative fitted spread can indicate that the spread has been assigned to the wrong tenor leg.
- QuantLib provides a FloatFloatSwap class for modeling swaps with two floating legs.
- Curve construction still requires consistent forecasting and discounting curve choices.
Tags
Full text
# Question regarding fitting Euribor curve using different basis quotes using Quantlib Python
# Question regarding fitting Euribor curve using different basis quotes using Quantlib Python
I am trying to fit a EUR curve based on the following instruments:-
- EONIA quotes
- 1m vs 6m basis quotes
- 3m (outright) quotes
- 6m (outright) quotes
- 6m vs 12m quotes
(1) , (3) & (4) are simply outright quotes and I fit that (using a python optimiser) by modelling them as :-
- EONIA is modelled as a Quantlib OvernightIndexedSwap object
- 3m & 6m Swaps are modelled as VanillaSwap object
I am having trouble fitting the basis quotes.
1m vs 6m basis quotes
The quotes are as follows:-`[('1Y',12.3),('2Y',12.6),('3Y',13),('4Y',13.1),('5Y',13.1),('10Y',11.6),('20Y',6.5),('30Y',4.4)]`
6m vs 12m basis quotes
The quotes are as follows:- `[('1Y',7),('2Y',7.2),('3Y',7.4),('4Y',7.6),('5Y',7.7),('10Y',7.5),('20Y',6.3),('30Y',5.4)]`
The attached code is what I use to model these basis swaps (basically I model them as 2 VanillaSwaps - long the long term Euribor and short the short term Euribor)
My question is regarding which leg to use a Swap1 vs Swap2.
My understanding is , the spread is added to the libor leg of the short tenor.
So for example, for modelling 1m vs 6m, I create VanillaSwap1 object (for the 1m Euribor leg) and add the spread to the floating leg. I then create VanillaSwap2 for the long tenor (i.e. 6m Euribor)
Similarly, for modelling 6m vs 12m basis,:- I create VanillSwap1 object (for the 6m Euribor leg) and add the spread to the floating leg. I then create VanillaSwap2 for the long tenor (i.e. 12m Euribor).
However, I am getting negative fitted zero rate (after the optimisation process) for the 6m vs 12m. NOTE:I get the correct zero spread rate which matches the market but the sign is -ve
For the 1m vs 6m basis, I correctly fit the zero rate spreads to the market
Am I doing something wrong here?
```
self.z_leg1_forcurve = curves[self.leg1_forcurve]
self.z_leg2_forcurve = curves[self.leg2_forcurve]
self.z_discurve_1 = curves[self.leg1_discurve]
self.z_discurve_2 = curves[self.leg2_discurve]
# set the base curve IborIndex
self.IBOR_Index1 = ql.IborIndex(
'IborIndex1',
ql.Period(self.leg1_frequency),
self.settle_days,
self.currency,
self.calendar,
ql.ModifiedFollowing,
False,
self.day_count,
self.z_leg1_forcurve.QLZeroCurve # Zero curve for the forecasting curve
)
# set the spread curve IborIndex
self.IBOR_Index2 = ql.IborIndex(
'IborIndex2',
ql.Period(self.leg2_frequency),
self.settle_days,
self.currency,
self.calendar,
ql.ModifiedFollowing,
False,
self.day_count,
self.z_leg2_forcurve.QLZeroCurve # Zero curve for the forecasting curve
)
# create a payment schedule for leg_1
schedule1 = ql.Schedule(
self.start_date,
self.end_date,
ql.Period(self.leg1_frequency),
self.calendar,
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Backward,
False
)
# create a payment schedule for leg_2
schedule2 = ql.Schedule(
self.start_date,
self.end_date,
ql.Period(self.leg2_frequency),
self.calendar,
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Backward,
False
)
# Swap1 is the short tenor (to which we add the spread quote)
self.QL_Swap1 = ql.VanillaSwap(
ql.VanillaSwap.Payer,
self.notional, # nominal
schedule1, # fixed leg schedule
0.0, # fixed rate
self.day_count, # fixed leg day_count
schedule1, # float leg schedule
self.IBOR_Index1, # libor index
self.quote, # spread (added to leg1 ALWAYS)
self.day_count # float leg day_count
)
# Swap2 is the long tenor
self.QL_Swap2 = ql.VanillaSwap(
ql.VanillaSwap.Payer,
self.notional, # nominal
schedule2, # fixed schedule
0.0, # fixedRate
self.day_count, # fixed leg DayCount
schedule2, # float schedule
self.IBOR_Index2, # liborIndex
0.0, # spread
self.day_count # float leg day_count
)
# discount the base curve with it's own discounting_curve
engine = ql.DiscountingSwapEngine(self.z_discurve_1.QLZeroCurve)
self.QL_Swap1.setPricingEngine(engine)
# discount the spread curve with it's own discounting_curve
engine2 = ql.DiscountingSwapEngine(self.z_discurve_2.QLZeroCurve)
self.QL_Swap2.setPricingEngine(engine2)
```
```
## Answer by David Duarte (score 1)
https://quant.stackexchange.com/a/50441
You are probably optimizing the spread on the wrong leg. Spread on the 1M in a 6M vs 1M swap will be positive and spread on the 12M in a 6M vs 12M swap will be negative.
Also, you could try using the FloatFloatSwap class instead of creating two swaps...
```
start = ql.Date(30,12,2019)
maturity = ql.Date(30,12,2024)
index6m = ql.Euribor6M(fwd_curve_6m)
float6m = ql.Schedule(start,
maturity,
index6m.tenor(),
index6m.fixingCalendar(),
index6m.businessDayConvention(),
index6m.businessDayConvention(),
ql.DateGeneration.Backward,
False)
index3m = ql.Euribor3M(fwd_curve_3m)
float3m = ql.Schedule(start,
maturity,
index3m.tenor(),
index3m.fixingCalendar(),
index3m.businessDayConvention(),
index3m.businessDayConvention(),
ql.DateGeneration.Backward,
False)
notional = 10e6
basis = ql.FloatFloatSwap(ql.VanillaSwap.Payer,
[notional] * (len(float3m)-1),
[notional] * (len(float6m)-1),
float3m,
index3m,
ql.Actual360(),
float6m,
index6m,
ql.Actual360(), False, False,
[1] * (len(float3m)-1),
[0.0007194] * (len(float3m)-1))
swap_engine = ql.DiscountingSwapEngine(discount_curve)
basis.setPricingEngine(swap_engine)
npv = basis.NPV()
print(f"NPV of basis swap is: {npv:,.2f}")
```Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.