1
$\begingroup$

I am new to Quantlib and I am looking to create a Zero Rate Curve from GBP OIS to then use to calculate the present value of fixed rate bonds.

I have Looked at the documentation: https://quantlib-python-docs.readthedocs.io/en/latest/termstructures.html

Initially I tried the ql.ZeroCurve structure, but I believe this is the curve structure that should be used once you have your zero rates.

I then implemented the PiecewiseLogLinearDiscount and PiecewiseLogCubicDiscount curves and the passed these the rates and dates as if the OIS curve is a par instrument. See "get_spot_rates" method below.

Please can someone help me

  1. Select the correct yield term structure and
  2. Generate a zero rate curve from this yield term structure to then use this in ql.DiscountingBondEngine(term_structure)?

There has to be a simpler, more elegant, and importantly, correct way to do this.

Thank you.

The below image is the Bloomberg OIS curve data with discount factors and zero rates:

Bloomberg Piecewise Linear Continous Interpolation of GBP OIS curve

The second image shows the zero rates that I am generating:

zero rates generated using code

Clearly I am going about this the wrong way and I think that quantlib should calculate this straight out of the box.

Here is the code I am using:

get_zero_rate_curve(ql.Date(25,3,2024)).get_spot_rates()

def get_zero_rate_curve(settlement_date:ql.Date)->ZeroRateCurve:
x = get_swap_rates(settlement_date)
dates, rates = zip(*x)
dates = [d for d in dates]
rates = [r/100 for r in rates]

zc = ZeroRateCurve( 
                    settlement_date=settlement_date,
                    dates=dates,
                    rates=rates,
                    day_count = ql.Actual365Fixed(),
                    interpolation_method="linear"
                    )

return zc

def get_swap_rates(settlement_date:ql.Date)->typing.Iterator:

swp = pd.read_csv("swap_rates.csv", header=0)
rates = swp["Ask"].to_list()
terms = swp["Term"].to_list()
periods = swp["Unit"].to_list()

terms_periods = zip(terms,periods)
ql_periods = []

for t,p in terms_periods:
    if p ==  "WK":
        ql_periods.append(settlement_date + ql.Period(t,ql.Weeks))
    elif p == "MO":
        ql_periods.append(settlement_date + ql.Period(t,ql.Months))
    elif p == "YR":
        ql_periods.append(settlement_date + ql.Period(t,ql.Years))
    else:
        raise ValueError ("period can only be WK, MO or YR")

return zip(ql_periods, rates)


class ZeroRateCurve:
"""
A Python class that defines a zero-rate curve using QuantLib.
"""

def __init__(self,settlement_date, dates, rates, day_count, interpolation_method):
    
    # rates.insert(0,0.0)
    # dates.insert(0,settlement_date)
    
    self.dates = dates
    self.rates = rates
    self.day_count = day_count
    self.interpolation_method = interpolation_method
    self.settlement_date = settlement_date
    ql.Settings.instance().evaluationDate = self.settlement_date
    self.curve = self._create_curve()

def _create_curve(self):

    """Constructs a zero curve based on provided market data and parameters.

    Args:
        settlement_date (Date): The settlement date for the zero curve.
        maturities (list): A list of Period objects representing bond maturities.
        yields (list):  A list of corresponding yields (as decimals).
        day_count (DayCount): The day count convention to use.
        interpolation_method (str): The interpolation method to use ("linear" or "spline").

    Returns:
        ZeroCurve: The constructed zero curve object.
    """
    # Create a list of market prices (assuming all bonds have face value 100)
    helpers = []
     
    for r, d in zip(self.rates, self.dates):
        maturity = d
        schedule = ql.Schedule(
                                self.settlement_date,
                                maturity,
                                ql.Period(ql.Semiannual),
                                ql.TARGET(),
                                ql.Unadjusted,
                                ql.Unadjusted,
                                ql.DateGeneration.Backward,
                                True)

        price = ql.QuoteHandle(ql.SimpleQuote(100))
        helper = ql.FixedRateBondHelper(price, 2, 100, schedule, [r], self.day_count)
        helpers.append(helper)

    if self.interpolation_method == "linear":
        # Use PiecewiseLogLinearDiscount with helpers
        curve = ql.PiecewiseLogLinearDiscount(self.settlement_date, helpers, self.day_count)
    elif self.interpolation_method == "cubic":
        # Use PiecewiseLogCubicDiscount with helpers
        curve = ql.PiecewiseLogCubicDiscount(self.settlement_date, helpers, self.day_count)
    else:
        raise ValueError("Invalid interpolation method. Choose 'linear' or 'cubic'")
    return curve

def get_handle(self):
    """
    Returns a YieldTermStructureHandle for the zero-rate curve.
    """

    return ql.YieldTermStructureHandle(self.curve)

def get_spot_rates(self):

    spots = []
    tenors = []
    ref_date = self.curve.referenceDate()
    
    calc_date = ref_date 

    for date in self.dates:
        yrs = self.day_count.yearFraction(calc_date, date)
        compounding = ql.Continuous
        freq = ql.Annual
        zero_rate = self.curve.zeroRate(date,ql.Actual365Fixed(),compounding)
        tenors.append(date)
        eq_rate = zero_rate.equivalentRate(
            self.day_count,compounding,freq,calc_date,date).rate()
        spots.append(100*eq_rate)
    
    return pd.DataFrame(list(zip(tenors, spots)),
            columns=["Maturities","Curve"],
            index=[""]*len(tenors))

