QuantLib OIS Bootstrap Repricing: Aligning Swap Dates and Frequency
Summary
The document investigates why OIS swaps repriced on a bootstrapped QuantLib curve show nonzero NPVs and a fair rate below the input quote. It asks whether the discrepancy comes from the curve bootstrap’s stopping condition or accuracy settings, using an example built from flat OIS quotes and separately constructed swaps.
The accepted explanation is that the swaps used for the check do not match the instruments represented by the curve helpers. The check should use a spot-date start, a maturity measured from that start, and an annual fixed-leg payment frequency. The document shows how to make those adjustments and suggests repricing the helper’s own swap as a direct consistency check. Its example illustrates the setup mismatch; it does not establish a general bootstrap tolerance or discuss how to tune one.
Key ideas
- A curve bootstrap is expected to reprice the benchmark instruments used to construct it.
- The separately created swaps differed from the helper instruments in their start date and fixed-leg frequency.
- For a like-for-like comparison, the swap should start on spot date and mature relative to that date.
- The fixed leg should use annual payments to match the OIS helper convention described.
- Repricing the swap extracted from each helper provides a direct consistency check.
Tags
Full text
# Bootstrap with QuantLib: Fair Swap or zero NPV
# Bootstrap with QuantLib: Fair Swap or zero NPV
#### In all brevity
- What is the termination condition used in QuantLib's curve bootstrapping?
- Can I modify this setup to my needs, e.g. can I tune this to a higher accuracy?
#### Background
When bootstrapping discounting and projection curves, a necessary condition is for the implied curves to price the benchmark instruments at market, i.e. to value the instruments that were originally supplied to the curve construction algorithm as close as possible to their input prices. With a curve that uses a local interpolation mechanism (iterative bootstrap), I would assume all benchmark instruments to be re-priced to market prices with minimal error, i.e.
> Assume a curve/bootstrap mechanism using local interpolation, e.g. linear interpolation of log discount factors. Given a market quote, and given the curve constructed 'so far', the bootstrap mechanism selects the 'next' reference instrument and updates the curve by shifting a rate at a node point until the reference instrument is priced at market.
#### My observation and question
I reprice my benchmark instruments on the curve I have just bootstrapped and find that these do not price as close to zero as I would have assumed, see minimal example below:
```
import QuantLib as ql
today = ql.Date(23,ql.June,2020)
ql.Settings.instance().evaluationDate = today
eonia = ql.Eonia()
```
Assuming 'flat' OIS quotes in the market at 1% per tenor and collecting the curve construction helpers:
```
quotes = {str(k)+'Y' : ql.SimpleQuote(0.01) for k in range(1,21)}
ois_helpers = []
for k,v in quotes.items():
ois_helpers.append(ql.OISRateHelper(
settlementDays = 2,
tenor = ql.Period(k),
rate = ql.QuoteHandle(v),
index = eonia,
telescopicValueDates =True))
eonia_curve = ql.PiecewiseLinearZero(2,ql.TARGET(),ois_helpers,ql.Actual365Fixed())
val_curve = ql.YieldTermStructureHandle(eonia_curve)
```
Creating another `eonia_index`, this time with a projection curve attached; and a valuation engine:
```
eonia_index = ql.Eonia(val_curve)
swap_engine = ql.DiscountingSwapEngine(val_curve)
```
Now I am setting up the benchmark instruments as 'real' instruments and get their NPVs. Note that I assume a notional of `1 million` currs:
```
print('TENOR \t PV \t fairrate% \t fairrate% + fairspread%')
for p in quotes.keys():
schedule = ql.MakeSchedule(today, today + ql.Period(p), ql.Period('1d'), calendar=ql.TARGET())
fixedRate = quotes[p].value()
ois_swap = ql.OvernightIndexedSwap(
ql.OvernightIndexedSwap.Receiver,
1E6,
schedule,
fixedRate,
ql.Actual360(),
eonia_index)
ois_swap.setPricingEngine(swap_engine)
print(p + "\t" +
str(round(ois_swap.NPV(),2)) + " \t " +
str(round(ois_swap.fairRate()*100,4)) + "\t\t" +
str(100*(ois_swap.fairRate()+ois_swap.fairSpread())))
```
Resulting in
```
TENOR NPV fairrate% fairrate% + fairspread%
1Y 50.25 0.995 1.0
2Y 100.55 0.995 1.0
3Y 149.95 0.995 1.0
4Y 199.23 0.995 1.0
5Y 247.63 0.995 1.0
6Y 295.67 0.995 1.0
7Y 343.23 0.995 1.0
8Y 390.7 0.995 1.0
9Y 437.44 0.995 1.0
10Y 483.46 0.995 1.0
11Y 529.01 0.995 1.0
12Y 574.48 0.995 1.0
13Y 619.49 0.995 1.0
14Y 663.69 0.995 1.0
15Y 707.68 0.995 1.0
16Y 751.11 0.995 1.0
17Y 794.1 0.995 1.0
18Y 836.66 0.995 1.0
19Y 879.03 0.995 1.0
20Y 920.98 0.995 1.0
```
Clearly, the implied fair rate is not exactly 1%, but implied fair rate plus implied spread gives the 1%. Also, each swap's NPV is close, but not 'very' close to zero.
I am wondering, whether
- there's something off in my instrument setup?
- Which terminal condition QuantLib's bootstrap methodology is applying here, and
- Whether I can set tighter bounds to that mechanism.
Thank you very much for any input / thoughts / pointers.
#### My setup
I have built the discount curve following Luigi's and Goutham's QuantLib Python Cookbook as of 2019-JUNE-01; I am using the QuantLib Python SWIG; version 1.19.
## Answer by David Duarte (score 6, accepted)
https://quant.stackexchange.com/a/55347
The problem is that you are not pricing the same thing, and for two reasons:
- The vanilla instruments you are pricing should start on spot date and have a maturity with that start as reference
- The frequency of the fixed leg on the OIS swap should be annual.
If you change you code to:
```
print('TENOR \t PV \t fairrate% \t fairrate% + fairspread%')
calendar = ql.TARGET()
for p in quotes.keys():
start = calendar.advance(today, 2, ql.Days)
schedule = ql.MakeSchedule(start, calendar.advance(start, ql.Period(p)), ql.Period('1Y'), calendar=calendar)
fixedRate = quotes[p].value()
ois_swap = ql.OvernightIndexedSwap(
ql.OvernightIndexedSwap.Receiver,
1E6,
schedule,
fixedRate,
ql.Actual360(),
eonia_index)
ois_swap.setPricingEngine(swap_engine)
print(p + "\t" +
str(round(ois_swap.NPV(),2)) + " \t \t" +
str(round(ois_swap.fairRate()*100,4)) + "\t\t" +
str(100*(ois_swap.fairRate()+ois_swap.fairSpread())))
```
You'll get the right output.
This can also be checked by extracting the instruments from the helpers:
```
print('TENOR \t PV \t fairrate% \t fairrate% + fairspread%')
for idx, p in enumerate(quotes.keys()):
ois_swap = ois_helpers[idx].swap()
ois_swap.setPricingEngine(swap_engine)
print(p + "\t" +
str(round(ois_swap.NPV(),2)) + " \t \t" +
str(round(ois_swap.fairRate()*100,4)) + "\t\t" +
str(100*(ois_swap.fairRate()+ois_swap.fairSpread())))
```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.