Matching QuantLib OIS Curves to Bloomberg Zero Rates
Summary
The document investigates why zero rates bootstrapped from USD overnight indexed swap quotes in QuantLib differ from Bloomberg values. It presents a curve-building example and identifies several convention mismatches that can explain the discrepancy: the OIS settlement lag, the actual instrument maturity date after business-day adjustments, and the zero-rate day-count and compounding conventions.
The accepted explanation recommends a two-business-day settlement lag for the cited USD SOFR swaps, using each instrument’s generated maturity date, and deriving the displayed zero rate from the discount factor with continuous compounding and an Actual/365 basis. The revised example reproduces the displayed Bloomberg values for its first four maturities while retaining matching market and model swap rates. The result illustrates that curve comparisons require aligned conventions and dates; the example is tied to its stated market setup and does not establish that the same settings apply to every currency, index, or Bloomberg curve configuration.
Key ideas
- OIS settlement lag affects the dates and discount factors used when bootstrapping a curve.
- Use each swap’s convention-adjusted maturity date when reading a zero rate from the curve.
- Zero rates can differ because of compounding and day-count conventions even when instrument pricing matches.
- The example aligns QuantLib settings with the cited Bloomberg USD SOFR setup and reproduces its first grid points.
Tags
Full text
# Discrepancy between Bootstraped Zero Rates: Gaps between Bloomberg and My Calculated Zero Rates
# Discrepancy between Bootstraped Zero Rates: Gaps between Bloomberg and My Calculated Zero Rates
I have been attempting to bootstrap zero rates using `quantlib`, but I am perplexed by the significant discrepancies between my calculated zero rates and those obtained from Bloomberg's bootstrapping process. I would greatly appreciate any insights or suggestions regarding potential reasons for this mismatch. Below is the reproducible example :
```
import QuantLib as ql
calculation_date = ql.Date().todaysDate() #When I posted this on Quant exchange the date was 26/5/2023
ql.Settings.instance().evaluationDate = calculation_date
index = ql.OvernightIndex("USD Overnight Index", 0, ql.USDCurrency(), ql.UnitedStates(ql.UnitedStates.Settlement), ql.Actual360())
swaps = {
ql.Period("1W"): 0.05064,
ql.Period("2W"): 0.05067,
ql.Period("3W"): 0.05072,
ql.Period("1M"): 0.051021000000000004,
ql.Period("2M"): 0.051391,
ql.Period("3M"): 0.051745,
ql.Period("4M"): 0.05194,
ql.Period("5M"): 0.051980000000000005,
ql.Period("6M"): 0.051820000000000005,
ql.Period("7M"): 0.051584000000000005,
ql.Period("8M"): 0.05131,
ql.Period("9M"): 0.050924,
ql.Period("10M"): 0.050603999999999996,
ql.Period("11M"): 0.050121,
ql.Period("12M"): 0.049550000000000004,
ql.Period("18M"): 0.04558500000000001,
ql.Period("2Y"): 0.042630999999999995,
ql.Period("3Y"): 0.038952,
ql.Period("4Y"): 0.036976,
ql.Period("5Y"): 0.035919,
ql.Period("6Y"): 0.03535,
ql.Period("7Y"): 0.034998,
ql.Period("8Y"): 0.034808,
ql.Period("9Y"): 0.034738000000000005,
ql.Period("10Y"): 0.034712,
ql.Period("12Y"): 0.034801,
ql.Period("15Y"): 0.034923,
ql.Period("20Y"): 0.034662,
ql.Period("25Y"): 0.03375,
ql.Period("30Y"): 0.032826,
ql.Period("40Y"): 0.030834999999999998,
ql.Period("50Y"): 0.02896
}
rate_helpers = []
for tenor, rate in swaps.items():
helper = ql.OISRateHelper(0, tenor, ql.QuoteHandle(ql.SimpleQuote(rate)), index)
rate_helpers.append(helper)
yts = ql.RelinkableYieldTermStructureHandle()
curve = ql.PiecewiseFlatForward(calculation_date, rate_helpers, ql.Actual360())
yts.linkTo(curve)
index = index.clone(yts)
engine = ql.DiscountingSwapEngine(yts)
print("maturity | market | model | zero rate | discount factor")
for tenor, rate in swaps.items():
schedule = ql.Schedule(calculation_date,
calculation_date + tenor,
ql.Period('1Y'),
ql.UnitedStates(ql.UnitedStates.GovernmentBond),
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Forward,
False)
swap = ql.OvernightIndexedSwap(ql.OvernightIndexedSwap.Payer,
1.0,
schedule,
0.01,
ql.Actual360(),
index)
swap.setPricingEngine(engine)
maturity_date = calculation_date + tenor
zero_rate = curve.zeroRate(maturity_date, ql.Actual360() , ql.Compounded).rate()
discount_factor = curve.discount(maturity_date)
print(f" {tenor} | {rate*100:.6f} | {swap.fairRate()*100:.6f} | {zero_rate*100:.6f} | {discount_factor:.6f}")
```
The output of this code is :
```
maturity | market | model | zero rate | discount factor
1W | 5.064000 | 5.064000 | 5.191792 | 0.999016
2W | 5.067000 | 5.067000 | 5.192324 | 0.998033
3W | 5.072000 | 5.072000 | 5.194951 | 0.997050
1M | 5.102100 | 5.102100 | 5.222740 | 0.995626
```
However, when referring to the output displayed on Bloomberg, I find it important to mention that the information presented, which I believe to be accurate, is as follows:
I am inclined to believe that the issue at hand could potentially be attributed to parameters.
I would greatly appreciate any suggestions, insights that could help me understand and resolve the disparities between my calculated zero rates and the accurate rates shown on Bloomberg. Thank you for your valuable assistance!
Below is the information I have from bloomberg regarding conventions
## Answer by user808182 (score 4, accepted)
https://quant.stackexchange.com/a/75681
There are several issues with your Python code:
- USD SOFR swaps have a settlement lag of two business days, see the T+2 under "Settlement" in your last screen shot from Bloomberg. So the first argument in your OISRateHelper must be 2, not 0.
- you should use the maturity_date of your OIS swap instruments when you print out your results, and not "calculation_date + tenor" since this does not take into account the settlement lag and holidays.
- the zero rate which Bloomberg shows, is derived from the discount factor with convention "continuously compounded", Actual365
With these and other modifications my code looks like this:
```
import QuantLib as ql
import math
calculation_date = ql.Date(26,5,2023)
ql.Settings.instance().evaluationDate = calculation_date
yts = ql.RelinkableYieldTermStructureHandle()
index = ql.OvernightIndex("USD Overnight Index", 0, ql.USDCurrency(), ql.UnitedStates(ql.UnitedStates.Settlement), ql.Actual360(), yts)
swaps = {
ql.Period("1W"): 0.05064,
ql.Period("2W"): 0.05067,
ql.Period("3W"): 0.05072,
ql.Period("1M"): 0.051021000000000004,
ql.Period("2M"): 0.051391,
ql.Period("3M"): 0.051745,
ql.Period("4M"): 0.05194,
ql.Period("5M"): 0.051980000000000005,
ql.Period("6M"): 0.051820000000000005,
ql.Period("7M"): 0.051584000000000005,
ql.Period("8M"): 0.05131,
ql.Period("9M"): 0.050924,
ql.Period("10M"): 0.050603999999999996,
ql.Period("11M"): 0.050121,
ql.Period("12M"): 0.049550000000000004,
ql.Period("18M"): 0.04558500000000001,
ql.Period("2Y"): 0.042630999999999995,
ql.Period("3Y"): 0.038952,
ql.Period("4Y"): 0.036976,
ql.Period("5Y"): 0.035919,
ql.Period("6Y"): 0.03535,
ql.Period("7Y"): 0.034998,
ql.Period("8Y"): 0.034808,
ql.Period("9Y"): 0.034738000000000005,
ql.Period("10Y"): 0.034712,
ql.Period("12Y"): 0.034801,
ql.Period("15Y"): 0.034923,
ql.Period("20Y"): 0.034662,
ql.Period("25Y"): 0.03375,
ql.Period("30Y"): 0.032826,
ql.Period("40Y"): 0.030834999999999998,
ql.Period("50Y"): 0.02896
}
rate_helpers = []
for tenor, rate in swaps.items():
helper = ql.OISRateHelper(2, tenor, ql.QuoteHandle(ql.SimpleQuote(rate)), index)
rate_helpers.append(helper)
curve = ql.PiecewiseFlatForward(calculation_date, rate_helpers, ql.Actual360())
yts.linkTo(curve)
engine = ql.DiscountingSwapEngine(yts)
print("maturity | market | model | zero rate | discount factor | present value")
for tenor, rate in swaps.items():
ois_swap = ql.MakeOIS(tenor, index, rate)
pv = ois_swap.NPV()
fair_rate = ois_swap.fairRate()
maturity_date = ois_swap.maturityDate()
discount_factor = curve.discount(maturity_date)
zero_rate = -math.log(discount_factor) * 365.0/(maturity_date-calculation_date)
print(f" {tenor} | {rate*100:.6f} | {fair_rate*100:.6f} | {zero_rate*100:.6f} | {discount_factor:.6f} | {pv:.6f}")
```
And my result for the first four grid points is, as in Bloomberg
```
maturity | market | model | zero rate | discount factor | present value
1W | 5.064000 | 5.064000 | 5.131807 | 0.998314 | -0.000000
2W | 5.067000 | 5.067000 | 5.132185 | 0.997332 | 0.000000
3W | 5.072000 | 5.072000 | 5.134266 | 0.996349 | 0.000000
1M | 5.102100 | 5.102100 | 5.157684 | 0.995066 | -0.000000
```
You might also take a look hereShown 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.