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}