/** * Copyright (C) 2009 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.analytics.financial.schedule; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.TreeSet; import org.threeten.bp.LocalDate; import org.threeten.bp.Period; import org.threeten.bp.ZonedDateTime; import org.threeten.bp.temporal.TemporalAdjusters; import com.opengamma.analytics.financial.instrument.index.GeneratorDeposit; import com.opengamma.analytics.financial.instrument.index.IborIndex; import com.opengamma.analytics.util.time.TenorUtils; import com.opengamma.financial.convention.StubType; import com.opengamma.financial.convention.businessday.BusinessDayConvention; import com.opengamma.financial.convention.businessday.FollowingBusinessDayConvention; import com.opengamma.financial.convention.businessday.PrecedingBusinessDayConvention; import com.opengamma.financial.convention.calendar.Calendar; import com.opengamma.financial.convention.daycount.DayCount; import com.opengamma.financial.convention.frequency.Frequency; import com.opengamma.financial.convention.frequency.PeriodFrequency; import com.opengamma.financial.convention.frequency.SimpleFrequency; import com.opengamma.financial.convention.rolldate.EndOfMonthRollDateAdjuster; import com.opengamma.financial.convention.rolldate.RollDateAdjuster; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.time.DateUtils; import com.opengamma.util.time.Tenor; /** * Utility to calculate schedules. */ public final class ScheduleCalculator { /** * A singleton empty array. */ private static final ZonedDateTime[] EMPTY_ARRAY = new ZonedDateTime[0]; /** * Restricted constructor. */ private ScheduleCalculator() { } // Already reviewed /** * Return a good business date computed from a given date and shifted by a certain number of business days. * If the number of shift days is 0, the return date is the next business day. * If the number of shift days is non-zero (positive or negative), a 0 shift is first applied and then a one business * day shift is applied as many time as the absolute value of the shift. If the shift is positive, the one business * day is to the future, if the shift is negative, the one business day is to the past. * @param date The initial date. * @param shiftDays The number of days of the adjustment. Can be negative or positive. * @param calendar The calendar representing the good business days. * @return The adjusted date. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime date, final int shiftDays, final Calendar calendar) { ArgumentChecker.notNull(date, "date"); ArgumentChecker.notNull(calendar, "calendar"); ZonedDateTime result = date; while (!calendar.isWorkingDay(result.toLocalDate())) { result = result.plusDays(1); } if (shiftDays > 0) { for (int loopday = 0; loopday < shiftDays; loopday++) { result = result.plusDays(1); while (!calendar.isWorkingDay(result.toLocalDate())) { result = result.plusDays(1); } } } else { for (int loopday = 0; loopday < -shiftDays; loopday++) { result = result.minusDays(1); while (!calendar.isWorkingDay(result.toLocalDate())) { result = result.minusDays(1); } } } return result; } /** * Return a good business dates computed from given array of date and shifted by a certain number of business days (one return date for each input date). * If the number of shift days is 0, the return date is the next business day. * If the number of shift days is non-zero (positive or negative), a 0 shift is first applied and then a one business day shift is applied as many time as the absolute value of the shift. * If the shift is positive, the one business day is to the future., if the shift is negative, the one business day is to the past. * @param dates The initial dates. * @param shiftDays The number of days of the adjustment. Can be negative or positive. * @param calendar The calendar representing the good business days. * @return The adjusted dates. */ public static ZonedDateTime[] getAdjustedDate(final ZonedDateTime[] dates, final int shiftDays, final Calendar calendar) { final int nbDates = dates.length; final ZonedDateTime[] result = new ZonedDateTime[nbDates]; for (int loopdate = 0; loopdate < nbDates; loopdate++) { result[loopdate] = getAdjustedDate(dates[loopdate], shiftDays, calendar); } return result; } /** * Return a good business date computed from a given date and shifted by a certain number of business days. The number of business days is given by the getDays part of a peeriod. * If the number of shift days is 0, the return date is the next business day. * If the number of shift days is non-zero (positive or negative), a 0 shift is first applied and then a one business day shift is applied as many time as the absolute value of the shift. * If the shift is positive, the one business day is to the future., if the shift is negative, the one business day is to the past. * @param date The initial date. * @param shiftDays The number of days of the adjustment as a period. * @param calendar The calendar representing the good business days. * @return The adjusted dates. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime date, final Period shiftDays, final Calendar calendar) { ArgumentChecker.notNull(shiftDays, "shift days"); return getAdjustedDate(date, shiftDays.getDays(), calendar); } /** * Return a good business date computed from a given date and shifted by a certain number of business days. * This version uses LocalDate. * If the number of shift days is 0, the return date is the next business day. * If the number of shift days is non-zero (positive or negative), a 0 shift is first applied and then a one business day shift is applied as many time as the absolute value of the shift. * If the shift is positive, the one business day is to the future., if the shift is negative, the one business day is to the past. * @param date The initial date. * @param shiftDays The number of days of the adjustment. Can be negative or positive. * @param calendar The calendar representing the good business days. * @return The adjusted dates. */ public static LocalDate getAdjustedDate(final LocalDate date, final int shiftDays, final Calendar calendar) { ArgumentChecker.notNull(date, "date"); ArgumentChecker.notNull(calendar, "calendar"); LocalDate result = date; while (!calendar.isWorkingDay(result)) { result = result.plusDays(1); } if (shiftDays > 0) { for (int loopday = 0; loopday < shiftDays; loopday++) { result = result.plusDays(1); while (!calendar.isWorkingDay(result)) { result = result.plusDays(1); } } } else { for (int loopday = 0; loopday < -shiftDays; loopday++) { result = result.minusDays(1); while (!calendar.isWorkingDay(result)) { result = result.minusDays(1); } } } return result; } /** * Compute the end date of a period from the start date, the tenor and the conventions without end-of-month convention. * @param startDate The period start date. * @param tenor The period tenor. * @param convention The business day convention. * @param calendar The calendar. * @return The end date. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime startDate, final Period tenor, final BusinessDayConvention convention, final Calendar calendar) { ArgumentChecker.notNull(startDate, "start date"); ArgumentChecker.notNull(convention, "convention"); ArgumentChecker.notNull(calendar, "calendar"); ArgumentChecker.notNull(tenor, "tenor"); final ZonedDateTime endDate = startDate.plus(tenor); // Unadjusted date. return convention.adjustDate(calendar, endDate); // Adjusted by Business day convention } /** * Compute the end date of a period from the start date, the tenor and the conventions. * @param startDate The period start date. * @param tenor The period tenor. * @param convention The business day convention. * @param calendar The calendar. * @param endOfMonthRule True if end-of-month rule applies, false if it does not. * The rule applies when the start date is the last business day of the month and the period is a number of months or years, not days or weeks. * When the rule applies, the end date is the last business day of the month. * @return The end date. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime startDate, final Period tenor, final BusinessDayConvention convention, final Calendar calendar, final boolean endOfMonthRule) { ArgumentChecker.notNull(startDate, "Start date"); ArgumentChecker.notNull(convention, "Convention"); ArgumentChecker.notNull(calendar, "Calendar"); ArgumentChecker.notNull(tenor, "Tenor"); final ZonedDateTime endDate = startDate.plus(tenor); // Unadjusted date. // Adjusted to month-end: when start date is last business day of the month, the end date is the last business day of the month. final boolean isStartDateEOM = (startDate.getMonth() != getAdjustedDate(startDate, 1, calendar).getMonth()); if ((tenor.getDays() == 0) & (endOfMonthRule) & (isStartDateEOM)) { final BusinessDayConvention preceding = new PrecedingBusinessDayConvention(); return preceding.adjustDate(calendar, endDate.with(TemporalAdjusters.lastDayOfMonth())); } return convention.adjustDate(calendar, endDate); // Adjusted by Business day convention } /** * Compute the end date of a period from the start date, period, conventions and roll date adjuster. If the roll date * adjuster is end of month, then only apply when the start date is last business day of the month and the period is a * number of months or years, not days or weeks. * * @param startDate the start date * @param period the period between the start and end date. * @param convention the business day convention used to adjust the end date. * @param calendar the calendar used to adjust the end date. * @param rollDateAdjuster the roll date adjuster used to adjust the end date, before the conventions are applied. * @return The end date. */ public static ZonedDateTime getAdjustedDate( final ZonedDateTime startDate, final Period period, final BusinessDayConvention convention, final Calendar calendar, final RollDateAdjuster rollDateAdjuster) { ArgumentChecker.notNull(startDate, "Start date"); ArgumentChecker.notNull(convention, "Convention"); ArgumentChecker.notNull(calendar, "Calendar"); ArgumentChecker.notNull(period, "Tenor"); ZonedDateTime endDate = startDate.plus(period); // Unadjusted date. // Adjusted to month-end: when start date is last business day of the month, the end date is the last business day of the month. if (rollDateAdjuster instanceof EndOfMonthRollDateAdjuster) { final boolean isStartDateEOM = (startDate.getMonth() != getAdjustedDate(startDate, 1, calendar).getMonth()); if ((period.getDays() == 0) && isStartDateEOM) { final BusinessDayConvention preceding = new PrecedingBusinessDayConvention(); return preceding.adjustDate(calendar, endDate.with(TemporalAdjusters.lastDayOfMonth())); } } else if (rollDateAdjuster != null) { /* * If we are rolling forward with a positive period and we have a day of month adjuster, we don't want to roll * backwards. */ final ZonedDateTime rolledEndDate = endDate.with(rollDateAdjuster); if (!period.isNegative() && rolledEndDate.isAfter(endDate)) { endDate = rolledEndDate; } } return convention.adjustDate(calendar, endDate); // Adjusted by Business day convention } /** * Compute the end date of a period from the start date, the tenor and the conventions. * @param startDate The period start date. * @param tenor The tenor. * @param convention The business day convention. * @param calendar The calendar. * @param endOfMonthRule True if end-of-month rule applies, false if it does not. * The rule applies when the start date is the last business day of the month and the period is a number of months or years, not days or business days (ON, TN). * When the rule applies, the end date is the last business day of the month. * @return The end date. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime startDate, final Tenor tenor, final BusinessDayConvention convention, final Calendar calendar, final boolean endOfMonthRule) { ArgumentChecker.notNull(startDate, "Start date"); ArgumentChecker.notNull(convention, "Convention"); ArgumentChecker.notNull(calendar, "Calendar"); ArgumentChecker.notNull(tenor, "Tenor"); final ZonedDateTime endDate = TenorUtils.adjustDateByTenor(startDate, tenor, calendar, 0); if (tenor.isBusinessDayTenor()) { // This handles tenor of the type ON, TN return endDate; } // Adjusted to month-end: when start date is last business day of the month, the end date is the last business day of the month. final boolean isStartDateEOM = (startDate.getMonth() != getAdjustedDate(startDate, 1, calendar).getMonth()); if ((tenor.getPeriod().getDays() == 0) & (endOfMonthRule) & (isStartDateEOM)) { final BusinessDayConvention preceding = new PrecedingBusinessDayConvention(); return preceding.adjustDate(calendar, endDate.with(TemporalAdjusters.lastDayOfMonth())); } return convention.adjustDate(calendar, endDate); // Adjusted by Business day convention } /** * Compute the end date of a period from the start date, the tenor and the conventions. * @param startDate The period start date. * @param tenor The period tenor. * @param generator The deposit generator with the required conventions. * @return The end date. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime startDate, final Period tenor, final GeneratorDeposit generator) { ArgumentChecker.notNull(generator, "Generator"); return getAdjustedDate(startDate, tenor, generator.getBusinessDayConvention(), generator.getCalendar(), generator.isEndOfMonth()); } /** * Compute the end date of a period from the start date, a period and a Ibor index. The index is used for the conventions. * @param startDate The period start date. * @param tenor The period tenor. * @param index The Ibor index. * @param calendar The holiday calendar. * @return The end date. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime startDate, final Period tenor, final IborIndex index, final Calendar calendar) { ArgumentChecker.notNull(index, "Index"); return getAdjustedDate(startDate, tenor, index.getBusinessDayConvention(), calendar, index.isEndOfMonth()); } /** * Compute the end date of a period from the start date and a Ibor index. The period between the start date and the end date is the index tenor. * @param startDate The period start date. * @param index The Ibor index. * @param calendar The holiday calendar. * @return The end date. */ public static ZonedDateTime getAdjustedDate(final ZonedDateTime startDate, final IborIndex index, final Calendar calendar) { ArgumentChecker.notNull(index, "Index"); return getAdjustedDate(startDate, index.getTenor(), index.getBusinessDayConvention(), calendar, index.isEndOfMonth()); } /** * Compute the end dates of periods from the start dates and a Ibor index. The period between the start date and the end date is the index tenor. * There is one return date for each input date. * @param startDates The period start dates. * @param index The Ibor index. * @param calendar The holiday calendar. * @return The end dates. */ public static ZonedDateTime[] getAdjustedDate(final ZonedDateTime[] startDates, final IborIndex index, final Calendar calendar) { final int nbDates = startDates.length; final ZonedDateTime[] result = new ZonedDateTime[nbDates]; for (int loopdate = 0; loopdate < nbDates; loopdate++) { result[loopdate] = getAdjustedDate(startDates[loopdate], index, calendar); } return result; } /** * Compute a schedule of unadjusted dates from a start date, an end date and the period between dates. * @param startDate The start date. * @param endDate The end date. * @param tenorPeriod The period between each date. * @param stub The stub type. * @return The date schedule (not including the start date). */ public static ZonedDateTime[] getUnadjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period tenorPeriod, final StubType stub) { ArgumentChecker.notNull(startDate, "Start date"); ArgumentChecker.notNull(endDate, "End date"); ArgumentChecker.notNull(tenorPeriod, "Period tenor"); ArgumentChecker.isTrue(startDate.isBefore(endDate), "Start date should be strictly before end date"); final boolean stubShort = stub.equals(StubType.SHORT_END) || stub.equals(StubType.SHORT_START) || stub.equals(StubType.NONE) || stub.equals(StubType.BOTH); final boolean fromEnd = isGenerateFromEnd(stub); // || stub.equals(StubType.NONE); // Implementation note: dates computed from the end. final List<ZonedDateTime> dates = new ArrayList<>(); int nbPeriod = 0; if (!fromEnd) { // Add the periods from the start date ZonedDateTime date = startDate.plus(tenorPeriod); while (date.isBefore(endDate)) { // date is strictly before endDate dates.add(date); nbPeriod++; date = startDate.plus(tenorPeriod.multipliedBy(nbPeriod + 1)); } if (!stubShort && !date.equals(endDate) && nbPeriod >= 1) { // For long stub the last date before end date, if any, is removed. dates.remove(nbPeriod - 1); } dates.add(endDate); return dates.toArray(EMPTY_ARRAY); } // From end - Subtract the periods from the end date ZonedDateTime date = endDate; while (date.isAfter(startDate)) { // date is strictly after startDate dates.add(date); nbPeriod++; date = endDate.minus(tenorPeriod.multipliedBy(nbPeriod)); } if (!stubShort && !date.equals(startDate) && nbPeriod > 1) { // For long stub the last date before end date, if any, is removed. dates.remove(nbPeriod - 1); } Collections.sort(dates); // To obtain the dates in chronological order. return dates.toArray(EMPTY_ARRAY); } /** * Compute a schedule of unadjusted dates from a start date, an end date and the period between dates. * @param startDate The start date. * @param endDate The end date. * @param tenorPeriod The period between each date. * @param stubShort In case the the periods do not fit exactly between start and end date, is the remaining interval shorter (true) or longer (false) than the requested period. * @param fromEnd The dates in the schedule can be computed from the end date (true) or from the start date (false). * @return The date schedule (not including the start date). */ public static ZonedDateTime[] getUnadjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period tenorPeriod, final boolean stubShort, final boolean fromEnd) { ArgumentChecker.notNull(startDate, "Start date"); ArgumentChecker.notNull(endDate, "End date"); ArgumentChecker.notNull(tenorPeriod, "Period tenor"); ArgumentChecker.isTrue(startDate.isBefore(endDate), "Start date {} should be strictly before end date {}", startDate, endDate); final List<ZonedDateTime> dates = new ArrayList<>(); int nbPeriod = 0; if (!fromEnd) { // Add the periods from the start date ZonedDateTime date = startDate.plus(tenorPeriod); while (date.isBefore(endDate)) { // date is strictly before endDate dates.add(date); nbPeriod++; date = startDate.plus(tenorPeriod.multipliedBy(nbPeriod + 1)); } if (!stubShort && !date.equals(endDate) && nbPeriod >= 1) { // For long stub the last date before end date, if any, is removed. dates.remove(nbPeriod - 1); } dates.add(endDate); return dates.toArray(EMPTY_ARRAY); } // From end - Subtract the periods from the end date ZonedDateTime date = endDate; while (date.isAfter(startDate)) { // date is strictly after startDate dates.add(date); nbPeriod++; date = endDate.minus(tenorPeriod.multipliedBy(nbPeriod)); } if (!stubShort && !date.equals(startDate) && nbPeriod > 1) { // For long stub the last date before end date, if any, is removed. dates.remove(nbPeriod - 1); } Collections.sort(dates); // To obtain the dates in chronological order. return dates.toArray(EMPTY_ARRAY); } /** * Adjust an array of date with a given convention and EOM flag. * @param dates The array of unadjusted dates. * @param convention The business day convention. * @param calendar The calendar. * @param eomApply The flag indicating if the EOM apply, i.e. if the flag is true, the adjusted date is the last business day of the unadjusted date. * @return The adjusted dates. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime[] dates, final BusinessDayConvention convention, final Calendar calendar, final boolean eomApply) { final ZonedDateTime[] result = new ZonedDateTime[dates.length]; if (eomApply) { final BusinessDayConvention precedingDBC = new PrecedingBusinessDayConvention(); //To ensure that the date stays in the current month. for (int loopdate = 0; loopdate < dates.length; loopdate++) { result[loopdate] = precedingDBC.adjustDate(calendar, dates[loopdate].with(TemporalAdjusters.lastDayOfMonth())); } return result; } for (int loopdate = 0; loopdate < dates.length; loopdate++) { result[loopdate] = convention.adjustDate(calendar, dates[loopdate]); } return result; } public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime[] dates, final BusinessDayConvention convention, final Calendar calendar, final boolean eomApply, final RollDateAdjuster adjuster) { final ZonedDateTime[] result = new ZonedDateTime[dates.length]; if (eomApply) { final BusinessDayConvention precedingDBC = new PrecedingBusinessDayConvention(); //To ensure that the date stays in the current month. for (int loopdate = 0; loopdate < dates.length; loopdate++) { result[loopdate] = precedingDBC.adjustDate(calendar, dates[loopdate].with(TemporalAdjusters.lastDayOfMonth())); } return result; } if (adjuster != null && !(adjuster instanceof EndOfMonthRollDateAdjuster)) { for (int loopdate = 0; loopdate < dates.length; loopdate++) { result[loopdate] = convention.adjustDate(calendar, dates[loopdate].with(adjuster)); } } else { for (int loopdate = 0; loopdate < dates.length; loopdate++) { result[loopdate] = convention.adjustDate(calendar, dates[loopdate]); } } // TODO workaround for PLAT-5695 final ZonedDateTime[] treeSetResult = new TreeSet<>(Arrays.asList(result)).toArray(new ZonedDateTime[] {}); return treeSetResult; } /** * Compute a schedule of adjusted dates from a start date, an end date and the period between dates. * @param startDate The start date. * @param endDate The end date. * @param schedulePeriod The period between each date in the schedule. * @param stubShort In case the the periods do not fit exactly between start and end date, is the remaining interval shorter (true) or longer (false) than the requested period. * @param fromEnd The dates in the schedule can be computed from the end date (true) or from the start date (false). * @param convention The business day convention. * @param calendar The calendar. * @param eomRule Flag indicating if the end-of-month rule should be applied. * @return The adjusted dates schedule. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period schedulePeriod, final boolean stubShort, final boolean fromEnd, final BusinessDayConvention convention, final Calendar calendar, final boolean eomRule) { final ZonedDateTime[] unadjustedDateSchedule = getUnadjustedDateSchedule(startDate, endDate, schedulePeriod, stubShort, fromEnd); final boolean eomApply = (eomRule && eomApplies(fromEnd, startDate, endDate, calendar)); return getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, eomApply); } public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period schedulePeriod, final boolean stubShort, final boolean fromEnd, final BusinessDayConvention convention, final Calendar calendar, final boolean eomRule, final RollDateAdjuster adjuster) { final ZonedDateTime[] unadjustedDateSchedule = getUnadjustedDateSchedule(startDate, endDate, schedulePeriod, stubShort, fromEnd); final boolean eomApply = (eomRule && eomApplies(fromEnd, startDate, endDate, calendar)); return getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, eomApply, adjuster); } /** * Compute a schedule of adjusted dates from a start date, an end date and the period between dates. * @param startDate The start date. * @param endDate The end date. * @param schedulePeriod The period between each date in the schedule. * @param stub The stub type. * @param convention The business day convention. * @param calendar The calendar. * @param eomRule Flag indicating if the end-of-month rule should be applied. * @return The adjusted dates schedule. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period schedulePeriod, final StubType stub, final BusinessDayConvention convention, final Calendar calendar, final boolean eomRule) { final ZonedDateTime[] unadjustedDateSchedule = getUnadjustedDateSchedule(startDate, endDate, schedulePeriod, stub); final boolean eomApply = (eomRule && eomApplies(isGenerateFromEnd(stub), startDate, endDate, calendar)); return getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, eomApply); } public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period schedulePeriod, final StubType stub, final BusinessDayConvention convention, final Calendar calendar, final boolean eomRule, final RollDateAdjuster adjuster) { final ZonedDateTime[] unadjustedDateSchedule = getUnadjustedDateSchedule(startDate, endDate, schedulePeriod, stub); final boolean eomApply = (eomRule && eomApplies(isGenerateFromEnd(stub), startDate, endDate, calendar)); return getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, eomApply, adjuster); } /** * Calculate a schedule of adjusted dates, but not the start date. * * @param startDate the start date * @param endDate the end date * @param schedulePeriod the periodic frequency * @param stub the stub type * @param convention the business day convention * @param calendar the holiday calendar * @param adjuster the roll convention * @return the schedule array, not including the start date */ public static ZonedDateTime[] getAdjustedDateSchedule( ZonedDateTime startDate, ZonedDateTime endDate, Period schedulePeriod, StubType stub, BusinessDayConvention convention, Calendar calendar, RollDateAdjuster adjuster) { ZonedDateTime[] unadjustedDateSchedule = getUnadjustedDateSchedule(startDate, endDate, schedulePeriod, stub); // convert roll adjuster into end-of-month flag and apply correctly if (adjuster instanceof EndOfMonthRollDateAdjuster) { // if calculating backwards, use end date to determine if rule applies, otherwise use start date boolean fromEnd = isGenerateFromEnd(stub); final boolean eomApply = eomApplies(fromEnd, startDate, endDate, calendar); if (fromEnd) { return getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, eomApply, adjuster); } else { ZonedDateTime[] adj = getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, eomApply, adjuster); // ensure date is not rolled beyond end date if (adj.length > 0 && adj[adj.length - 1].isAfter(endDate)) { adj[adj.length - 1] = convention.adjustDate(calendar, endDate); } return adj; } } return getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, false, adjuster); } public static ZonedDateTime[] getAdjustedDateSchedule( final ZonedDateTime[] startDates, final Period schedulePeriod, final BusinessDayConvention businessDayConvention, final Calendar calendar, final RollDateAdjuster adjuster) { final ZonedDateTime[] endDates = new ZonedDateTime[startDates.length]; for (int i = 0; i < startDates.length; i++) { endDates[i] = getAdjustedDate(startDates[i], schedulePeriod, businessDayConvention, calendar, adjuster); } return endDates; } /** * Compute a schedule of adjusted dates from a start date, an end date and the period between dates. * @param startDate The start date. * @param endDate The end date. * @param scheduleFrequency The frequency of dates in the schedule. * @param stubShort In case the the periods do not fit exactly between start and end date, is the remaining interval shorter (true) or longer (false) than the requested period. * @param fromEnd The dates in the schedule can be computed from the end date (true) or from the start date (false). * @param convention The business day convention. * @param calendar The calendar. * @param eomRule Flag indicating if the end-of-month rule should be applied. * @return The adjusted dates schedule. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Frequency scheduleFrequency, final boolean stubShort, final boolean fromEnd, final BusinessDayConvention convention, final Calendar calendar, final boolean eomRule) { ArgumentChecker.notNull(scheduleFrequency, "Schedule frequency"); final Period schedulePeriod = periodFromFrequency(scheduleFrequency); return getAdjustedDateSchedule(startDate, endDate, schedulePeriod, stubShort, fromEnd, convention, calendar, eomRule); } /** * Compute a schedule of adjusted dates from a start date, total tenor and the period between dates. * @param startDate The start date. * @param tenorTotal The total tenor. * @param tenorPeriod The period between each date. * @param stubShort In case the the periods do not fit exactly between start and end date, is the remaining interval shorter (true) or longer (false) than the requested period. * @param fromEnd The dates in the schedule can be computed from the end date (true) or from the start date (false). * @param convention The business day convention. * @param calendar The calendar. * @param eomRule Flag indicating if the end-of-month rule should be applied. * @return The adjusted dates schedule (not including the start date). */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final Period tenorTotal, final Period tenorPeriod, final boolean stubShort, final boolean fromEnd, final BusinessDayConvention convention, final Calendar calendar, final boolean eomRule) { ZonedDateTime endDate = startDate.plus(tenorTotal); final ZonedDateTime[] unadjustedDateSchedule = getUnadjustedDateSchedule(startDate, endDate, tenorPeriod, stubShort, fromEnd); final boolean eomApply = (eomRule && eomApplies(fromEnd, startDate, endDate, calendar) && (tenorTotal.getDays() == 0)); // Implementation note: the "tenorTotal.getDays() == 0" condition is required as the rule does not apply for period of less than 1 month (like 1 week). return getAdjustedDateSchedule(unadjustedDateSchedule, convention, calendar, eomApply); } /** * Compute a schedule of adjusted dates from a start date, total tenor and a Ibor index. * @param startDate The start date. * @param tenorTotal The total tenor. * @param stubShort In case the the periods do not fit exactly between start and end date, is the remaining interval shorter (true) or longer (false) than the requested period. * @param fromEnd The dates in the schedule can be computed from the end date (true) or from the start date (false). * @param index The related ibor index. The period tenor, business day convention, calendar and EOM rule of the index are used. * @param calendar The holiday calendar. * @return The adjusted dates schedule (not including the start date). */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final Period tenorTotal, final boolean stubShort, final boolean fromEnd, final IborIndex index, final Calendar calendar) { return getAdjustedDateSchedule(startDate, tenorTotal, index.getTenor(), stubShort, fromEnd, index.getBusinessDayConvention(), calendar, index.isEndOfMonth()); } /** * Convert a Frequency to a Period when possible. * @param frequency The frequency. * @return The converted period. */ private static Period periodFromFrequency(final Frequency frequency) { PeriodFrequency periodFrequency; if (frequency instanceof PeriodFrequency) { periodFrequency = (PeriodFrequency) frequency; } else if (frequency instanceof SimpleFrequency) { periodFrequency = ((SimpleFrequency) frequency).toPeriodFrequency(); } else { throw new IllegalArgumentException("For the moment can only deal with PeriodFrequency and SimpleFrequency"); } return periodFrequency.getPeriod(); } // TODO: review the methods below. // ------------------------------------------------------------------------- /** * Calculates the unadjusted date schedule. * * @param effectiveDate the effective date, not null * @param maturityDate the maturity date, not null * @param frequency how many times a year dates occur, not null * @return the schedule, not null */ public static ZonedDateTime[] getUnadjustedDateSchedule(final ZonedDateTime effectiveDate, final ZonedDateTime maturityDate, final Frequency frequency) { ArgumentChecker.notNull(effectiveDate, "effective date"); ArgumentChecker.notNull(maturityDate, "maturity date"); ArgumentChecker.notNull(frequency, "frequency"); if (effectiveDate.isAfter(maturityDate)) { throw new IllegalArgumentException("Effective date was after maturity"); } return getUnadjustedDateSchedule(effectiveDate, effectiveDate, maturityDate, frequency); } /** * Calculates the unadjusted date schedule. * * @param effectiveDate the effective date, not null * @param accrualDate the accrual date, not null * @param maturityDate the maturity date, not null * @param frequency how many times a year dates occur, not null * @return the schedule, not null */ public static ZonedDateTime[] getUnadjustedDateSchedule(final ZonedDateTime effectiveDate, final ZonedDateTime accrualDate, final ZonedDateTime maturityDate, final Frequency frequency) { ArgumentChecker.notNull(effectiveDate, "effective date"); ArgumentChecker.notNull(accrualDate, "accrual date"); ArgumentChecker.notNull(maturityDate, "maturity date"); ArgumentChecker.notNull(frequency, "frequency"); if (effectiveDate.isAfter(maturityDate)) { throw new IllegalArgumentException("Effective date was after maturity"); } if (accrualDate.isAfter(maturityDate)) { throw new IllegalArgumentException("Accrual date was after maturity"); } // TODO what if there's no valid date between accrual date and maturity date? PeriodFrequency periodFrequency; if (frequency instanceof PeriodFrequency) { periodFrequency = (PeriodFrequency) frequency; } else if (frequency instanceof SimpleFrequency) { periodFrequency = ((SimpleFrequency) frequency).toPeriodFrequency(); } else { throw new IllegalArgumentException("For the moment can only deal with PeriodFrequency and SimpleFrequency"); } final Period period = periodFrequency.getPeriod(); final List<ZonedDateTime> dates = new ArrayList<>(); ZonedDateTime date = effectiveDate; // TODO this is only correct if effective date = accrual date date = date.plus(period); while (isWithinSwapLifetime(date, maturityDate)) { // REVIEW: could speed this up by working out how many periods between start and end date? dates.add(date); date = date.plus(period); } return dates.toArray(EMPTY_ARRAY); } //TODO: add doc public static ZonedDateTime[] getUnadjustedDateSchedule(final ZonedDateTime effectiveDate, final ZonedDateTime accrualDate, final ZonedDateTime maturityDate, final Period period) { ArgumentChecker.notNull(effectiveDate, "effective date"); ArgumentChecker.notNull(accrualDate, "accrual date"); ArgumentChecker.notNull(maturityDate, "maturity date"); ArgumentChecker.notNull(period, "period"); if (effectiveDate.isAfter(maturityDate)) { throw new IllegalArgumentException("Effective date was after maturity"); } if (accrualDate.isAfter(maturityDate)) { throw new IllegalArgumentException("Accrual date was after maturity"); } // TODO what if there's no valid date between accrual date and maturity date? final List<ZonedDateTime> dates = new ArrayList<>(); int nbPeriod = 1; // M 26-Aug ZonedDateTime date = effectiveDate; // TODO this is only correct if effective date = accrual date date = date.plus(period); while (isWithinSwapLifetime(date, maturityDate)) { // REVIEW: could speed this up by working out how many periods between start and end date? dates.add(date); nbPeriod++; // M 26-Aug date = effectiveDate.plus(period.multipliedBy(nbPeriod)); // M 26-Aug date = date.plus(period); } return dates.toArray(EMPTY_ARRAY); } // ------------------------------------------------------------------------- /** * Counts back from maturityDate, filling to equally spaced dates frequency * times a year until the last date <b>after</b> effective date. * * @param effectiveDate the date that terminates to back counting (i.e. the first date is after this date), not null * @param maturityDate the date to count back from, not null * @param frequency how many times a year dates occur, not null * @return the first date after effectiveDate (i.e. effectiveDate is <b>not</b> included to the maturityDate (included) */ public static ZonedDateTime[] getBackwardsUnadjustedDateSchedule(final ZonedDateTime effectiveDate, final ZonedDateTime maturityDate, final Frequency frequency) { ArgumentChecker.notNull(effectiveDate, "effective date"); ArgumentChecker.notNull(maturityDate, "maturity date"); ArgumentChecker.notNull(frequency, "frequency"); if (effectiveDate.isAfter(maturityDate)) { throw new IllegalArgumentException("Effective date was after maturity"); } PeriodFrequency periodFrequency; if (frequency instanceof PeriodFrequency) { periodFrequency = (PeriodFrequency) frequency; } else if (frequency instanceof SimpleFrequency) { periodFrequency = ((SimpleFrequency) frequency).toPeriodFrequency(); } else { throw new IllegalArgumentException("For the moment can only deal with PeriodFrequency and SimpleFrequency"); } final Period period = periodFrequency.getPeriod(); final List<ZonedDateTime> dates = new ArrayList<>(); ZonedDateTime date = maturityDate; // TODO review the tolerance given while (date.isAfter(effectiveDate) && DateUtils.getExactDaysBetween(effectiveDate, date) > 4.0) { dates.add(date); date = date.minus(period); } Collections.sort(dates); return dates.toArray(EMPTY_ARRAY); } private static boolean isWithinSwapLifetime(final ZonedDateTime date, final ZonedDateTime maturity) { // TODO change me urgently if (date.isBefore(maturity)) { return true; } if (DateUtils.getDaysBetween(date, maturity) < 7) { return true; } return false; } public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime[] dates, final BusinessDayConvention convention, final Calendar calendar) { return getAdjustedDateSchedule(dates, convention, calendar, 0); } /** * Return the dates adjusted by a certain number of business days. * @param dates The initial dates. * @param convention The business day convention. * @param calendar The calendar. * @param settlementDays The number of days of the adjustment. Can be negative or positive. * @return The adjusted dates. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime[] dates, final BusinessDayConvention convention, final Calendar calendar, final int settlementDays) { ArgumentChecker.notEmpty(dates, "dates"); ArgumentChecker.notNull(convention, "convention"); ArgumentChecker.notNull(calendar, "calendar"); final int n = dates.length; final ZonedDateTime[] result = new ZonedDateTime[n]; for (int i = 0; i < n; i++) { ZonedDateTime date = convention.adjustDate(calendar, dates[i]); if (settlementDays > 0) { for (int loopday = 0; loopday < settlementDays; loopday++) { date = date.plusDays(1); while (!calendar.isWorkingDay(date.toLocalDate())) { date = date.plusDays(1); } } } else { for (int loopday = 0; loopday < -settlementDays; loopday++) { date = date.minusDays(1); while (!calendar.isWorkingDay(date.toLocalDate())) { date = date.minusDays(1); } } } result[i] = date; } return result; } public static ZonedDateTime getAdjustedDate(final ZonedDateTime originalDate, final BusinessDayConvention convention, final Calendar calendar, final int offset) { ArgumentChecker.notNull(originalDate, "date"); ArgumentChecker.notNull(convention, "convention"); ArgumentChecker.notNull(calendar, "calendar"); ZonedDateTime date = convention.adjustDate(calendar, originalDate); if (offset > 0) { for (int loopday = 0; loopday < offset; loopday++) { date = date.plusDays(1); while (!calendar.isWorkingDay(date.toLocalDate())) { date = date.plusDays(1); } } } else { for (int loopday = 0; loopday < -offset; loopday++) { date = date.minusDays(1); while (!calendar.isWorkingDay(date.toLocalDate())) { date = date.minusDays(1); } } } return date; } /** * Construct an array of dates according the a start date, an end date, the period between dates and the conventions. * The start date is not included in the array. The date are constructed forward and the stub period, if any, is last. * The end date is always included in the schedule. * @param startDate The reference initial date for the construction. * @param endDate The end date. Usually unadjusted. * @param period The period between payments. * @param businessDayConvention The business day convention. * @param calendar The applicable calendar. * @param isEOM The end-of-month rule flag. * @param stubShort Flag indicating if the stub, if any, is short (true) or long (false). * @return The array of dates. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period period, final BusinessDayConvention businessDayConvention, final Calendar calendar, final boolean isEOM, final boolean stubShort) { boolean eomApply = false; if (isEOM) { final BusinessDayConvention following = new FollowingBusinessDayConvention(); eomApply = (following.adjustDate(calendar, startDate.plusDays(1)).getMonth() != startDate.getMonth()); } // When the end-of-month rule applies and the start date is on month-end, the dates are the last business day of the month. BusinessDayConvention actualBDC; final List<ZonedDateTime> adjustedDates = new ArrayList<>(); ZonedDateTime date = startDate; if (eomApply) { actualBDC = new PrecedingBusinessDayConvention(); //To ensure that the date stays in the current month. date = date.plus(period).with(TemporalAdjusters.lastDayOfMonth()); while (date.isBefore(endDate)) { // date is strictly before endDate adjustedDates.add(actualBDC.adjustDate(calendar, date)); date = date.plus(period).with(TemporalAdjusters.lastDayOfMonth()); } } else { actualBDC = businessDayConvention; date = date.plus(period); while (date.isBefore(endDate)) { // date is strictly before endDate adjustedDates.add(businessDayConvention.adjustDate(calendar, date)); date = date.plus(period); } } // For long stub the last date before end date, if any, is removed. if (!stubShort && adjustedDates.size() >= 1) { adjustedDates.remove(adjustedDates.size() - 1); } adjustedDates.add(actualBDC.adjustDate(calendar, endDate)); // the end date return adjustedDates.toArray(EMPTY_ARRAY); } public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Frequency frequency, final BusinessDayConvention businessDayConvention, final Calendar calendar, final boolean isEOM) { PeriodFrequency periodFrequency; if (frequency instanceof PeriodFrequency) { periodFrequency = (PeriodFrequency) frequency; } else if (frequency instanceof SimpleFrequency) { periodFrequency = ((SimpleFrequency) frequency).toPeriodFrequency(); } else { throw new IllegalArgumentException("For the moment can only deal with PeriodFrequency and SimpleFrequency"); } final Period period = periodFrequency.getPeriod(); return getAdjustedDateSchedule(startDate, endDate, period, businessDayConvention, calendar, isEOM, true); } /** * Construct an array of dates according the a start date, an end date, the period between dates and the conventions. * The start date is not included in the array. The date are constructed forward and the stub period, if any, is last * and short. The end date is always included in the schedule. * @param startDate The reference initial date for the construction. * @param endDate The end date. Usually unadjusted. * @param period The period between payments. * @param businessDayConvention The business day convention. * @param calendar The applicable calendar. * @param isEOM The end-of-month rule flag. * @return The array of dates. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final ZonedDateTime endDate, final Period period, final BusinessDayConvention businessDayConvention, final Calendar calendar, final boolean isEOM) { return getAdjustedDateSchedule(startDate, endDate, period, businessDayConvention, calendar, isEOM, true); } /** * Construct an array of dates according the a start date, an end date, the period between dates and the conventions. * The start date is not included in the array. The date are constructed forward and the stub period, if any, is last. * The end date is always included in the schedule. * @param startDate The reference initial date for the construction. * @param tenor The annuity tenor. * @param period The period between payments. * @param businessDayConvention The business day convention. * @param calendar The applicable calendar. * @param isEOM The end-of-month rule flag. * @param shortStub Flag indicating if the stub, if any, is short (true) or long (false). * @return The array of dates. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final Period tenor, final Period period, final BusinessDayConvention businessDayConvention, final Calendar calendar, final boolean isEOM, final boolean shortStub) { final ZonedDateTime endDate = startDate.plus(tenor); return getAdjustedDateSchedule(startDate, endDate, period, businessDayConvention, calendar, isEOM, shortStub); } /** * Construct an array of dates according the a start date, an end date, the period between dates and the conventions. * The start date is not included in the array. The date are constructed forward and the stub period, if any, is short * and last. The end date is always included in the schedule. * @param startDate The reference initial date for the construction. * @param tenorAnnuity The annuity tenor. * @param periodPayments The period between payments. * @param businessDayConvention The business day convention. * @param calendar The applicable calendar. * @param isEOM The end-of-month rule flag. * @return The array of dates. */ public static ZonedDateTime[] getAdjustedDateSchedule(final ZonedDateTime startDate, final Period tenorAnnuity, final Period periodPayments, final BusinessDayConvention businessDayConvention, final Calendar calendar, final boolean isEOM) { final ZonedDateTime endDate = startDate.plus(tenorAnnuity); return getAdjustedDateSchedule(startDate, endDate, periodPayments, businessDayConvention, calendar, isEOM, true); } public static ZonedDateTime[] getSettlementDateSchedule(final ZonedDateTime[] dates, final Calendar calendar, final BusinessDayConvention businessDayConvention, final int settlementDays) { ArgumentChecker.notEmpty(dates, "dates"); ArgumentChecker.notNull(calendar, "calendar"); final int n = dates.length; final ZonedDateTime[] result = new ZonedDateTime[n]; for (int i = 0; i < n; i++) { ZonedDateTime date = businessDayConvention.adjustDate(calendar, dates[i].plusDays(1)); for (int j = 0; j < settlementDays; j++) { date = businessDayConvention.adjustDate(calendar, date.plusDays(1)); } result[i] = date; } return result; } public static LocalDate[] getSettlementDateSchedule(final LocalDate[] dates, final Calendar calendar, final BusinessDayConvention businessDayConvention, final int settlementDays) { ArgumentChecker.notEmpty(dates, "dates"); ArgumentChecker.notNull(calendar, "calendar"); final int n = dates.length; final LocalDate[] result = new LocalDate[n]; for (int i = 0; i < n; i++) { LocalDate date = businessDayConvention.adjustDate(calendar, dates[i].plusDays(1)); for (int j = 0; j < settlementDays; j++) { date = businessDayConvention.adjustDate(calendar, date.plusDays(1)); } result[i] = date; } return result; } public static ZonedDateTime[] getAdjustedResetDateSchedule(final ZonedDateTime effectiveDate, final ZonedDateTime[] dates, final BusinessDayConvention convention, final Calendar calendar, final int settlementDays) { ArgumentChecker.notNull(effectiveDate, "effective date"); ArgumentChecker.notEmpty(dates, "dates"); ArgumentChecker.notNull(convention, "convention"); ArgumentChecker.notNull(calendar, "calendar"); final int n = dates.length; final ZonedDateTime[] result = new ZonedDateTime[n]; result[0] = effectiveDate; for (int i = 1; i < n; i++) { result[i] = convention.adjustDate(calendar, dates[i - 1].minusDays(settlementDays)); } return result; } public static ZonedDateTime[] getAdjustedMaturityDateSchedule(final ZonedDateTime effectiveDate, final ZonedDateTime[] dates, final BusinessDayConvention convention, final Calendar calendar, final Frequency frequency) { ArgumentChecker.notEmpty(dates, "dates"); ArgumentChecker.notNull(convention, "convention"); ArgumentChecker.notNull(calendar, "calendar"); ArgumentChecker.notNull(frequency, "frequency"); PeriodFrequency periodFrequency; if (frequency instanceof PeriodFrequency) { periodFrequency = (PeriodFrequency) frequency; } else if (frequency instanceof SimpleFrequency) { periodFrequency = ((SimpleFrequency) frequency).toPeriodFrequency(); } else { throw new IllegalArgumentException("For the moment can only deal with PeriodFrequency and SimpleFrequency"); } final Period period = periodFrequency.getPeriod(); final int n = dates.length; final ZonedDateTime[] results = new ZonedDateTime[n]; results[0] = effectiveDate.plus(period); for (int i = 1; i < n; i++) { results[i] = convention.adjustDate(calendar, dates[i - 1].plus(period)); // TODO need to further shift these dates by a convention } return results; } /** * Converts a set of dates into time periods in years for a specified date and using a specified day count convention. * * @param dates a set of dates, not null * @param dayCount the day count convention, not null * @param fromDate the date from which to measure the time period to the dates, not null * @return a double array of time periods (in years) - if a date is <b>before</b> the fromDate as negative value is returned, not null */ public static double[] getTimes(final ZonedDateTime[] dates, final DayCount dayCount, final ZonedDateTime fromDate) { ArgumentChecker.notEmpty(dates, "dates"); ArgumentChecker.notNull(dayCount, "day count"); ArgumentChecker.notNull(fromDate, "from date"); final int n = dates.length; final double[] result = new double[n]; double yearFrac; for (int i = 0; i < (n); i++) { if (dates[i].isAfter(fromDate)) { yearFrac = dayCount.getDayCountFraction(fromDate, dates[i]); } else { yearFrac = -dayCount.getDayCountFraction(dates[i], fromDate); } result[i] = yearFrac; } return result; } public static int numberOfNegativeValues(final double[] periods) { int count = 0; for (final double period : periods) { if (period < 0.0) { count++; } } return count; } public static double[] removeFirstNValues(final double[] data, final int n) { return Arrays.copyOfRange(data, n, data.length); } public static double[] getYearFractions(final ZonedDateTime[] dates, final DayCount dayCount, final ZonedDateTime fromDate) { ArgumentChecker.notEmpty(dates, "dates"); ArgumentChecker.notNull(dayCount, "day count"); ArgumentChecker.notNull(fromDate, "from date"); final int n = dates.length; final double[] result = new double[n]; result[0] = dayCount.getDayCountFraction(fromDate, dates[0]); for (int i = 1; i < n; i++) { result[i] = dayCount.getDayCountFraction(dates[i - 1], dates[i]); } return result; } /** * Generates the start dates from the specified start date and set of end dates. * @param startDate the first start date. * @param endDates the set of end dates to generate start dates from. * @return the start dates relative to the end dates. */ public static ZonedDateTime[] getStartDates(final ZonedDateTime startDate, final ZonedDateTime[] endDates) { final ZonedDateTime[] startDates = new ZonedDateTime[endDates.length]; startDates[0] = startDate; System.arraycopy(endDates, 0, startDates, 1, endDates.length - 1); return startDates; } //------------------------------------------------------------------------- /** * Checks if the schedule is generated from the end. * * @param stub the stub type * @return true if generating from the end */ private static boolean isGenerateFromEnd(final StubType stub) { return StubType.LONG_START.equals(stub) || StubType.SHORT_START.equals(stub); } /** * Checks if the EOM rule applies. * <p> * If generation occurs forwards, check if the start date is the last day of the month. * If generation occurs backwards, check if the end date is the last day of the month. * * @param fromEnd true if generating from the end backwards * @param startDate the start date * @param endDate the end date * @param calendar the holiday calendar * @return true if the rule applies */ private static boolean eomApplies(boolean fromEnd, ZonedDateTime startDate, ZonedDateTime endDate, Calendar calendar) { if (fromEnd) { // end-of-month rule applies if end date is on last day of month (last business day used here) return getAdjustedDate(endDate, 1, calendar).getMonth() != endDate.getMonth(); } else { // end-of-month rule applies if start date is on last day of month (last business day used here) return getAdjustedDate(startDate, 1, calendar).getMonth() != startDate.getMonth(); } } }