001package net.objectlab.kit.datecalc.common.ccy;
002
003import java.io.Serializable;
004import java.util.List;
005import java.util.stream.Collectors;
006
007import net.objectlab.kit.datecalc.common.CurrencyDateCalculator;
008import net.objectlab.kit.datecalc.common.CurrencyDateCalculatorBuilder;
009import net.objectlab.kit.datecalc.common.DefaultHolidayCalendar;
010import net.objectlab.kit.datecalc.common.HolidayCalendar;
011import net.objectlab.kit.datecalc.common.HolidayHandler;
012import net.objectlab.kit.datecalc.common.ImmutableHolidayCalendar;
013import net.objectlab.kit.datecalc.common.NonWorkingDayChecker;
014import net.objectlab.kit.datecalc.common.ReadOnlyHolidayCalendar;
015import net.objectlab.kit.datecalc.common.SpotLag;
016import net.objectlab.kit.datecalc.common.Tenor;
017import net.objectlab.kit.datecalc.common.TenorCode;
018import net.objectlab.kit.datecalc.common.WorkingWeek;
019
020/**
021 * Abstract Currency calculator implementation in order to encapsulate all the common functionality
022 * between Jdk/Jdk8 and Joda implementations. It is parameterized on <E>
023 * but basically <code>Date</code> and <code>LocalDate</code> are the only
024 * viable values for it for now.
025 *
026 * This follows convention for currency, see http://www.londonfx.co.uk/valdates.html
027 *
028 * <h3>Currency Holiday</h3>
029 * For most T+2 currency pairs (spotLag=2), if T+1 is a USD holiday, then this does not normally affect the spot date,
030 * but if a non-USD currency in the currency pair has a holiday on T+1, then it will make the spot date
031 * become T+3. If USD or either currency of a pair have a holiday on T+2, then the spot date
032 * will be T+3. This means, for example, that crosses such as EUR/GBP can never have a spot date
033 * on 4th July (although such a date could be quoted as an outright).
034 *
035 * <h3>Latin American currencies</h3>
036 * USD holidays normally affect the spot date only if T+2 is a USD holiday.
037 * If T+1 is a USD holiday, this does not normally prevent T+2 from being the spot date.
038 * Certain Latin American currencies (ARS, CLP and MXN) are an exception to this.
039 * If T+1 is a USD holiday, then the spot date for the affected currencies will be T+3.
040 * For example, if the trade date is a Monday and a USD holiday falls on the Tuesday,
041 * then the spot date for EUR/USD will be the Wednesday, but the spot date for USD/MXN will be the Thursday.
042 *
043 * @since 1.4.0
044 */
045public abstract class AbstractCurrencyDateCalculator<E extends Serializable> implements CurrencyDateCalculator<E>, NonWorkingDayChecker<E> {
046    private static final int MONTHS_IN_YEAR = 12;
047    private static final int DAYS_IN_WEEK = 7;
048    private final String ccy1;
049    private final String ccy2;
050    private final String crossCcy;
051    private final HolidayCalendar<E> ccy1HolidayCalendar;
052    private final HolidayCalendar<E> ccy2HolidayCalendar;
053    private final HolidayCalendar<E> crossCcyHolidayCalendar;
054    private final HolidayHandler<E> holidayHandler;
055    private final WorkingWeek ccy1Week;
056    private final WorkingWeek ccy2Week;
057    private final WorkingWeek crossCcyWeek;
058    private final boolean brokenDateAllowed;
059    private final boolean useCrossCcyOnT1ForCcy1;
060    private final boolean useCrossCcyOnT1ForCcy2;
061    private final boolean adjustStartDateWithCcy1Ccy2;
062    private final SpotLag spotLag;
063
064    protected AbstractCurrencyDateCalculator(final CurrencyDateCalculatorBuilder<E> builder) {
065        builder.checkValidity();
066        this.ccy1 = builder.getCcy1();
067        this.ccy2 = builder.getCcy2();
068        this.crossCcy = builder.getCrossCcy();
069        this.ccy1HolidayCalendar = new ImmutableHolidayCalendar<>(
070                builder.getCcy1Calendar() != null ? builder.getCcy1Calendar() : new DefaultHolidayCalendar<E>());
071        this.ccy2HolidayCalendar = new ImmutableHolidayCalendar<>(
072                builder.getCcy2Calendar() != null ? builder.getCcy2Calendar() : new DefaultHolidayCalendar<E>());
073        this.crossCcyHolidayCalendar = new ImmutableHolidayCalendar<>(
074                builder.getCrossCcyCalendar() != null ? builder.getCrossCcyCalendar() : new DefaultHolidayCalendar<E>());
075        this.holidayHandler = builder.getTenorHolidayHandler();
076        this.ccy1Week = builder.getCcy1Week();
077        this.ccy2Week = builder.getCcy2Week();
078        this.crossCcyWeek = builder.getCrossCcyWeek();
079        this.brokenDateAllowed = builder.isBrokenDateAllowed();
080        this.spotLag = builder.getSpotLag();
081        this.adjustStartDateWithCcy1Ccy2 = builder.isAdjustStartDateWithCurrencyPair();
082        this.useCrossCcyOnT1ForCcy1 = builder.getCurrencyCalculatorConfig() != null
083                && builder.getCurrencyCalculatorConfig().getCurrenciesSubjectToCrossCcyForT1(crossCcy).contains(ccy1);
084        this.useCrossCcyOnT1ForCcy2 = builder.getCurrencyCalculatorConfig() != null
085                && builder.getCurrencyCalculatorConfig().getCurrenciesSubjectToCrossCcyForT1(crossCcy).contains(ccy2);
086    }
087
088    @Override
089    public boolean isUseCrossCcyOnT1ForCcy1() {
090        return useCrossCcyOnT1ForCcy1;
091    }
092
093    @Override
094    public boolean isUseCrossCcyOnT1ForCcy2() {
095        return useCrossCcyOnT1ForCcy2;
096    }
097
098    @Override
099    public SpotLag getSpotLag() {
100        return spotLag;
101    }
102
103    @Override
104    public boolean isAdjustStartDateWithCurrencyPair() {
105        return adjustStartDateWithCcy1Ccy2;
106    }
107
108    @Override
109    public boolean isBrokenDateAllowed() {
110        return brokenDateAllowed;
111    }
112
113    @Override
114    public String getCrossCcy() {
115        return crossCcy;
116    }
117
118    @Override
119    public String getCcy1() {
120        return ccy1;
121    }
122
123    @Override
124    public String getCcy2() {
125        return ccy2;
126    }
127
128    @Override
129    public String getName() {
130        return getCcy1() + "." + getCcy2();
131    }
132
133    @Override
134    public ReadOnlyHolidayCalendar<E> getCcy1Calendar() {
135        return ccy1HolidayCalendar;
136    }
137
138    @Override
139    public ReadOnlyHolidayCalendar<E> getCcy2Calendar() {
140        return ccy2HolidayCalendar;
141    }
142
143    @Override
144    public ReadOnlyHolidayCalendar<E> getCrossCcyCalendar() {
145        return crossCcyHolidayCalendar;
146    }
147
148    @Override
149    public WorkingWeek getCcy1Week() {
150        return ccy1Week;
151    }
152
153    @Override
154    public WorkingWeek getCcy2Week() {
155        return ccy2Week;
156    }
157
158    @Override
159    public WorkingWeek getCrossCcyWeek() {
160        return crossCcyWeek;
161    }
162
163    protected abstract E calculateNextDay(E date);
164
165    protected abstract int calendarWeekDay(E date);
166
167    protected abstract E max(E d1, E d2);
168
169    private boolean isNonWorkingDay(final E date, final WorkingWeek ww, final HolidayCalendar<E> calendar) {
170        return !ww.isWorkingDayFromCalendar(calendarWeekDay(date)) || calendar != null && calendar.isHoliday(date);
171    }
172
173    @Override
174    public boolean isNonWorkingDay(final E date) {
175        return isNonWorkingDay(date, ccy1Week, ccy1HolidayCalendar) || isNonWorkingDay(date, ccy2Week, ccy2HolidayCalendar)
176                || !brokenDateAllowed && isNonWorkingDay(date, crossCcyWeek, crossCcyHolidayCalendar);
177    }
178
179    private E adjustToNextWorkingDateForCcyPairIfRequired(final E startDate) {
180        E date = startDate;
181        while (isNonWorkingDay(date, ccy1Week, ccy1HolidayCalendar) || isNonWorkingDay(date, ccy2Week, ccy2HolidayCalendar)) {
182            date = calculateNextDay(date);
183        }
184        return date;
185    }
186
187    private E adjustToNextWorkingDateForCcyPairAndUsdIfRequired(final E startDate) {
188        E date = startDate;
189        while (isNonWorkingDay(date, crossCcyWeek, crossCcyHolidayCalendar) || isNonWorkingDay(date, ccy1Week, ccy1HolidayCalendar)
190                || isNonWorkingDay(date, ccy2Week, ccy2HolidayCalendar)) {
191            date = calculateNextDay(date);
192        }
193        return date;
194    }
195
196    private E calculateNextWorkingDay(final E startDate, final WorkingWeek ww, final HolidayCalendar<E> calendar) {
197        E date = calculateNextDay(startDate);
198        while (isNonWorkingDay(date, ww, calendar)) {
199            date = calculateNextDay(date);
200        }
201        return date;
202    }
203
204    private E calculateNextWorkingDayIfRequired(final E startDate, final WorkingWeek ww, final HolidayCalendar<E> calendar) {
205        E date = startDate;
206        while (isNonWorkingDay(date, ww, calendar)) {
207            date = calculateNextDay(date);
208        }
209        return date;
210    }
211
212    @Override
213    public E calculateSpotDate(final E startDate) {
214        E date = startDate;
215        if (adjustStartDateWithCcy1Ccy2 || spotLag == SpotLag.T_0) {
216            date = adjustToNextWorkingDateForCcyPairIfRequired(startDate);
217        }
218
219        // calculate Spot for ccy1
220        final E spotCcy1 = calculateCcySpot(ccy1, date, ccy1Week, ccy1HolidayCalendar);
221
222        // calculate Spot for ccy2
223        final E spotCcy2 = calculateCcySpot(ccy2, date, ccy2Week, ccy2HolidayCalendar);
224
225        // if spotCcy1 == spotCcy2 -> return it
226        E spotDate = max(spotCcy1, spotCcy2);
227
228        // if not, consider max and adjustToNextWorkingDateForCcyPairIfRequired
229        if (brokenDateAllowed) {
230            spotDate = adjustToNextWorkingDateForCcyPairIfRequired(spotDate);
231        } else {
232            spotDate = adjustToNextWorkingDateForCcyPairAndUsdIfRequired(spotDate);
233        }
234
235        return spotDate;
236    }
237
238    private E calculateCcySpot(final String ccy, final E date, final WorkingWeek workingWeek, final HolidayCalendar<E> calendar) {
239        // calculate T+1
240        E calcSpot = date;
241
242        if (spotLag != SpotLag.T_0) {
243            if (spotLag == SpotLag.T_2) {
244                calcSpot = calculateNextWorkingDay(calcSpot, workingWeek, crossCcy.equalsIgnoreCase(ccy) ? null : calendar); // crossCcy
245                // does
246                // not
247                // impact
248                // T+1
249
250                if (useCrossCcyOnT1ForCcy1 && ccy1.equals(ccy) || useCrossCcyOnT1ForCcy2 && ccy2.equals(ccy)) {
251                    // move if USD is holiday
252                    calcSpot = calculateNextWorkingDayIfRequired(calcSpot, crossCcyWeek, crossCcyHolidayCalendar);
253                    // check that it is still ok for the original ccy
254                    calcSpot = calculateNextWorkingDayIfRequired(calcSpot, workingWeek, calendar);
255                }
256            }
257
258            // calculate T+2
259            calcSpot = calculateNextWorkingDay(calcSpot, workingWeek, calendar);
260        }
261        return calcSpot;
262    }
263
264    @Override
265    public E calculateTenorDate(final E startDate, final Tenor tenor) {
266        if (tenor == null) {
267            throw new IllegalArgumentException("Tenor cannot be null");
268        }
269
270        TenorCode tenorCode = tenor.getCode();
271        E date = startDate;
272        if (tenorCode != TenorCode.OVERNIGHT && tenorCode != TenorCode.TOM_NEXT /*&& spotLag != 0*/) {
273            // get to the Spot date first:
274            date = calculateSpotDate(date);
275        }
276        int unit = tenor.getUnits();
277        if (tenorCode == TenorCode.WEEK) {
278            tenorCode = TenorCode.DAY;
279            unit *= DAYS_IN_WEEK;
280        }
281
282        if (tenorCode == TenorCode.YEAR) {
283            tenorCode = TenorCode.MONTH;
284            unit *= MONTHS_IN_YEAR;
285        }
286
287        return applyTenor(date, tenorCode, unit);
288    }
289
290    private E applyTenor(final E date, final TenorCode tenorCode, final int unit) {
291        E calc = date;
292        // move by tenor
293        switch (tenorCode) {
294        case OVERNIGHT:
295            calc = adjustForCcyPairIfRequired(calc);
296            break;
297        case TOM_NEXT: // it would have NOT moved by
298        case SPOT_NEXT:
299            calc = calculateNextDay(calc);
300            calc = adjustForCcyPairIfRequired(calc);
301            break;
302        case SPOT: // good as-is
303            break;
304        case DAY:
305            for (int i = 0; i < unit; i++) {
306                calc = calculateNextDay(calc);
307            }
308            calc = adjustForCcyPairIfRequired(calc);
309            break;
310        case MONTH:
311            calc = addMonths(calc, unit);
312            calc = adjustForCcyPairIfRequired(calc);
313            break;
314        default:
315            throw new UnsupportedOperationException("Sorry not yet...");
316        }
317        return calc;
318    }
319
320    private E adjustForCcyPairIfRequired(final E startDate) {
321        return holidayHandler.adjustDate(startDate, 1, this);
322    }
323
324    protected abstract E addMonths(E calc, int unit);
325
326    @Override
327    public List<E> calculateTenorDates(final E startDate, final List<Tenor> tenors) {
328        return tenors.stream().map(tenor -> calculateTenorDate(startDate, tenor)).collect(Collectors.toList());
329    }
330}