0
$\begingroup$

I'm trying to build the yield curve simply using Treasury yields one would get by querying the FRED API, but as my code below will show, I'm told on the 1st iteration that there was failure at the 4th live instrument (which would be the 1-Yr Treasury, which makes me think that the issue is either in the call to FixedRateBondHelper() or the schedule we pass to it with MakeSchedule(), and not to DepositRateHelper()) because the root is not bracketed.

I checked for the usual culprits: inconsistent manner of expressing yields, failure to use DepositRateHelper() for sub-annual instruments, I've tried different dates, and I've tried multiple Yield Term Structure functions. No success. I'm using simple Treasury Yields and nothing exotic (no building curves based on swaps that call for different functions), so I'm sure that what I've done wrong can be found if someone runs the same numbers that I have included ready to run in my code below (traceback is below too). Thank you for your time.

import QuantLib as ql, pandas as pd, datetime as dt

class YieldCurve():
    def __init__(self, yields):
        """
        yields(pandas Series): pandas series in which the values are floats representing the yield and the index is the name of
        the particular bond, and the "name" attribute is a Pandas Timestamp. Eg. what is output from a call to pandas DataReader:
        """
        #PRELIMINARY WORK
        self.yields = yields
        self.date = self.yields.name.date()

        # BUILD YIELD CURVE
        ## Format QuantLib Arguments
        self.valuation_date = ql.Date(self.date.day, self.date.month, self.date.year)#self.date in ql format
        ql.Settings.instance().evaluationDate = self.valuation_date#Instantiate QuantLib date for the valuation date
        self.day_count = ql.Actual360()

        ## Obtain Rate Helpers
        self.rate_helpers = []
        ### Get variables either common to both deposit and fixed-rate bond helpers or used for multiple instruments within those helpers
        for maturity, yield_value in self.yields.items():
            yield_value = yield_value / 100.0
            quote = ql.SimpleQuote(yield_value)
            calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)#we use this for an American yield curve as opposed to ql.Target() for Euro-denominated instruments. The argument provided was come upon when providing no argument would throw a TypeError. We saw this argument used in a stack exchange post at https://quant.stackexchange.com/questions/60330/quantlib-python-ql-schedule-getting-end-of-month-dates
        ### Get deposit rate helpers
            if maturity.endswith('MO'):
                # Use deposit rate helpers for Treasury bills
                maturity_period = ql.Period(int(maturity[3:-2]), ql.Months)
                compounding_frequency = ql.Simple
                rate_helper = ql.DepositRateHelper(
                    ql.QuoteHandle(quote),#quote arg
                    maturity_period,#tenor arg
                    0,#fixingDays arg
                    calendar,#calendar arg
                    ql.ModifiedFollowing,#convention arg. this is the business day convention, not the daycount convention, and ModifiedFollowing is appropriate for the American treasury market
                    False,#endOfMonth arg
                    self.day_count#dayCounter arg. equivalent of day count convention 
                )
                self.rate_helpers.append(rate_helper)
        ### Get fixed-rate bond helpers
            else:# Use fixed-rate bond helpers for Treasury notes/bonds
                ### Define variables relevant to all instruments that use FixedRateBondHelper
                maturity_period = ql.Period(int(maturity[3:]), ql.Years)
                maturity_date = self.valuation_date + maturity_period
                print("valuation date: ", self.valuation_date)
                print("maturity_date: ", maturity_date)
                compounding_frequency = ql.Semiannual
                #### Get schedule for instrument
                schedule = ql.MakeSchedule(
                    self.valuation_date,#effectiveDate param
                    maturity_date,#terminationDate param
                    frequency = compounding_frequency,  # Tenor or frequency of the schedule (e.g., semiannual)
                    calendar = calendar,#calendar param
                    convention = ql.ModifiedFollowing,#convention parameter. Business day convention for schedule generation
                    terminalDateConvention = ql.ModifiedFollowing,#terminalDateConvention parameter. Business day convention for adjusting the end date
                    rule = ql.DateGeneration.Backward,#Date generation rule. used to generate the bond schedule by working backward from the maturity date to the valuation date. It determines how the intermediate cash flow dates are calculated between these two dates. This approach ensures that the cash flows are correctly scheduled in reverse order, aligning with the bond's structure.
                    endOfMonth = False#endOfMonth param. determines whether the cash flow dates should be adjusted to the end of the month. Setting it to False means that the cash flow dates should not be adjusted to the end of the month.In the context of the American Treasury market, it is typical to set endOfMonth to False since Treasury bonds typically have cash flow dates that are not adjusted to the end of the month.
                )
                #### Call FixedRateBondHelper()
                print("yield_value: ", yield_value)
                rate_helper = ql.FixedRateBondHelper(
                    ql.QuoteHandle(quote),
                    2, #settlementDays arg. the number of business days between the trade date (when the transaction is executed) and the settlement date (when the transaction is settled and ownership of the securities is transferred). 2 was used in docs and this is appropriate for the AMerican treasury market
                    100,#faceAmount arg
                    schedule,#schedule param
                    [yield_value],#coupons param
                    self.day_count,#dayCounter param
                    ql.ModifiedFollowing,#paymentConv param. a common choice for adjusting payment dates. It follows the rule that if a payment date falls on a non-business day, it is adjusted to the next valid business day unless that day belongs to a different month. In the latter case, the payment date is adjusted to the previous valid business day. For the American Treasury market, the ql.ModifiedFollowing convention is often used to handle potential adjustments for weekends, holidays, or other non-business days while maintaining consistency with the general market practice.
                    100, #redemption param. represents the redemption value or par value of the bond.
                    ql.Date(), ## issueDate, set to default value
                    calendar, #paymentCalendar param
                    maturity_period,#exCouponPeriod param
                    calendar, #exCouponCalendar param
                    ql.ModifiedFollowing, #exCouponConvention param
                    False,#exCouponEndOfMonth param
                    False#useCleanPrice param. Whether to use clean price or dirty price (set to False for dirty price)
                )
                self.rate_helpers.append(rate_helper)
            
        ## Construct Yield Curve
        self.yield_curve = ql.PiecewiseLogCubicDiscount(self.valuation_date,
                             self.rate_helpers,
                             self.day_count)

