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]
```