1
$\begingroup$

i'm trying to get payment schedule of a bond that pays coupon quarterly. value date is 2020-04-01, first coupon date is 2020-06-30, maturity date is 2022-09-30. so I expect to have a list of dates:

2020-04-01

2020-06-30

2020-09-30

2020-12-31

2021-03-31

2021-06-30

2021-09-30

2021-12-31

2022-03-31

2022-06-30

2022-09-30

however, when I use schedule as below

list(ql.Schedule(
ql.Date('01-04-2020', '%d-%m-%Y'),
ql.Date('30-09-2022', '%d-%m-%Y'),
ql.Period("3m"),
ql.UnitedStates(),
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Forward,
False,
ql.Date('30-06-2020', '%d-%m-%Y'),
))

i got the following outputs, for March and December it's not end of month. I can see there is endOfMonth but if i set it to true it also changes 2020-04-01 to 2020-04-30 which I don't want it to happen. according to doc, endOfMonth can be used if the start date is at the end of the month, whether other dates are required to be scheduled at the end of the month (except the last date). and in my case the start date is not at the end of the month. any suggestions to achieve the correct coupon payment dates?

2020-04-01

2020-06-30

2020-09-30

2020-12-30

2021-03-30

2021-06-30

2021-09-30

2021-12-30

2022-03-30

2022-06-30

2022-09-30

$\endgroup$

2 Answers 2

4
$\begingroup$

The constructor for a Schedule in QuantLib is:

ql.Schedule(effectiveDate, terminationDate, tenor,
            calendar, convention, terminationDateConvention, rule,
            endOfMonth, firstDate=Date(), nextToLastDate=Date()
)

The way you have it defined, you are basically rolling on the 30th and that's why you have a difference in those 4 dates:

mydates =  [
'2020-04-01', 
'2020-06-30', '2020-09-30', '2020-12-31', '2021-03-31', '2021-06-30',
'2021-09-30', '2021-12-31', '2022-03-31', '2022-06-30', '2022-09-30',    
]

schedule = ql.Schedule(
ql.Date('01-04-2020', '%d-%m-%Y'),
ql.Date('30-09-2022', '%d-%m-%Y'),
ql.Period("3m"),
ql.UnitedStates(),
ql.ModifiedFollowing,
ql.ModifiedFollowing,
ql.DateGeneration.Forward,
False,
ql.Date('30-06-2020', '%d-%m-%Y'))

pd.DataFrame({
    "mydates": mydates,
    "qlDates": [dt.ISO() for dt in schedule]
}).style.apply(lambda row: ["background: red; color: white"]*2  if row[0] != row[1] else [""]*2, axis = 1)

enter image description here

Also, notice that one of the dates in your list is not a business day in the ql.UnitedStated calendar so you can't generate those dates with an adjusted business day convention:

cal = ql.UnitedStates()
[cal.isBusinessDay(ql.Date(dt, '%Y-%m-%d')) for dt in mydates]

[True, True, True, True, True, True, True, False, True, True, True]

So my sugestion would be:

  • Change the conventions to Unadjusted since you want a date that is not a business day
  • Change the endOfMonth to True
  • Change the date generation rule to Backward
  • Loose the firstDate parameter as you don't have any stub (and you were actually setting the roll date to the 30th with this)
schedule = ql.Schedule(
ql.Date('01-04-2020', '%d-%m-%Y'),
ql.Date('30-09-2022', '%d-%m-%Y'),
ql.Period("3m"),
ql.UnitedStates(),
ql.Unadjusted,
ql.Unadjusted,
ql.DateGeneration.Backward,
True)

pd.DataFrame({
    "mydates": mydates,
    "qlDates": [dt.ISO() for dt in schedule]
}).style.apply(lambda row: ["background: red"]*2  if row[0] != row[1] else [""]*2, axis = 1)

enter image description here

Edit after comments: As Dimitri very well pointed out there is no single "USD Holiday" calendar and what you have are several combinations of US holidays used by different markets/asset classes/exchanges.

In QuantLib, you can specify some of these. For example, that date in question (31-12-2021) will be treated diferently throughout the alternatives:

mydate = ql.Date(31,12,2021)
cal = ql.UnitedStates()
alts = ['FederalReserve', 'GovernmentBond', 'LiborImpact', 'NERC', 'NYSE', 'Settlement']
for alt in alts:
    mycalendar = eval(f'ql.UnitedStates(ql.UnitedStates.{alt})')
    print(alt, mycalendar.isBusinessDay(mydate))

FederalReserve True
GovernmentBond True
LiborImpact False
NERC True
NYSE True
Settlement False

Also a great point about "junior generated" schedules and I've seen it a few times. Some schedules, and it can be for bonds or derivatives, simply can't be generated by market convention rules, and in extreme cases you might just have to insert them manually. In QuantLib, you can do it like so:

