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$ Mar 26 at 13:58

Your Answer

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

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