'''

And this is the first half of the "swap_rates.csv":

Term Unit Ticker Bid Ask Spread Bid Spr Val Ask Spr Val Final Bid Rate Final Ask Rate Rate Type Daycount Freq
1 WK BPSWS1Z 5.186080933 5.194919586 0 0 5.186080933 5.194919586 Swap Rates ACT/365 1
2 WK BPSWS2Z 5.187725067 5.197275162 0 0 5.187725067 5.197275162 Swap Rates ACT/365 1
1 MO BPSWSA 5.193910599 5.20308876 0 0 5.193910599 5.20308876 Swap Rates ACT/365 1
2 MO BPSWSB 5.179055691 5.198744297 0 0 5.179055691 5.198744297 Swap Rates ACT/365 1
3 MO BPSWSC 5.1726408 5.181359291 0 0 5.1726408 5.181359291 Swap Rates ACT/365 1
4 MO BPSWSD 5.140148163 5.156652451 0 0 5.140148163 5.156652451 Swap Rates ACT/365 1
5 MO BPSWSE 5.100813866 5.114186287 0 0 5.100813866 5.114186287 Swap Rates ACT/365 1
6 MO BPSWSF 5.073588848 5.081411839 0 0 5.073588848 5.081411839 Swap Rates ACT/365 1
7 MO BPSWSG 5.035993576 5.046205521 0 0 5.035993576 5.046205521 Swap Rates ACT/365 1
8 MO BPSWSH 4.999409199 5.007991314 0 0 4.999409199 5.007991314 Swap Rates ACT/365 1
9 MO BPSWSI 4.96168375 4.96671629 0 0 4.96168375 4.96671629 Swap Rates ACT/365 1
10 MO BPSWSJ 4.921626091 4.927973747 0 0 4.921626091 4.927973747 Swap Rates ACT/365 1
11 MO BPSWSK 4.883406639 4.889992714 0 0 4.883406639 4.889992714 Swap Rates ACT/365 1
1 YR BPSWS1 4.848460197 4.853509903 0 0 4.848460197 4.853509903 Swap Rates ACT/365 1
18 MO BPSWS1F 4.558068752 4.566730976 0 0 4.558068752 4.566730976 Swap Rates ACT/365 1
2 YR BPSWS2 4.373448849 4.378611088 0 0 4.373448849 4.378611088 Swap Rates ACT/365 1
$\endgroup$

1 Answer 1

3
$\begingroup$

If you intend to find the zero rates or the discount factors of the OIS curve for GBP then I would use the following approach where instead of using FixedRateBondHelper I use OISRateHelper:

df = pd.read_clipboard()  # Read the data from the posted question
ql.Settings.instance().evaluationDate = ql.Date("2024-03-25", "%Y-%m-%d")
helpers = []
for row in df.iterrows():
    mid = ((row[1].Bid + row[1].Ask) / 2) / 100
    if row[1].Unit == "WK":
        helpers.append(
            ql.OISRateHelper(
                0,
                ql.Period(row[1].Term, ql.Weeks),
                ql.QuoteHandle(ql.SimpleQuote(mid)),
                ql.Estr(),
            )
        )
    elif row[1].Unit == "MO":
        helpers.append(
            ql.OISRateHelper(
                0,
                ql.Period(row[1].Term, ql.Months),
                ql.QuoteHandle(ql.SimpleQuote(mid)),
                ql.Estr(),
            )
        )
    elif row[1].Unit == "YR":
        helpers.append(
            ql.OISRateHelper(
                0,
                ql.Period(row[1].Term, ql.Years),
                ql.QuoteHandle(ql.SimpleQuote(mid)),
                ql.Estr(),
            )
        )

curve = ql.PiecewiseLogLinearDiscount(0, ql.TARGET(), helpers, ql.Actual365Fixed())
date, curve = zip(*curve.nodes())
date = [d.ISO() for d in date]
display(pd.DataFrame().from_dict({"date": date, "curve": curve}))

The result is then:

date curve
2024-03-25 1
2024-04-02 0.998848
2024-04-08 0.997985
2024-04-25 0.995543
2024-05-27 0.991001
2024-06-25 0.986943
2024-07-25 0.982852
2024-08-26 0.978618
2024-09-25 0.974705
2024-10-25 0.970905
2024-11-25 0.967068
2024-12-27 0.963209
2025-01-27 0.959569
2025-02-25 0.956256
2025-03-25 0.953122
2025-09-25 0.934063
2026-03-25 0.917025

Which is not a perfect match to your image. However, I missing some parameters such as interpolation method to make an exact curve. But it should point you towards the right direction of replicating the curve! I would also like to point out that I used PiecewiseLogLinearDiscount and the QuantLib API allows the following calls:

  • PiecewiseLogLinearDiscount
  • PiecewiseLogCubicDiscount
  • PiecewiseLinearZero
  • PiecewiseCubicZero
  • PiecewiseLinearForward
  • PiecewiseSplineCubicDiscount

You can read about it here.

$\endgroup$
1
  • 1
    $\begingroup$ thanks #Xiarpedia - that works well. Much appreciated! $\endgroup$ Commented Mar 26 at 13:58

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