schedule = ql.Schedule([ql.Date(dt, '%Y-%m-%d') for dt in mydates])
print([*schedule])
$\endgroup$
2
  • $\begingroup$ thank you for your answer, your code resolved this issue, also sorry I missed 31st Dec is a USD holiday. however i'd like to get more knowledge on this, I do have a portfolio of bonds and some of them do have stub in the beginning. so it seems that I may need to keep this forward so that it works for majority of the cases. in this particular example, let's say the issue date is 26th March 2020. how would it work? $\endgroup$
    – user51725
    Commented Jan 6, 2021 at 3:27
  • $\begingroup$ isn't USD holiday meaning just USD currency settlement holiday? and this concept applies to other currencies too? there are also different holidays used in other countries too for stocks and bonds? $\endgroup$
    – user51725
    Commented Jan 6, 2021 at 5:20
2
$\begingroup$

I'd like to add some color to David Duarte's excellent answer.

When a corporate or emerging markets bond originates, some junior person is tasked with making up the list of coupon dates for the prospectus. These days they usually use a tool that undersands commonly used conventions, but decades ago they did it manually and sometimes messed up. I used to check to make sure that my library (not ql, but a similar scheduler, a little more flexible) matched the dates in the prospectus, and ended up writing a few lines of C++ code that took as input the dates from the prospectus and the parameters on Bloomberg terminal (if available) and tried different combinations of the scheduler parameters trying to match the dates. Sometimes we encountered a bond where this failed, so the schedule from the prospectus had to be a custom list of dates. This seldom happens these days, was not needed for your bond, but operationally one should be prepared for this eventuality.

The code also indicated whether you need to specify firstDate for odd first period and penultimate (nextToLastDate) for odd last period. In my opinion, it is better practice not to pass them if they are redundant - only pass them if they make a difference. Many bonds have odd first and/or last periods. (Very few bonds have odd periods in the middle of the schedule.)

You may want to write a tool like this for QL schedule and contribute it to the community.

Please keep in mind that there is no such thing as "USD Holiday". While for many other currencies, there is one calendar that everyone uses, so one can call the London calendar "GBP Calendar", this just doesn't work for US. There are different holidays used in the US for stocks (NYSE) and bonds and a few others too. Using ql.UnitedStates() without specifying which calendar you mean (e.g. GovernmentBond) can lead to confusion. Take a look at https://www.sifma.org/resources/general/holiday-schedule/ and scroll down to "U.S. Holiday Recommendations". Click on 2021, observe that it says "Early Close (2:00 p.m. Eastern Time): Friday, December 31, 2021".

Let's try your problematic date in QL:

NewYearsEve = ql.Date(31,12,2021) # Because New Years day, January 1st, 2022 is a Saturday, some holiday calendars have an observed holiday here

ql.UnitedStates().isBusinessDay(NewYearsEve) # Bad!

ql.UnitedStates(ql.UnitedStates.Settlement).isBusinessDay(NewYearsEve)

ql.UnitedStates(ql.UnitedStates.LiborImpact).isBusinessDay(NewYearsEve)

are False, but

ql.UnitedStates(ql.UnitedStates.NYSE).isBusinessDay(NewYearsEve)

ql.UnitedStates(ql.UnitedStates.GovernmentBond).isBusinessDay(NewYearsEve)

ql.UnitedStates(ql.UnitedStates.NERC).isBusinessDay(NewYearsEve)

ql.UnitedStates(ql.UnitedStates.FederalReserve).isBusinessDay(NewYearsEve)

are True. I see no reason not to have a bond coupon on this date.

An example of stocks and bonds using different calendars

IndigenousPeoplesDay = ql.Date(11,10,2021) # aka Canadian Thanksgiving, Columbus Day

ql.UnitedStates().isBusinessDay(IndigenousPeoplesDay) # Bad!

ql.UnitedStates(ql.UnitedStates.Settlement).isBusinessDay(IndigenousPeoplesDay) )

ql.UnitedStates(ql.UnitedStates.GovernmentBond).isBusinessDay(IndigenousPeoplesDay)

are False, but

ql.UnitedStates(ql.UnitedStates.NYSE).isBusinessDay(IndigenousPeoplesDay)

is True - you can trade stocks.

Likewise Quantlib has UnitedKingdom()
https://rkapl123.github.io/QLAnnotatedSource/d4/deb/class_quant_lib_1_1_united_kingdom.html and Brazil() holidays, which are similarly problematic. For example, in Scotland, Easter Monday is not a bank holiday, but Hogmanay is, and the Summer Bank Holidaonis on the first Monday in August, rather than the last. In Brazil, the holidays when CDI (overnight interest rate similar to SOFR) is not published are not the same as the holidays when exchange-traded DI futures don't trade.

CDR function on Bloomberg Terminal is a great place to explore holiday calendars if you're curious.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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