Rebuilding Par Swap Rates from a Zero Curve in QuantLib
Summary
The document examines why par swap quotes reconstructed from a zero-coupon curve may fail to match the quotes used to build that curve. It describes bootstrapping a curve from deposit and swap helpers, extracting zero rates, then constructing swaps against a curve made from those rates. The response recommends reducing the problem to a small example and checking that the market helpers and priced instruments use matching conventions.
The included example uses standard QuantLib instruments and conventions to bootstrap a curve from a deposit quote and several swap quotes, then prices swaps to compare model rates with inputs. It demonstrates that matching the instrument definitions can recover the supplied quotes in a simple setup. The original example contains many configurable choices, including calendars, payment schedules, day-count rules, indices, and curve interpolation, any of which can create differences. Its illustrative result does not diagnose which specific convention caused the original mismatch; a 3-month quote also needs a clear instrument definition.
Key ideas
- A bootstrapped curve reproduces input quotes only when the priced instruments match the helper conventions.
- Calendars, schedules, day counts, indices, and payment frequencies can all affect swap valuations.
- A compact example can isolate convention mismatches more effectively than a large implementation.
- A short-tenor quote needs an explicit definition of its underlying instrument.
Tags
Full text
# Quantlib: convert par swap rates to zero rates back and forth
# Quantlib: convert par swap rates to zero rates back and forth
I built a zero-coupon curve out of a generic par swap rate curve (Step 1) and I am trying to recover the swap curve back from the zero-coupon curve (Step 2).
Step 1 works but not Step 2. I get close quotes but they do not exactly match. Anyone has any idea what's wrong in my Step 2?
My guess is that it does not come from calendar issues, as I am using theoretical calendar, daycounter, and index, with no adjustment.
Here's my code:
STEP 1:
```
# define constants
face_amount = 100
settlementDays = 0
calendar = ql.NullCalendar()
fixedLegAdjustment = ql.Unadjusted
floatingLegAdjustment = ql.Unadjusted
fixedLegDayCounter = ql.SimpleDayCounter()
floatingLegDayCounter = ql.SimpleDayCounter()
fixedLegFrequency = ql.Semiannual
end_of_month = False
floating_rate = ql.IborIndex("MyIndex", ql.Period(3, ql.Months), settlementDays, ql.USDCurrency(), calendar, floatingLegAdjustment, end_of_month, floatingLegDayCounter)
# irs is a DataFrame with one line and the column as maturities (from 3M to 120M)
deposits = [irs.columns[0]]
swaps = irs.columns[1:]
# curve dates
zero_rates = {}
curve_date = ql.DateParser.parseFormatted(str("2017-01-01"), "%Y-%m-%d")
ql.Settings.instance().evaluationDate = curve_date
spot_date = calendar.advance(curve_date, settlementDays, ql.Days)
# deposit helper
deposit_helpers_mat = []
for tenor in deposits:
deposit_helpers_mat.append([ql.Period(int(tenor), ql.Months), ql.QuoteHandle(ql.SimpleQuote(irs[int(tenor)] / 100))])
deposit_helper = [ql.DepositRateHelper(tenors_deposit, settlementDays, calendar, fixedLegAdjustment, end_of_month, fixedLegDayCounter) for tenors_deposit, deposit_rates in deposit_helpers_mat]
# swap helper
swap_helpers_mat = []
for tenor in swaps:
swap_helpers_mat.append([ql.Period(int(tenor), ql.Months), ql.QuoteHandle(ql.SimpleQuote(irs[int(tenor)] / 100))])
swap_helper = [ql.SwapRateHelper(swap_rates, tenors_swap, calendar, fixedLegFrequency, fixedLegAdjustment, fixedLegDayCounter, floating_rate) for tenors_swap, swap_rates in swap_helpers_mat]
# aggregate helpers
helper = deposit_helper + swap_helper
# build curve
zc_curve = ql.PiecewiseCubicZero(curve_date, helper, ql.SimpleDayCounter())
zero_rate = []
tenors = []
# loop over maturities
for tenor in np.arange(3, 120 + 1, 3):
maturity_date = calendar.advance(spot_date, ql.Period(int(tenor), ql.Months))
zero_rate_curve = (zc_curve.zeroRate(maturity_date, ql.SimpleDayCounter(), ql.Compounded, ql.Annual).rate()* 100)
zero_rate.append(zero_rate_curve)
tenors.append(tenor)
# build the zero curve representation into a DataFrame
zero_rates = pd.DataFrame(np.transpose(list(zip(zero_rate))), columns=list(tenors))
```
STEP 2:
```
# constant
fixedRate = 0.02
spread =0
TENORS = np.arange(3, 120 + 1, 3)
# pre-allocate
irs_rates = {}
# calculate dates
curve_date = ql.DateParser.parseFormatted(str("2017-01-01"), "%Y-%m-%d")
ql.Settings.instance().evaluationDate = curve_date
spot_date = calendar.advance(curve_date, settlementDays, ql.Days)
# zero curve
irs_rate = []
tenors = []
maturity_dates = []
zc_rates = []
# loop over maturities
for tenor in TENORS:
# maturity date
maturity_date = calendar.advance(spot_date, ql.Period(int(tenor), ql.Months))
# gather maturity dates
maturity_dates.append(maturity_date)
# gather zc rates
zc_rates.append(zero_rates[int(tenor)] / 100)
# build zero coupon curve object
zero_curve = ql.YieldTermStructureHandle(ql.CubicZeroCurve(maturity_dates, zc_rates, fixedLegDayCounter, calendar))
# libor curve
libor_curve = ql.YieldTermStructureHandle(ql.CubicZeroCurve(maturity_dates, zc_rates, floatingLegDayCounter, calendar))
# floating rate
floating_rate = ql.IborIndex("MyIndex", ql.Period(3, ql.Months), settlementDays, ql.USDCurrency(), calendar, floatingLegAdjustment, end_of_month, floatingLegDayCounter, libor_curve)
# build swap curve
# loop over maturities
j = 0
for maturity in maturity_dates:
# fixed leg tenor
fixedLegTenor = ql.Period(3, ql.Months)
# fixed leg coupon schedule
fixedLegSchedule = ql.Schedule(spot_date, maturity, fixedLegTenor, calendar, fixedLegAdjustment, fixedLegAdjustment, ql.DateGeneration.Forward, end_of_month)
# floating leg tenor
floatingLegTenor = ql.Period(3, ql.Months)
# floating leg coupon schedule
floatingLegSchedule = ql.Schedule(spot_date, maturity, floatingLegTenor, calendar, floatingLegAdjustment, floatingLegAdjustment, ql.DateGeneration.Forward, end_of_month)
# build swap pricer
swap_rate = ql.VanillaSwap(ql.VanillaSwap.Payer, face_amount, fixedLegSchedule, fixedRate, fixedLegDayCounter, floatingLegSchedule, floating_rate, spread, floatingLegDayCounter)
# build swap curve
swap_curve = ql.DiscountingSwapEngine(zero_curve)
# get swap rate
swap_rate.setPricingEngine(swap_curve)
# gather par irs rate
irs_rate.append(swap_rate.fairRate() * 100)
# gather irs tenor
tenor = int(TENORS[j])
j = j + 1
tenors.append(tenor)
# build the swap curve representation into a DataFrame
irs_rates = pd.DataFrame(np.transpose(list(zip(irs_rate))), columns=list(tenors))
```
Many thanks in advance for your help!
## Answer by David Duarte (score 3)
https://quant.stackexchange.com/a/54605
Maybe you should start with a simple example, because you have so many moving parts that it's hard to figure out where the difference is. Most likely some different convention between your helpers and the instruments you are trying to price.
```
import QuantLib as ql
today = ql.Date().todaysDate()
calendar = ql.TARGET()
spot = calendar.advance(today, 2, ql.Days)
helpers = ql.RateHelperVector()
helpers.append( ql.DepositRateHelper(0.01, ql.Euribor6M()) )
swaps = [
('1Y', 0.015),
('2Y', 0.02),
('3Y', 0.025)
]
for tenor, rate in swaps:
swapIndex = ql.EurLiborSwapIsdaFixA(ql.Period(tenor))
helpers.append( ql.SwapRateHelper(rate, swapIndex) )
curve = ql.PiecewiseLogLinearDiscount(spot, helpers, ql.Actual360())
yts = ql.YieldTermStructureHandle(curve)
engine = ql.DiscountingSwapEngine(yts)
index = ql.Euribor6M(yts)
print("maturity, market, model")
for tenor, rate in swaps:
swap = ql.MakeVanillaSwap(ql.Period(tenor), index, 0.01, ql.Period('0D'), pricingEngine=engine)
print(f"{tenor}, {rate:.6f}, {swap.fairRate():.6f}")
```
maturity, market, model 1Y, 0.015000, 0.015000 2Y, 0.020000, 0.020000 3Y, 0.025000, 0.025000
## Answer by David Duarte (score 0)
https://quant.stackexchange.com/a/54617
To answer the questions in your comment:
- What is a 3M swap rate? Either it's a fixed rate vs a shorter tenor (ex:1m) or a fixed rate vs the same tenor but forward (in this case a FRA), or if it's starting spot then it's the same as a zero rate because it has to intermediate payments.
- The floating rate conventions are in the definition of the floating index. Also, there are templates you can use: ql.USDLibor(ql.Period('3M'))
- To specify your helper, you can either use a template where the conventions are already defined ( for example: ql.UsdLiborSwapIsdaFixAm ) or use one of the several constructors. Check here for more info: https://quantlib-python-docs.readthedocs.io/en/latest/thelpers.html#swapratehelperShown 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.