/** * 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); }
/** * 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 }
/** * 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; }
/** Expiry calculator for gold future contracts. */ public final class GoldFutureExpiryCalculator implements ExchangeTradedInstrumentExpiryCalculator { /** Name of the calculator */ public static final String NAME = "GoldFutureExpiryCalculator"; /** Singleton. */ private static final GoldFutureExpiryCalculator INSTANCE = new GoldFutureExpiryCalculator(); /** Adjuster. */ private static final TemporalAdjuster LAST_DAY_ADJUSTER = TemporalAdjusters.lastDayOfMonth(); /** * Gets the singleton instance. * * @return the instance, not null */ public static GoldFutureExpiryCalculator getInstance() { return INSTANCE; } /** Restricted constructor. */ private GoldFutureExpiryCalculator() {} // ------------------------------------------------------------------------- /** * Gets trading months (not static as depends on current date). * * @param now the date today, not null * @return the valid trading months, not null */ private Month[] getTradingMonths(final LocalDate now) { // this may need improvements as the year end approaches Set<Month> ret = new TreeSet<>(); ret.add(now.getMonth()); // this month ret.add(now.getMonth().plus(1)); // next month ret.add(now.getMonth().plus(2)); // next 2 months // February, April, August, and October in next 23 months ret.add(Month.FEBRUARY); ret.add(Month.APRIL); ret.add(Month.AUGUST); ret.add(Month.OCTOBER); // June and December falling in next 72 month period ret.add(Month.JUNE); ret.add(Month.DECEMBER); // assuming this gives enough valid dates so dont go round to next 12 month period return ret.toArray(new Month[0]); } /** * Expiry date of Soybean Futures: The 3rd last business day of the month. See * http://www.cmegroup.com/trading/metals/precious/gold_contract_specifications.html * * @param n the n'th expiry date after today, greater than zero * @param today the valuation date, not null * @param holidayCalendar the holiday calendar, not null * @return the expiry date, not null */ @Override public LocalDate getExpiryDate( final int n, final LocalDate today, final Calendar holidayCalendar) { ArgumentChecker.isTrue(n > 0, "n must be greater than zero; have {}", n); ArgumentChecker.notNull(today, "today"); ArgumentChecker.notNull(holidayCalendar, "holiday calendar"); LocalDate expiryDate = getExpiryMonth(n, today).with(LAST_DAY_ADJUSTER); int nBusinessDays = 3; if (holidayCalendar.isWorkingDay(expiryDate)) { nBusinessDays--; } // go back to 3 business days while (nBusinessDays > 0) { expiryDate = expiryDate.minusDays(1); if (holidayCalendar.isWorkingDay(expiryDate)) { nBusinessDays--; } } return expiryDate; } @Override public LocalDate getExpiryMonth(final int n, final LocalDate today) { ArgumentChecker.isTrue(n > 0, "n must be greater than zero"); ArgumentChecker.notNull(today, "today"); LocalDate expiryDate = today; Month[] validMonths = getTradingMonths(today); for (int m = n; m > 0; m--) { expiryDate = getNextExpiryMonth(validMonths, expiryDate); } return expiryDate; } private LocalDate getNextExpiryMonth(final Month[] validMonths, final LocalDate dtCurrent) { Month mthCurrent = dtCurrent.getMonth(); int idx = Arrays.binarySearch(validMonths, mthCurrent); if (Math.abs(idx) >= (validMonths.length - 1)) { return LocalDate.of(dtCurrent.getYear() + 1, validMonths[0], dtCurrent.getDayOfMonth()); } else if (idx >= 0) { return dtCurrent.with(validMonths[idx + 1]); } else { return dtCurrent.with(validMonths[-idx + 1]); } } @Override public String getName() { return NAME; } }