Skip to content

Commit

Permalink
Merge pull request #1061.
Browse files Browse the repository at this point in the history
Fix actual/actual (ISMA) day counter calculation for long/short final periods
  • Loading branch information
lballabio committed Mar 9, 2021
2 parents 553bb1c + bae0cd3 commit 3f6b727
Show file tree
Hide file tree
Showing 3 changed files with 162 additions and 18 deletions.
63 changes: 45 additions & 18 deletions ql/time/daycounters/actualactual.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,26 +50,53 @@ namespace QuantLib {
const Schedule& schedule) {
// Process the schedule into an array of dates.
Date issueDate = schedule.date(0);
Date firstCoupon = schedule.date(1);
Date notionalCoupon =
schedule.calendar().advance(firstCoupon,
-schedule.tenor(),
schedule.businessDayConvention(),
schedule.endOfMonth());

std::vector<Date> newDates = schedule.dates();
newDates[0] = notionalCoupon;

//long first coupon
if (notionalCoupon > issueDate) {
Date priorNotionalCoupon =
schedule.calendar().advance(notionalCoupon,
-schedule.tenor(),
schedule.businessDayConvention(),
schedule.endOfMonth());
newDates.insert(newDates.begin(),
priorNotionalCoupon); //insert as the first element?

if (!schedule.hasIsRegular() || !schedule.isRegular(1))
{
Date firstCoupon = schedule.date(1);

Date notionalFirstCoupon =
schedule.calendar().advance(firstCoupon,
-schedule.tenor(),
schedule.businessDayConvention(),
schedule.endOfMonth());

newDates[0] = notionalFirstCoupon;

//long first coupon
if (notionalFirstCoupon > issueDate) {
Date priorNotionalCoupon =
schedule.calendar().advance(notionalFirstCoupon,
-schedule.tenor(),
schedule.businessDayConvention(),
schedule.endOfMonth());
newDates.insert(newDates.begin(),
priorNotionalCoupon); //insert as the first element?
}
}

if (!schedule.hasIsRegular() || !schedule.isRegular(schedule.size() - 1))
{
Date notionalLastCoupon =
schedule.calendar().advance(schedule.date(schedule.size() - 2),
schedule.tenor(),
schedule.businessDayConvention(),
schedule.endOfMonth());

newDates[schedule.size() - 1] = notionalLastCoupon;

if (notionalLastCoupon < schedule.endDate())
{
Date nextNotionalCoupon =
schedule.calendar().advance(notionalLastCoupon,
schedule.tenor(),
schedule.businessDayConvention(),
schedule.endOfMonth());
newDates.push_back(nextNotionalCoupon);
}
}

return newDates;
}

Expand Down
116 changes: 116 additions & 0 deletions test-suite/daycounters.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,121 @@ void DayCounterTest::testActualActual() {
}
}

