package net.objectlab.kit.datecalc.common.ccy; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import net.objectlab.kit.datecalc.common.CurrencyDateCalculator; import net.objectlab.kit.datecalc.common.CurrencyDateCalculatorBuilder; import net.objectlab.kit.datecalc.common.DefaultHolidayCalendar; import net.objectlab.kit.datecalc.common.HolidayCalendar; import net.objectlab.kit.datecalc.common.HolidayHandler; import net.objectlab.kit.datecalc.common.ImmutableHolidayCalendar; import net.objectlab.kit.datecalc.common.NonWorkingDayChecker; import net.objectlab.kit.datecalc.common.ReadOnlyHolidayCalendar; import net.objectlab.kit.datecalc.common.SpotLag; import net.objectlab.kit.datecalc.common.Tenor; import net.objectlab.kit.datecalc.common.TenorCode; import net.objectlab.kit.datecalc.common.WorkingWeek; /** * Abstract Currency calculator implementation in order to encapsulate all the common functionality * between Jdk/Jdk8 and Joda implementations. It is parameterized on <E> * but basically <code>Date</code> and <code>LocalDate</code> are the only * viable values for it for now. * * This follows convention for currency, see http://www.londonfx.co.uk/valdates.html * * <h3>Currency Holiday</h3> * For most T+2 currency pairs (spotLag=2), if T+1 is a USD holiday, then this does not normally affect the spot date, * but if a non-USD currency in the currency pair has a holiday on T+1, then it will make the spot date * become T+3. If USD or either currency of a pair have a holiday on T+2, then the spot date * will be T+3. This means, for example, that crosses such as EUR/GBP can never have a spot date * on 4th July (although such a date could be quoted as an outright). * * <h3>Latin American currencies</h3> * USD holidays normally affect the spot date only if T+2 is a USD holiday. * If T+1 is a USD holiday, this does not normally prevent T+2 from being the spot date. * Certain Latin American currencies (ARS, CLP and MXN) are an exception to this. * If T+1 is a USD holiday, then the spot date for the affected currencies will be T+3. * For example, if the trade date is a Monday and a USD holiday falls on the Tuesday, * then the spot date for EUR/USD will be the Wednesday, but the spot date for USD/MXN will be the Thursday. * * @since 1.4.0 */ public abstract class AbstractCurrencyDateCalculator<E extends Serializable> implements CurrencyDateCalculator<E>, NonWorkingDayChecker<E> { private static final int MONTHS_IN_YEAR = 12; private static final int DAYS_IN_WEEK = 7; private final String ccy1; private final String ccy2; private final String crossCcy; private final HolidayCalendar<E> ccy1HolidayCalendar; private final HolidayCalendar<E> ccy2HolidayCalendar; private final HolidayCalendar<E> crossCcyHolidayCalendar; private final HolidayHandler<E> holidayHandler; private final WorkingWeek ccy1Week; private final WorkingWeek ccy2Week; private final WorkingWeek crossCcyWeek; private final boolean brokenDateAllowed; private final boolean useCrossCcyOnT_1ForCcy1; private final boolean useCrossCcyOnT_1ForCcy2; private final boolean adjustStartDateWithCcy1Ccy2; private final SpotLag spotLag; protected AbstractCurrencyDateCalculator(final CurrencyDateCalculatorBuilder<E> builder) { builder.checkValidity(); this.ccy1 = builder.getCcy1(); this.ccy2 = builder.getCcy2(); this.crossCcy = builder.getCrossCcy(); this.ccy1HolidayCalendar = new ImmutableHolidayCalendar<E>( builder.getCcy1Calendar() != null ? builder.getCcy1Calendar() : new DefaultHolidayCalendar<E>()); this.ccy2HolidayCalendar = new ImmutableHolidayCalendar<E>( builder.getCcy2Calendar() != null ? builder.getCcy2Calendar() : new DefaultHolidayCalendar<E>()); this.crossCcyHolidayCalendar = new ImmutableHolidayCalendar<E>( builder.getCrossCcyCalendar() != null ? builder.getCrossCcyCalendar() : new DefaultHolidayCalendar<E>()); this.holidayHandler = builder.getTenorHolidayHandler(); this.ccy1Week = builder.getCcy1Week(); this.ccy2Week = builder.getCcy2Week(); this.crossCcyWeek = builder.getCrossCcyWeek(); this.brokenDateAllowed = builder.isBrokenDateAllowed(); this.spotLag = builder.getSpotLag(); this.adjustStartDateWithCcy1Ccy2 = builder.isAdjustStartDateWithCurrencyPair(); this.useCrossCcyOnT_1ForCcy1 = builder.getCurrencyCalculatorConfig() != null && builder.getCurrencyCalculatorConfig().getCurrenciesSubjectToCrossCcyForT1(crossCcy).contains(ccy1); this.useCrossCcyOnT_1ForCcy2 = builder.getCurrencyCalculatorConfig() != null && builder.getCurrencyCalculatorConfig().getCurrenciesSubjectToCrossCcyForT1(crossCcy).contains(ccy2); } public boolean isUseCrossCcyOnT1ForCcy1() { return useCrossCcyOnT_1ForCcy1; } public boolean isUseCrossCcyOnT1ForCcy2() { return useCrossCcyOnT_1ForCcy2; } public SpotLag getSpotLag() { return spotLag; } public boolean isAdjustStartDateWithCurrencyPair() { return adjustStartDateWithCcy1Ccy2; } public boolean isBrokenDateAllowed() { return brokenDateAllowed; } public String getCrossCcy() { return crossCcy; } public String getCcy1() { return ccy1; } public String getCcy2() { return ccy2; } public String getName() { return getCcy1() + "." + getCcy2(); } public ReadOnlyHolidayCalendar<E> getCcy1Calendar() { return ccy1HolidayCalendar; } public ReadOnlyHolidayCalendar<E> getCcy2Calendar() { return ccy2HolidayCalendar; } public ReadOnlyHolidayCalendar<E> getCrossCcyCalendar() { return crossCcyHolidayCalendar; } public WorkingWeek getCcy1Week() { return ccy1Week; } public WorkingWeek getCcy2Week() { return ccy2Week; } public WorkingWeek getCrossCcyWeek() { return crossCcyWeek; } protected abstract E calculateNextDay(E date); protected abstract int calendarWeekDay(E date); protected abstract E max(E d1, E d2); private boolean isNonWorkingDay(final E date, final WorkingWeek ww, final HolidayCalendar<E> calendar) { return !ww.isWorkingDayFromCalendar(calendarWeekDay(date)) || calendar != null && calendar.isHoliday(date); } public boolean isNonWorkingDay(final E date) { return isNonWorkingDay(date, ccy1Week, ccy1HolidayCalendar) || isNonWorkingDay(date, ccy2Week, ccy2HolidayCalendar) || !brokenDateAllowed && isNonWorkingDay(date, crossCcyWeek, crossCcyHolidayCalendar); } private E adjustToNextWorkingDateForCcyPairIfRequired(final E startDate) { E date = startDate; while (isNonWorkingDay(date, ccy1Week, ccy1HolidayCalendar) || isNonWorkingDay(date, ccy2Week, ccy2HolidayCalendar)) { date = calculateNextDay(date); } return date; } private E adjustToNextWorkingDateForCcyPairAndUsdIfRequired(final E startDate) { E date = startDate; while (isNonWorkingDay(date, crossCcyWeek, crossCcyHolidayCalendar) || isNonWorkingDay(date, ccy1Week, ccy1HolidayCalendar) || isNonWorkingDay(date, ccy2Week, ccy2HolidayCalendar)) { date = calculateNextDay(date); } return date; } private E calculateNextWorkingDay(final E startDate, final WorkingWeek ww, final HolidayCalendar<E> calendar) { E date = calculateNextDay(startDate); while (isNonWorkingDay(date, ww, calendar)) { date = calculateNextDay(date); } return date; } private E calculateNextWorkingDayIfRequired(final E startDate, final WorkingWeek ww, final HolidayCalendar<E> calendar) { E date = startDate; while (isNonWorkingDay(date, ww, calendar)) { date = calculateNextDay(date); } return date; } public E calculateSpotDate(final E startDate) { E date = startDate; if (adjustStartDateWithCcy1Ccy2 || spotLag == SpotLag.T_0) { date = adjustToNextWorkingDateForCcyPairIfRequired(startDate); } // calculate Spot for ccy1 final E spotCcy1 = calculateCcySpot(ccy1, date, ccy1Week, ccy1HolidayCalendar); // calculate Spot for ccy2 final E spotCcy2 = calculateCcySpot(ccy2, date, ccy2Week, ccy2HolidayCalendar); // if spotCcy1 == spotCcy2 -> return it E spotDate = max(spotCcy1, spotCcy2); // if not, consider max and adjustToNextWorkingDateForCcyPairIfRequired if (brokenDateAllowed) { spotDate = adjustToNextWorkingDateForCcyPairIfRequired(spotDate); } else { spotDate = adjustToNextWorkingDateForCcyPairAndUsdIfRequired(spotDate); } return spotDate; } private E calculateCcySpot(final String ccy, final E date, final WorkingWeek workingWeek, final HolidayCalendar<E> calendar) { // calculate T+1 E calcSpot = date; if (spotLag != SpotLag.T_0) { if (spotLag == SpotLag.T_2) { calcSpot = calculateNextWorkingDay(calcSpot, workingWeek, crossCcy.equalsIgnoreCase(ccy) ? null : calendar); // crossCcy // does // not // impact // T+1 if (useCrossCcyOnT_1ForCcy1 && ccy1.equals(ccy) || useCrossCcyOnT_1ForCcy2 && ccy2.equals(ccy)) { // move if USD is holiday calcSpot = calculateNextWorkingDayIfRequired(calcSpot, crossCcyWeek, crossCcyHolidayCalendar); // check that it is still ok for the original ccy calcSpot = calculateNextWorkingDayIfRequired(calcSpot, workingWeek, calendar); } } // calculate T+2 calcSpot = calculateNextWorkingDay(calcSpot, workingWeek, calendar); } return calcSpot; } public E calculateTenorDate(final E startDate, final Tenor tenor) { if (tenor == null) { throw new IllegalArgumentException("Tenor cannot be null"); } TenorCode tenorCode = tenor.getCode(); E date = startDate; if (tenorCode != TenorCode.OVERNIGHT && tenorCode != TenorCode.TOM_NEXT /*&& spotLag != 0*/) { // get to the Spot date first: date = calculateSpotDate(date); } int unit = tenor.getUnits(); if (tenorCode == TenorCode.WEEK) { tenorCode = TenorCode.DAY; unit *= DAYS_IN_WEEK; } if (tenorCode == TenorCode.YEAR) { tenorCode = TenorCode.MONTH; unit *= MONTHS_IN_YEAR; } return applyTenor(date, tenorCode, unit); } private E applyTenor(final E date, final TenorCode tenorCode, final int unit) { E calc = date; // move by tenor switch (tenorCode) { case OVERNIGHT: calc = adjustForCcyPairIfRequired(calc); break; case TOM_NEXT: // it would have NOT moved by calc = calculateNextDay(calc); calc = adjustForCcyPairIfRequired(calc); break; case SPOT_NEXT: calc = calculateNextDay(calc); calc = adjustForCcyPairIfRequired(calc); break; case SPOT: calc = date; break; case DAY: for (int i = 0; i < unit; i++) { calc = calculateNextDay(calc); } calc = adjustForCcyPairIfRequired(calc); break; case MONTH: calc = addMonths(calc, unit); calc = adjustForCcyPairIfRequired(calc); break; default: throw new UnsupportedOperationException("Sorry not yet..."); } return calc; } private E adjustForCcyPairIfRequired(final E startDate) { final E date = holidayHandler.adjustDate(startDate, 1, this); return date; } protected abstract E addMonths(E calc, int unit); public List<E> calculateTenorDates(final E startDate, final List<Tenor> tenors) { final List<E> results = new ArrayList<E>(); for (final Tenor tenor : tenors) { results.add(calculateTenorDate(startDate, tenor)); } return results; } }