0
$\begingroup$

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:

enter image description here

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

enter image description here enter image description here enter image description here

$\endgroup$

1 Answer 1

1
$\begingroup$

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 here

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.