void DayCounterTest::testActualActualIsma()
{
BOOST_TEST_MESSAGE("Testing actual/actual (ISMA) with odd last period...");

bool isEndOfMonth(false);
Frequency frequency(Semiannual);
Date interestAccrualDate(30, Jan, 1999);
Date maturityDate(30, Jun, 2000);
Date firstCouponDate(30, Jul, 1999);
Date penultimateCouponDate(30, Jan, 2000);
Date d1(30, Jan, 2000);
Date d2(30, Jun, 2000);

double expected(152. / (182. * 2));

Schedule schedule = MakeSchedule()
.from(interestAccrualDate)
.to(maturityDate)
.withFrequency(frequency)
.withFirstDate(firstCouponDate)
.withNextToLastDate(penultimateCouponDate)
.endOfMonth(isEndOfMonth);

DayCounter dayCounter = ActualActual(ActualActual::ISMA, schedule);

double calculated(dayCounter.yearFraction(d1, d2));

if (std::fabs(calculated - expected) > 1.0e-10) {
std::ostringstream period;
period << "period: " << d1 << " to " << d2 << "\n"
<< "firstCouponDate: " << firstCouponDate << "\n"
<< "penultimateCouponDate: " << penultimateCouponDate << "\n";
BOOST_ERROR(dayCounter.name() << ":\n"
<< period.str()
<< std::setprecision(10)
<< " calculated: " << calculated << "\n"
<< " expected: " << expected);
}

//////////////////////////////////

isEndOfMonth = true;
frequency = Quarterly;
interestAccrualDate = Date(31, May, 1999);
maturityDate = Date(30, Apr, 2000);
firstCouponDate = Date(31, Aug, 1999);
penultimateCouponDate = Date(30, Nov, 1999);
d1 = Date(30, Nov, 1999);
d2 = Date(30, Apr, 2000);

expected = 91.0 / (91.0 * 4) + 61.0 / (92.0 * 4);

schedule = MakeSchedule()
.from(interestAccrualDate)
.to(maturityDate)
.withFrequency(frequency)
.withFirstDate(firstCouponDate)
.withNextToLastDate(penultimateCouponDate)
.endOfMonth(isEndOfMonth);

dayCounter = ActualActual(ActualActual::ISMA, schedule);

calculated = dayCounter.yearFraction(d1, d2);

if (std::fabs(calculated - expected) > 1.0e-10) {
std::ostringstream period;
period << "period: " << d1 << " to " << d2 << "\n"
<< "firstCouponDate: " << firstCouponDate << "\n"
<< "penultimateCouponDate: " << penultimateCouponDate << "\n";
BOOST_ERROR(dayCounter.name() << ":\n"
<< period.str()
<< std::setprecision(10)
<< " calculated: " << calculated << "\n"
<< " expected: " << expected);
}


//////////////////////////////////

isEndOfMonth = false;
frequency = Quarterly;
interestAccrualDate = Date(31, May, 1999);
maturityDate = Date(30, Apr, 2000);
firstCouponDate = Date(31, Aug, 1999);
penultimateCouponDate = Date(30, Nov, 1999);
d1 = Date(30, Nov, 1999);
d2 = Date(30, Apr, 2000);

expected = 91.0 / (91.0 * 4) + 61.0 / (90.0 * 4);

schedule = MakeSchedule()
.from(interestAccrualDate)
.to(maturityDate)
.withFrequency(frequency)
.withFirstDate(firstCouponDate)
.withNextToLastDate(penultimateCouponDate)
.endOfMonth(isEndOfMonth);

dayCounter = ActualActual(ActualActual::ISMA, schedule);

calculated = dayCounter.yearFraction(d1, d2);

if (std::fabs(calculated - expected) > 1.0e-10) {
std::ostringstream period;
period << "period: " << d1 << " to " << d2 << "\n"
<< "firstCouponDate: " << firstCouponDate << "\n"
<< "penultimateCouponDate: " << penultimateCouponDate << "\n";
BOOST_ERROR(dayCounter.name() << ":\n"
<< period.str()
<< std::setprecision(10)
<< " calculated: " << calculated << "\n"
<< " expected: " << expected);
}
}

void DayCounterTest::testActualActualWithSemiannualSchedule() {

BOOST_TEST_MESSAGE("Testing actual/actual with schedule "
Expand Down Expand Up @@ -941,6 +1056,7 @@ void DayCounterTest::testIntraday() {
test_suite* DayCounterTest::suite() {
auto* suite = BOOST_TEST_SUITE("Day counter tests");
suite->add(QUANTLIB_TEST_CASE(&DayCounterTest::testActualActual));
suite->add(QUANTLIB_TEST_CASE(&DayCounterTest::testActualActualIsma));
suite->add(QUANTLIB_TEST_CASE(
&DayCounterTest::testActualActualWithSemiannualSchedule));
suite->add(QUANTLIB_TEST_CASE(
Expand Down
1 change: 1 addition & 0 deletions test-suite/daycounters.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
class DayCounterTest {
public:
static void testActualActual();
static void testActualActualIsma();
static void testActualActualWithSchedule();
static void testActualActualWithAnnualSchedule();
static void testActualActualWithSemiannualSchedule();
Expand Down

0 comments on commit 3f6b727

Please sign in to comment.