QuantLib icon indicating copy to clipboard operation
QuantLib copied to clipboard

Issue with first period cashflow in FixedRateBond

Open alexsbromberg opened this issue 6 years ago • 6 comments

I'm having an issue where the FixedRateBond class is unable to generate the correct cashflows under certain circumstances. I'm using the python bindings for quantlib 1.11. I'm also running this in docker using the the latest build, lballabio/quantlib-python:1.11-xenial.

Here's my code:

        calendar = ql.UnitedStates(ql.UnitedStates.GovernmentBond)
        holidays_to_remove = [ql.Date(10,11,2017)]
        [calendar.removeHoliday(holiday) for holiday in holidays_to_remove]


        fixedRateBondSchedule = ql.Schedule(
            ql.Date(30, 9, 2017),
            ql.Date(30, 9, 2024),
            ql.Period(ql.Semiannual),
            calendar,
            ql.Unadjusted,
            ql.Unadjusted,
            ql.DateGeneration.Backward,
            True,
            ql.Date(31, 3, 2018),
            ql.Date(31, 3, 2024)
        )

        bond = ql.FixedRateBond(
            0,
            100,
            fixedRateBondSchedule,
            [.02125],
            ql.ActualActual(ql.ActualActual.Bond),
            ql.Unadjusted,
            100
        )
        cfs = [ (str(c.date()), str(c.amount())) for c in bond.cashflows() ]
        for x in cfs:
            print(x)

This generates the following cash flows:

('March 31st, 2018',   '1.05669398907')
('September 30th, 2018', '1.0625')
('March 31st, 2019', '1.0625')
('September 30th, 2019', '1.0625')
('March 31st, 2020', '1.0625')
('September 30th, 2020', '1.0625')
('March 31st, 2021', '1.0625')
('September 30th, 2021', '1.0625')
('March 31st, 2022', '1.0625')
('September 30th, 2022', '1.0625')
('March 31st, 2023', '1.0625')
('September 30th, 2023', '1.0625')
('March 31st, 2024', '1.0625')
('September 30th, 2024', '1.0625')
('September 30th, 2024', '100.0')

You can see that the first interest amount is slightly less than all the other interest amounts, even though the first interest period is a normal length. I can fix this by changing the date generation method from ql.DateGeneration.Backward to ql.DateGeneration.Forward. When i do that, i get the expected 1.0625 amount for the first period. Is this a bug in the underlying quantlib code, or am I doing something wrong here?

alexsbromberg avatar Feb 01 '18 14:02 alexsbromberg

I'll have to dig into it. It looks like a bug, anyway. Thanks for the report.

lballabio avatar Feb 01 '18 16:02 lballabio

I looked into the discrepancy and found that the refPeriodstart date used in FixedRateLeg::operator Leg() to calculate the the first cashflow is different between Forward and Backward (ql/cashflows/fixedratecoupon.cpp L 189,+13). Forward uses the correct Date(30, Sep, 2017) while Backward finds Date(29, Sep, 2017).

In particular, Backward generation finds that Date(30, Sep, 2017) and Date(31, Mar, 2018) is non-regular (ql/time/schedule.cpp L 262,+6) in alexsbromberg's example.

I believe the correct fix is to check whether dates_.front() advanced one more negative tenor is equal to effectiveDate and insert into isRegular_ true if equal and false otherwise (Currently, it unconditionally inserts false into isRegular_).

la-wu avatar Feb 10 '18 23:02 la-wu

@Lumaere thanks for looking into this too. I spent some time on this yesterday and came to the same conclusion as you with one caveat. In this scenario, it's probably more appropriate for me to be using the NullCalendar instead of the UnitedStates calendar. If i don't, then the calculated reference period for irregular periods for end of month bonds will still be wrong, even with your fix, since the end of month function chooses the last business day in the month; I want the last actual day in the month.

alexsbromberg avatar Feb 11 '18 13:02 alexsbromberg

Hello, I am new to the community. Want to check the status on this issue and provide another example. Running the following code with quantlib-python 1.16.1

business_convention = ql.Following
date_generation = ql.DateGeneration.Backward
month_end = False
cpn_freq = 2
tenor = ql.Period(cpn_freq)
day_counter = ql.ActualActual(ql.ActualActual.Bond)
settlement_days = 1
face_value = 100
compounding = ql.Compounded

issue_date = ql.Date(3, 9, 2019)
maturity_date = ql.Date(31, 8, 2024)
coupon = 1.25 / 100

schedule = ql.Schedule(issue_date, maturity_date, tenor, calendar, business_convention,
        ql.Unadjusted, date_generation, month_end)
bond = ql.FixedRateBond(settlement_days, face_value, schedule, [coupon], day_counter, business_convention)

cfs = [ (str(c.date()), str(c.amount())) for c in bond.cashflows() ]
for x in cfs:
    print(x)

Gives result as

('February 29th, 2020', '0.6080163043478359') ('August 31st, 2020', '0.6250000000000089') ('February 28th, 2021', '0.6250000000000089') ('August 31st, 2021', '0.6250000000000089') ('February 28th, 2022', '0.6250000000000089') ('August 31st, 2022', '0.6250000000000089') ('February 28th, 2023', '0.6250000000000089') ('August 31st, 2023', '0.6250000000000089') ('February 29th, 2024', '0.6250000000000089') ('August 31st, 2024', '0.6250000000000089') ('August 31st, 2024', '100.0')

I am expecting the first row is Feb 29 2020 with 0.625

Thanks!

yyTheTuna avatar Oct 24 '19 13:10 yyTheTuna

No, I think this last case is correct. Your first coupon starts on September 3rd 2019 (not August 31st) and ends on February 29th 2020, so it's less that a full 6-months period.

lballabio avatar Dec 24 '19 10:12 lballabio

Ciao all, in this specific case the reference date ref is calculated as one day before the start date, so the difference amount is due to this one-day discrepancy. The piece of code is in fixedratecoupon.cpp, lines 194-200:

Date ref = schedule_.hasTenor() &&
    schedule_.hasIsRegular() && !schedule_.isRegular(1) ?
    schedule_.calendar().advance(end,
                                 -schedule_.tenor(),
                                 schedule_.businessDayConvention(),
                                 schedule_.endOfMonth())
    : start;

For this specific case the ternary conditional expression is true and therefore the calendar().advance is shifting the original start date one day before. So the reason for the discrepancy should be investigated in the Schedule constructor. For some reason the coupon dates calculated with Backward rule are non regular. In schedule.cpp L214 we can see that when the Schedule constructor is invoked with the firstDate argument then the exitDate = firstDate_. When the firstDate is not evaluated instead, exitDate = effectiveDate

exitDate = effectiveDate;
if (firstDate_ != Date())
    exitDate = firstDate_;

And then at L241 at the end of the for loop calculating the coupon dates it is checked if the effective date period is "regular". It is assumed that the effective date is already stored in dates_ vector. But in this case in the dates_ vector we don't have the effective date, because in the for loop it has not been calculated. In fact the exitDate condition was not the effectiveDate, it was the firstDate. So the front position of the dates_ vector holds the firstDate which is always different from the effectiveDate. Therefore the expression below is always considering the effective date period as non regular.

if (calendar_.adjust(dates_.front(),convention)!=
    calendar_.adjust(effectiveDate,convention)) {
    dates_.insert(dates_.begin(), effectiveDate);
    isRegular_.insert(isRegular_.begin(), false);
}
break;

That's it. Any suggestion from QuantLib experienced developers is appreciated. I attach the source code used for the test. testFixedRateBond.cpp.txt

jakeheke75 avatar Dec 25 '22 22:12 jakeheke75