sample_yields = pd.Series([5.25, 5.46, 5.50, 5.41, 4.87, 4.49, 4.14, 3.99, 3.85, 4.11, 3.92], index = ['DGS1MO', 'DGS3MO', 'DGS6MO', 'DGS1', 'DGS2', 'DGS3', 'DGS5', 'DGS7', 'DGS10', 'DGS20', 'DGS30'])
sample_yields.name = dt.datetime.strptime('2023-07-03', '%Y-%m-%d')

yc_obj = YieldCurve(sample_yields)

Traceback:

---------------------------------------------------------------------------
RuntimeError                              Traceback (most recent call last)
Cell In[8], line 1
----> 1 yc_obj = YieldCurve(yields)

Cell In[2], line 146, in YieldCurve.__init__(self, yields)
    139 self.yield_curve = ql.PiecewiseLogCubicDiscount(self.valuation_date,
    140                      self.rate_helpers,
    141                      self.day_count)
    145 # PHASE : EXTRACT YIELD CURVE ATTRIBUTES
--> 146 self.curve_dates = self.yield_curve.dates()
    147 self.curve_rates = [self.yield_curve.zeroRate(date, self.day_count, ql.Continuous).rate() for date in self.curve_dates]

File ~\anaconda3\envs\x00\lib\site-packages\QuantLib\QuantLib.py:26649, in PiecewiseLogCubicDiscount.dates(self)
  26647 def dates(self):
  26648     r"""dates(PiecewiseLogCubicDiscount self) -> DateVector"""
> 26649     return _QuantLib.PiecewiseLogCubicDiscount_dates(self)

RuntimeError: 1st iteration: failed at 4th alive instrument, pillar July 3rd, 2024, maturity July 3rd, 2024, reference date July 3rd, 2023: root not bracketed: f[0.586591,1.61233] -> [-5.862737e+01,-1.612583e+02]
```
$\endgroup$

1 Answer 1

1
$\begingroup$

The quote required by FixedRateBondHelper is a price. In this case, since you're quoted par yields, you'll have to pass yield_value as the coupon rate (which you're already doing) and pass 100 as the quote instead. With this change, your code bootstraps successfully.

$\endgroup$

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