0
$\begingroup$

I am attempting to build a forward curve for multiple tenors (1M / 3M / 6M / 12M) using the quantlib library. The input to my model are sofr swaps(1W through 50Y). It appears I am building my curve correctly as I can zero rates and discount factors for my spot curve directly back to bloomberg, but when I try getting forward rates I am no longer able to tie.

import pandas as pd
import QuantLib as ql
import math

rates = {
    '1W':5.30694,
    '2W':5.3096,
    '3W':5.3119,
    '1M':5.3155,
    '2M':5.3381,
    '3M':5.36788,
    '4M':5.39426,
    '5M':5.4169,
    '6M':5.43035,
    '7M':5.4355,
    '8M':5.43425,
    '9M':5.42945,
    '10M':5.415,
    '11M':5.39297,
    '12M':5.36985,
    '18M':5.071,
    '2Y':4.81595,
    '3Y':4.42031,
    '4Y':4.1825,
    '5Y':4.04375,
    '6Y':3.95985,
    '7Y':3.90315,
    '8Y':3.8633,
    '9Y':3.83855,
    '10Y':3.82213,
    '12Y':3.80775,
    '15Y':3.79964,
    '20Y':3.74566,
    '25Y':3.63489,
    '30Y':3.528,
    '40Y':3.31713,
    '50Y':3.11,

}

calculation_date = ql.Date(2,8,2023)
settle_time = 2

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 = {}
for x in rates.keys():
    swaps.update(
        {
            ql.Period(x):rates.get(x)/100
        }
    )        

#build helpers
rate_helpers = []
for tenor, rate in swaps.items():
    helper = ql.OISRateHelper(settle_time, tenor, ql.QuoteHandle(ql.SimpleQuote(rate)), index)
    rate_helpers.append(helper)

#build curve based on swap helpers
curve = ql.PiecewiseFlatForward(calculation_date, rate_helpers, ql.Actual360())
curve.enableExtrapolation()
yts.linkTo(curve)
engine = ql.DiscountingSwapEngine(yts)


#used to compare zeros and discount factors back to bloomberg
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}")

#create monthly cadence for differrent tenors of forward rates
days = ql.MakeSchedule(curve.referenceDate(), curve.maxDate(), ql.Period('3M'))

dates,list_3mo,list_1mo,list_6mo,list_12mo = [],[],[],[],[]
for d in days:    
    forward_3mo = curve.forwardRate(
        d,
        ql.UnitedStates(ql.UnitedStates.Settlement).advance(d,90,ql.Days),
        ql.Actual365Fixed(),
        ql.Continuous,
    ).rate()
    
    forward_1mo = curve.forwardRate(
        d,
        ql.UnitedStates(ql.UnitedStates.Settlement).advance(d,30,ql.Days),
        ql.Actual365Fixed(),
        ql.Continuous,
    ).rate()
    
    forward_6mo = curve.forwardRate(
        d,
        ql.UnitedStates(ql.UnitedStates.Settlement).advance(d,ql.Period('6M')),
        ql.Actual365Fixed(),
        ql.Continuous,
    ).rate()
    
    forward_12mo = curve.forwardRate(
        d,
        ql.UnitedStates(ql.UnitedStates.Settlement).advance(d,360,ql.Days),
        ql.Actual365Fixed(),
        ql.Continuous,
    ).rate()
    
    dates.append(d)
    list_1mo.append(forward_1mo)
    list_3mo.append(forward_3mo)
    list_6mo.append(forward_6mo)
    list_12mo.append(forward_12mo)

#save output
df = pd.DataFrame(data=zip(dates,list_1mo,list_3mo,list_6mo,list_12mo),columns=('Dates','SOFR_1MO','SOFR_3MO','SOFR_6MO','SOFR_12MO'))
print(df)
df.to_csv('forward_rates.csv')

Bloomberg Snap of Forward Rates

The dates that I return versus the screenshot are off by 2 days and the forward rates / zero rates are slightly off. Can anyone provide any insight into what some of the difference might be? The forward rates shown by bloomberg are continuous | ACT/365.

$\endgroup$
2
  • $\begingroup$ USD SOFR is act360 by convention, are you sure bloomberg is showing act365F? $\endgroup$
    – Attack68
    Aug 2 at 23:03
  • $\begingroup$ Correct me if Im wrong but I believe I am creating the curve using Actual/360 and then the forward rates that bloomberg show are the Act365 convention. $\endgroup$ Aug 3 at 13:14

0

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.

Browse other questions tagged or ask your own question.