QuantLib icon indicating copy to clipboard operation
QuantLib copied to clipboard

EOM Schedule Generation

Open pcaspers opened this issue 4 years ago • 5 comments

Consider the schedules s1and s2generated by the following code

#include <iostream>
#include <ql/time/calendars/nullcalendar.hpp>
#include <ql/time/schedule.hpp>

using namespace QuantLib;

int main() {

    Schedule s1(Date(15, January, 2020), Date(30, June, 2022), 1 * Months, NullCalendar(), Unadjusted, Unadjusted,
                DateGeneration::Forward, true, Date(31, January, 2020), Null<Date>());

    Schedule s2(Date(15, January, 2020), Date(30, June, 2022), 1 * Months, NullCalendar(), Unadjusted, Unadjusted,
                DateGeneration::Backward, true, Date(31, January, 2020), Null<Date>());

    std::cout << "Schedule 1:" << std::endl;
    for (auto const &d : s1)
        std::cout << d << std::endl;

    std::cout << "\nSchedule 2:" << std::endl;
    for (auto const &d : s2)
        std::cout << d << std::endl;

    return 0;
}

While s2 looks sort of expected, s1 is missing out the front date 15-01-2020, see the output below.

I wonder if this is intended or a bug in the schedule generation?

Schedule 1:
January 31st, 2020
February 29th, 2020
March 31st, 2020
April 30th, 2020
May 31st, 2020
June 30th, 2020
July 31st, 2020
August 31st, 2020
September 30th, 2020
October 31st, 2020
November 30th, 2020
December 31st, 2020
January 31st, 2021
February 28th, 2021
March 31st, 2021
April 30th, 2021
May 31st, 2021
June 30th, 2021
July 31st, 2021
August 31st, 2021
September 30th, 2021
October 31st, 2021
November 30th, 2021
December 31st, 2021
January 31st, 2022
February 28th, 2022
March 31st, 2022
April 30th, 2022
May 31st, 2022
June 30th, 2022

Schedule 2:
January 15th, 2020
January 31st, 2020
February 29th, 2020
March 31st, 2020
April 30th, 2020
May 31st, 2020
June 30th, 2020
July 31st, 2020
August 31st, 2020
September 30th, 2020
October 31st, 2020
November 30th, 2020
December 31st, 2020
January 31st, 2021
February 28th, 2021
March 31st, 2021
April 30th, 2021
May 31st, 2021
June 30th, 2021
July 31st, 2021
August 31st, 2021
September 30th, 2021
October 31st, 2021
November 30th, 2021
December 31st, 2021
January 31st, 2022
February 28th, 2022
March 31st, 2022
April 30th, 2022
May 31st, 2022
June 30th, 2022

pcaspers avatar Aug 20 '20 08:08 pcaspers

Do we think this is a bug? I am happy to work on this in this case.

pcaspers avatar Sep 24 '20 08:09 pcaspers

I think in the first case there's code that clips the start date to the end of month. It did look wrong to me, but I'm not sure. Glad to hear a second opinion 😄

I suppose the same happens in reverse if you go backwards from a maturity in the middle of the month?

lballabio avatar Sep 24 '20 08:09 lballabio

I'd have to check the second case, I don't know at the moment. I will have a look.

pcaspers avatar Sep 24 '20 08:09 pcaspers

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Dec 24 '20 10:12 stale[bot]

I'm also having this issue; two more test cases in case it may be helpful:

void ScheduleTest::testEomKeepsStart() {
    Schedule s1 = Schedule(
        Date(3, August, 2021),
        Date(31, July, 2024),
        Period(6, Months),
        UnitedStates(),
        Unadjusted,
        Unadjusted,
        DateGeneration::Forward,
        true,
        Date(31, January, 2022),
        Date(31, January, 2024)
    );

    BOOST_CHECK_EQUAL(s1[0], Date(3, August, 2021));

    Schedule s2 = Schedule(
        Date(21, December, 2020),
        Date(16, February, 2031),
        Period(6, Months),
        UnitedStates(),
        Unadjusted,
        Unadjusted,
        DateGeneration::Forward,
        true,
        Date(31, March, 2021),
        Date(30, September, 2030)
    );

    BOOST_CHECK_EQUAL(s2[0], Date(21, December, 2020));
}

bspeice avatar Apr 27 '22 14:04 bspeice

Ciao all, as Luigi suggested there is code that clips the start date of the schedule plan to the end of the month. It seems that this behaviour is happening when the Schedule constructor is invoked with: -endOfMonth argument true -DateGeneration::Rule Forward -terminationDateConvention Unadjusted -firstDate is on or after the last business day for that month -effectiveDate and firstDate are falling in the same month and in the same year

The code is in schedule.cpp, lines 370-386

Date d1 = dates_.front(), d2 = dates_.back();
if (terminationDateConvention != Unadjusted) {
    d1 = calendar_.endOfMonth(dates_.front());
    d2 = calendar_.endOfMonth(dates_.back());
} else {
    // the termination date is the first if going backwards,
    // the last otherwise.
    if (*rule_ == DateGeneration::Backward)
        d2 = Date::endOfMonth(dates_.back());
    else
        d1 = Date::endOfMonth(dates_.front());
}
// if the eom adjustment leads to a single date schedule
// we do not apply it
if(d1 != d2) {
    dates_.front() = d1;
    dates_.back() = d2;

Consider the following example: effectiveDate 16 January 2023, terminationDate 28 February 2023, firstDate 31 January 2023. A simple plan with 3 schedule lines. In the code we have d1 = dates_.front() = effective date, d2 = dates_.back = termination date. The assignment d1 = Date::endOfMonth(dates_.front()) will move the effective date at the end of that month, therefore it will be the same as the first date. With the assignment dates_.front() = d1 the first schedule line with the effective date is lost. Maybe in order to manage this exceptional case we could write something like this:

    else if (effectiveDate.month() != firstDate_.month() || effectiveDate.year() != firstDate_.year())
        d1 = Date::endOfMonth(dates_.front());

Any feedback from QuantLib experienced developers would be appreciated. Here is the source code used to test the case

#include <iostream>
#include <ql/time/all.hpp>

using namespace QuantLib;

int main(){
    Schedule s1 =
        MakeSchedule().from(Date(16, January, 2023))
                      .to(Date(28, February, 2023))
                      .withCalendar(NullCalendar())
                      .withTenor(1 * Months)
                      .withConvention(Unadjusted)
                      .withTerminationDateConvention(Unadjusted)
                      .forwards()
                      .endOfMonth()
                      .withFirstDate(Date(31, January, 2023));
    
    for(auto const &d:s1)
        std::cout << d << '\n';
    return 0;
}

jakeheke75 avatar Dec 22 '22 16:12 jakeheke75

Hello @lballabio, which is your opinion about this issue? Could it be a corner case happening when effectiveDate and firstDate are falling in the same month and in the same year AND the effectiveDate is moved at the end of the month, effectively becoming equal to the firstDate? If you think that the analysis above makes sense I will try to give it a shot with a PR. Thanks.

jakeheke75 avatar Apr 19 '23 11:04 jakeheke75

Hopefully fixed by #1509.

lballabio avatar May 29 '23 12:05 lballabio