QuantLib
QuantLib copied to clipboard
Issue with first period cashflow in FixedRateBond
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?
I'll have to dig into it. It looks like a bug, anyway. Thanks for the report.
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_
).
@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.
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!
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.
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