# GBP OIS Curve - Zero Rate Curve Calculation in Quantlib

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.

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:

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

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:

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.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":

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

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