/**
* Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.basics.date;
import static com.opengamma.strata.basics.date.LocalDateUtils.daysBetween;
import static com.opengamma.strata.basics.date.LocalDateUtils.doy;
import static java.lang.Math.toIntExact;
import java.time.LocalDate;
import com.opengamma.strata.basics.schedule.Frequency;
/**
* Standard day count convention implementations.
* <p>
* See {@link DayCounts} for the description of each.
*/
enum StandardDayCounts implements DayCount {
// always one
ONE_ONE("1/1") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
return 1;
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
return 1;
}
},
// actual days / actual days in year
ACT_ACT_ISDA("Act/Act ISDA") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
int y1 = firstDate.getYear();
int y2 = secondDate.getYear();
double firstYearLength = firstDate.lengthOfYear();
if (y1 == y2) {
double actualDays = doy(secondDate) - doy(firstDate);
return actualDays / firstYearLength;
}
double firstRemainderOfYear = firstYearLength - doy(firstDate) + 1;
double secondRemainderOfYear = doy(secondDate) - 1;
double secondYearLength = secondDate.lengthOfYear();
return firstRemainderOfYear / firstYearLength +
secondRemainderOfYear / secondYearLength +
(y2 - y1 - 1);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// complex ICMA calculation
ACT_ACT_ICMA("Act/Act ICMA") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
// avoid using ScheduleInfo in this case
if (firstDate.equals(secondDate)) {
return 0d;
}
// calculation is based on the schedule period, firstDate assumed to be the start of the period
LocalDate scheduleStartDate = scheduleInfo.getStartDate();
LocalDate scheduleEndDate = scheduleInfo.getEndDate();
LocalDate nextCouponDate = scheduleInfo.getPeriodEndDate(firstDate);
Frequency freq = scheduleInfo.getFrequency();
boolean eom = scheduleInfo.isEndOfMonthConvention();
// final period, also handling single period schedules
if (nextCouponDate.equals(scheduleEndDate)) {
return finalPeriod(firstDate, secondDate, freq, eom);
}
// initial period
if (firstDate.equals(scheduleStartDate)) {
return initPeriod(firstDate, secondDate, nextCouponDate, freq, eom);
}
double actualDays = daysBetween(firstDate, secondDate);
double periodDays = daysBetween(firstDate, nextCouponDate);
return actualDays / (freq.eventsPerYear() * periodDays);
}
// calculate nominal periods backwards from couponDate
private double initPeriod(LocalDate startDate, LocalDate endDate, LocalDate couponDate, Frequency freq, boolean eom) {
LocalDate currentNominal = couponDate;
LocalDate prevNominal = eom(couponDate, currentNominal.minus(freq), eom);
double result = 0;
while (prevNominal.isAfter(startDate)) {
result += calc(prevNominal, currentNominal, startDate, endDate, freq);
currentNominal = prevNominal;
prevNominal = eom(couponDate, currentNominal.minus(freq), eom);
}
return result + calc(prevNominal, currentNominal, startDate, endDate, freq);
}
// calculate nominal periods forwards from couponDate
private double finalPeriod(LocalDate couponDate, LocalDate endDate, Frequency freq, boolean eom) {
LocalDate curNominal = couponDate;
LocalDate nextNominal = eom(couponDate, curNominal.plus(freq), eom);
double result = 0;
while (nextNominal.isBefore(endDate)) {
result += calc(curNominal, nextNominal, curNominal, endDate, freq);
curNominal = nextNominal;
nextNominal = eom(couponDate, curNominal.plus(freq), eom);
}
return result + calc(curNominal, nextNominal, curNominal, endDate, freq);
}
// apply eom convention
private LocalDate eom(LocalDate base, LocalDate calc, boolean eom) {
return (eom && base.getDayOfMonth() == base.lengthOfMonth() ? calc.withDayOfMonth(calc.lengthOfMonth()) : calc);
}
// calculate the result
private double calc(LocalDate prevNominal, LocalDate curNominal, LocalDate start, LocalDate end, Frequency freq) {
if (end.isAfter(prevNominal)) {
long curNominalEpochDay = curNominal.toEpochDay();
long prevNominalEpochDay = prevNominal.toEpochDay();
long startEpochDay = start.toEpochDay();
long endEpochDay = end.toEpochDay();
double periodDays = curNominalEpochDay - prevNominalEpochDay;
double actualDays = Math.min(endEpochDay, curNominalEpochDay) - Math.max(startEpochDay, prevNominalEpochDay);
return actualDays / (freq.eventsPerYear() * periodDays);
}
return 0;
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// AFB year-based calculation
ACT_ACT_AFB("Act/Act AFB") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
// tests show that there is no need to perform an initial check of period less than
// or equal one year when using the OpenGamma interpretation of end-of-February rule
// calculate the number of whole years back from the end
// OpenGamma interpretation: reject ISDA end-of-Feb if 28th Feb, apply simple subtraction from secondDate
LocalDate end = secondDate;
LocalDate start = secondDate.minusYears(1);
int years = 0;
while (!start.isBefore(firstDate)) {
years++;
end = start;
start = secondDate.minusYears(years + 1);
}
// calculate the remaining fraction, including start, excluding end
long actualDays = daysBetween(firstDate, end);
LocalDate nextLeap = DateAdjusters.nextOrSameLeapDay(firstDate);
return years + (actualDays / (nextLeap.isBefore(end) ? 366d : 365d));
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// actual days / actual days in year from start date
ACT_ACT_YEAR("Act/Act Year") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
LocalDate startDate = firstDate;
int yearsAdded = 0;
while (secondDate.compareTo(startDate.plusYears(1)) > 0) {
startDate = firstDate.plusYears(++yearsAdded);
}
double actualDays = daysBetween(startDate, secondDate);
double actualDaysInYear = daysBetween(startDate, startDate.plusYears(1));
return yearsAdded + (actualDays / actualDaysInYear);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// actual days / 365 or 366
ACT_365_ACTUAL("Act/365 Actual") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
long actualDays = daysBetween(firstDate, secondDate);
LocalDate nextLeap = DateAdjusters.nextLeapDay(firstDate);
return actualDays / (nextLeap.isAfter(secondDate) ? 365d : 366d);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// actual days / 365 or 366
ACT_365L("Act/365L") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
long actualDays = daysBetween(firstDate, secondDate);
// avoid using ScheduleInfo in this case
if (firstDate.equals(secondDate)) {
return 0d;
}
// calculation is based on the end of the schedule period (next coupon date) and annual/non-annual frequency
LocalDate nextCouponDate = scheduleInfo.getPeriodEndDate(firstDate);
if (scheduleInfo.getFrequency().isAnnual()) {
LocalDate nextLeap = DateAdjusters.nextLeapDay(firstDate);
return actualDays / (nextLeap.isAfter(nextCouponDate) ? 365d : 366d);
} else {
return actualDays / (nextCouponDate.isLeapYear() ? 366d : 365d);
}
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// simple actual days / 360
ACT_360("Act/360") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
return daysBetween(firstDate, secondDate) / 360d;
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// simple actual days / 364
ACT_364("Act/364") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
return daysBetween(firstDate, secondDate) / 364d;
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// simple actual days / 365
ACT_365F("Act/365F") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
return daysBetween(firstDate, secondDate) / 365d;
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// simple actual days / 365.25
ACT_365_25("Act/365.25") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
return daysBetween(firstDate, secondDate) / 365.25d;
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
return toIntExact(actualDays);
}
},
// no leaps / 365
NL_365("NL/365") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
long actualDays = daysBetween(firstDate, secondDate);
int numberOfLeapDays = 0;
LocalDate temp = DateAdjusters.nextLeapDay(firstDate);
while (temp.isAfter(secondDate) == false) {
numberOfLeapDays++;
temp = DateAdjusters.nextLeapDay(temp);
}
return (actualDays - numberOfLeapDays) / 365d;
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
long actualDays = daysBetween(firstDate, secondDate);
int numberOfLeapDays = 0;
LocalDate temp = DateAdjusters.nextLeapDay(firstDate);
while (temp.isAfter(secondDate) == false) {
numberOfLeapDays++;
temp = DateAdjusters.nextLeapDay(temp);
}
return toIntExact(actualDays) - numberOfLeapDays;
}
},
// ISDA thirty day months / 360
THIRTY_360_ISDA("30/360 ISDA") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31 && d1 == 30) {
d2 = 30;
}
return thirty360(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31 && d1 == 30) {
d2 = 30;
}
return thirty360Days(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
},
// US thirty day months / 360 with dynamic EOM rule
THIRTY_U_360("30U/360") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
if (scheduleInfo.isEndOfMonthConvention()) {
return THIRTY_U_360_EOM.calculateYearFraction(firstDate, secondDate, scheduleInfo);
} else {
return THIRTY_360_ISDA.calculateYearFraction(firstDate, secondDate, scheduleInfo);
}
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
return THIRTY_360_ISDA.days(firstDate, secondDate);
}
},
// US thirty day months / 360 with fixed EOM rule
THIRTY_U_360_EOM("30U/360 EOM") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (lastDayOfFebruary(firstDate)) {
if (lastDayOfFebruary(secondDate)) {
d2 = 30;
}
d1 = 30;
}
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31 && d1 == 30) {
d2 = 30;
}
return thirty360(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (lastDayOfFebruary(firstDate)) {
if (lastDayOfFebruary(secondDate)) {
d2 = 30;
}
d1 = 30;
}
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31 && d1 == 30) {
d2 = 30;
}
return thirty360Days(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
},
THIRTY_360_PSA("30/360 PSA") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31 || lastDayOfFebruary(firstDate)) {
d1 = 30;
}
if (d2 == 31 && d1 == 30) {
d2 = 30;
}
return thirty360(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31 || lastDayOfFebruary(firstDate)) {
d1 = 30;
}
if (d2 == 31 && d1 == 30) {
d2 = 30;
}
return thirty360Days(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
},
// ISDA EU thirty day months / 360
THIRTY_E_360_ISDA("30E/360 ISDA") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31 || lastDayOfFebruary(firstDate)) {
d1 = 30;
}
if (d2 == 31 || (lastDayOfFebruary(secondDate) && !secondDate.equals(scheduleInfo.getEndDate()))) {
d2 = 30;
}
return thirty360(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31 || lastDayOfFebruary(firstDate)) {
d1 = 30;
}
if (d2 == 31 || (lastDayOfFebruary(secondDate))) {
d2 = 30;
}
return thirty360Days(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
},
// E thirty day months / 360
THIRTY_E_360("30E/360") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31) {
d2 = 30;
}
return thirty360(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31) {
d2 = 30;
}
return thirty360Days(
firstDate.getYear(), firstDate.getMonthValue(), d1,
secondDate.getYear(), secondDate.getMonthValue(), d2);
}
},
// E+ thirty day months / 360
THIRTY_EPLUS_360("30E+/360") {
@Override
public double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
int m1 = firstDate.getMonthValue();
int m2 = secondDate.getMonthValue();
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31) {
d2 = 1;
m2 = m2 + 1; // nature of calculation means no need to adjust Dec to Jan
}
return thirty360(
firstDate.getYear(), m1, d1,
secondDate.getYear(), m2, d2);
}
@Override
public int calculateDays(LocalDate firstDate, LocalDate secondDate) {
int d1 = firstDate.getDayOfMonth();
int d2 = secondDate.getDayOfMonth();
int m1 = firstDate.getMonthValue();
int m2 = secondDate.getMonthValue();
if (d1 == 31) {
d1 = 30;
}
if (d2 == 31) {
d2 = 1;
m2 = m2 + 1; // nature of calculation means no need to adjust Dec to Jan
}
return thirty360Days(
firstDate.getYear(), m1, d1,
secondDate.getYear(), m2, d2);
}
};
// name
private final String name;
// create
private StandardDayCounts(String name) {
this.name = name;
}
// calculate using the standard 30/360 function - 360(y2 - y1) + 30(m2 - m1) + (d2 - d1)) / 360
private static double thirty360(int y1, int m1, int d1, int y2, int m2, int d2) {
return (360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1)) / 360d;
}
//calculate using the 30/360 function as above but does not divide by 360, as the number of days is needed, not the fraction.
private static int thirty360Days(int y1, int m1, int d1, int y2, int m2, int d2) {
return 360 * (y2 - y1) + 30 * (m2 - m1) + (d2 - d1);
}
// determine if the date is the last day of february
private static boolean lastDayOfFebruary(LocalDate date) {
return date.getMonthValue() == 2 && date.getDayOfMonth() == date.lengthOfMonth();
}
@Override
public double yearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
if (secondDate.isBefore(firstDate)) {
throw new IllegalArgumentException("Dates must be in time-line order");
}
return calculateYearFraction(firstDate, secondDate, scheduleInfo);
}
@Override
public int days(LocalDate firstDate, LocalDate secondDate) {
if (secondDate.isBefore(firstDate)) {
throw new IllegalArgumentException("Dates must be in time-line order");
}
return calculateDays(firstDate, secondDate);
}
@Override
public double relativeYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo) {
// override to avoid duplicate null checks
if (secondDate.isBefore(firstDate)) {
return -calculateYearFraction(secondDate, firstDate, scheduleInfo);
}
return calculateYearFraction(firstDate, secondDate, scheduleInfo);
}
// calculate the year fraction, using validated inputs
abstract double calculateYearFraction(LocalDate firstDate, LocalDate secondDate, ScheduleInfo scheduleInfo);
//calculate the number of days between the specified dates, using validated inputs
abstract int calculateDays(LocalDate firstDate, LocalDate secondDate);
//-------------------------------------------------------------------------
@Override
public String getName() {
return name;
}
@Override
public String toString() {
return name;
}
}