Skip to content
All library documents

Building a GBP OIS Discount Curve from Market Quotes

Article Quant Q&A · Author: TheGr8Destructo

Summary

The document addresses how to construct a GBP overnight index swap curve in QuantLib for discounting fixed rate bonds. Its central recommendation is to represent the market instruments with overnight indexed swap rate helpers, rather than treating quoted swap rates as if they were coupon bond yields or feeding them directly into a zero rate curve. The helpers use market quotes and the relevant overnight index to bootstrap a term structure.

The example builds a piecewise log-linear discount curve and displays the resulting discount factors at curve dates. It also notes that QuantLib offers alternative interpolation choices, including methods based on discounts, zero rates, or forwards. The illustrated curve does not exactly reproduce the reference market display; missing conventions or interpolation settings may account for the difference. Accurate replication therefore requires matching instrument conventions, index, calendars, dates, day counts, and curve construction settings to the market data.

Key ideas

  • Use overnight indexed swap rate helpers to bootstrap an OIS curve from OIS quotes.
  • A piecewise discount curve can provide discount factors for bond valuation.
  • Market swap rates should be modeled as their quoted instruments rather than treated as zero rates.
  • Interpolation choice affects the shape of the term structure between market maturities.
  • Matching a reference curve requires the appropriate market conventions and inputs.

Tags

Full text
# GBP OIS Curve - Zero Rate Curve Calculation in Quantlib


# 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.

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 |

## Answer by Xiarpedia (score 3, accepted)

https://quant.stackexchange.com/a/78775

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.

Shown in full with attribution under the source's licence. Licence: CC BY-SA 4.0 (Stack Exchange)

This summary was written by Stratmill's research agent from the original; it is not a copy of the source.