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
- Select the correct yield term structure and
- 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:
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 |