/**
* Copyright (C) 2013 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.impl.credit.isda;
import java.time.LocalDate;
import java.time.Period;
import java.time.temporal.JulianFields;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.opengamma.strata.basics.date.BusinessDayConvention;
import com.opengamma.strata.basics.date.HolidayCalendar;
import com.opengamma.strata.basics.schedule.StubConvention;
import com.opengamma.strata.collect.ArgChecker;
/**
* For a CDS with set set of payments on the fixed leg, this holds the payments dates and the accrual start and end dates. It does not
* hold the payment amounts with depends on the day-count (normally ACT/360) and the spread.
*/
public class IsdaPremiumLegSchedule {
private final int _nPayments;
private final LocalDate[] _accStartDates;
private final LocalDate[] _accEndDates;
private final LocalDate[] _paymentDates;
private final LocalDate[] _nominalPaymentDates;
/**
* This mimics JpmcdsDateListMakeRegular. Produces a set of ascending dates by following the rules:<p>
* If the stub is at the front end, we role backwards from the endDate at an integer multiple of the specified step size (e.g. 3M),
* adding these date until we pass the startDate(this date is not added). If the stub type is short, the startDate is added (as the first date), hence the first period
* will be less than (or equal to) the remaining periods. If the stub type is long, the startDate is also added, but the date immediately
* after that is removed, so the first period is longer than the remaining.<p>
* If the stub is at the back end, we role forward from the startDate at an integer multiple of the specified step size (e.g. 3M),
* adding these date until we pass the endDate(this date is not added). If the stub type is short, the endDate is added (as the last date), hence the last period
* will be less than (or equal to) the other periods. If the stub type is long, the endDate is also added, but the date immediately
* before that is removed, so the last period is longer than the others.
*
* @param startDate The start date - this will be the first entry in the list
* @param endDate The end date - this will be the last entry in the list
* @param step the step period (e.g. 3M - will produce dates every 3 months, with adjustments at the beginning or end based on stub type)
* @param stubType the stub convention
* @return an array of LocalDate
*/
public static LocalDate[] getUnadjustedDates(LocalDate startDate, LocalDate endDate, Period step, StubConvention stubType) {
ArgChecker.notNull(startDate, "null startDate");
ArgChecker.notNull(endDate, "null endDate");
ArgChecker.notNull(step, "step");
ArgChecker.notNull(stubType, "null stubType");
ArgChecker.isFalse(endDate.isBefore(startDate), "end date is before startDate");
if (startDate.isEqual(endDate)) { // this can only happen if protectionStart == true
LocalDate[] tempDates = new LocalDate[2];
tempDates[0] = startDate;
tempDates[1] = endDate;
return tempDates;
}
ArgChecker.isFalse(stubType == StubConvention.NONE, "NONE is not allowed as a stub convention");
ArgChecker.isFalse(stubType == StubConvention.BOTH, "BOTH is not allowed as a stub convention");
long firstJulianDate = startDate.getLong(JulianFields.MODIFIED_JULIAN_DAY);
long secondJulianDate = endDate.getLong(JulianFields.MODIFIED_JULIAN_DAY);
double days = step.getDays() + 365.0 * (step.getMonths() / 12. + step.getYears());
int nApprox = 3 + (int) ((secondJulianDate - firstJulianDate) / days);
List<LocalDate> dates = new ArrayList<>(nApprox);
// stub at front end, so start at endDate and work backwards
if (stubType.isCalculateBackwards()) {
int intervals = 0;
LocalDate tDate = endDate;
while (tDate.isAfter(startDate)) {
dates.add(tDate);
Period tStep = step.multipliedBy(++intervals); // this mimics ISDA c code, rather than true market convention
tDate = endDate.minus(tStep);
}
int n = dates.size();
if (tDate.isEqual(startDate) || n == 1 || stubType == StubConvention.SHORT_INITIAL) {
dates.add(startDate);
} else {
// long front stub - remove the last date entry in the list and replace it with startDate
dates.remove(n - 1);
dates.add(startDate);
}
int m = dates.size();
LocalDate[] res = new LocalDate[m];
// want to output in ascending chronological order, so need to reverse the list
int j = m - 1;
for (int i = 0; i < m; i++, j--) {
res[j] = dates.get(i);
}
return res;
// stub at back end, so start at startDate and work forward
} else {
int intervals = 0;
LocalDate tDate = startDate;
while (tDate.isBefore(endDate)) {
dates.add(tDate);
Period tStep = step.multipliedBy(++intervals); // this mimics ISDA c code, rather than true market convention
tDate = startDate.plus(tStep);
}
int n = dates.size();
if (tDate.isEqual(endDate) || n == 1 || stubType == StubConvention.SHORT_FINAL) {
dates.add(endDate);
} else {
// long back stub - remove the last date entry in the list and replace it with endDate
dates.remove(n - 1);
dates.add(endDate);
}
LocalDate[] res = new LocalDate[dates.size()];
return dates.toArray(res);
}
}
public static IsdaPremiumLegSchedule truncateSchedule(LocalDate stepin, IsdaPremiumLegSchedule schedule) {
return schedule.truncateSchedule(stepin);
}
/**
* Remove all payment intervals before the given date
* @param stepin a date
* @return truncate schedule
*/
public IsdaPremiumLegSchedule truncateSchedule(LocalDate stepin) {
if (!_accStartDates[0].isBefore(stepin)) {
return this; // nothing to truncate
}
int index = getAccStartDateIndex(stepin);
if (index < 0) {
index = -(index + 1) - 1; // keep the one before the insertion point
}
return truncateSchedule(index);
}
/**
* makes a new ISDAPremiumLegSchedule with payment before index removed
* @param index the index of the old schedule that will be the zero index of the new
* @return truncate schedule
*/
public IsdaPremiumLegSchedule truncateSchedule(int index) {
return new IsdaPremiumLegSchedule(_nominalPaymentDates, _paymentDates, _accStartDates, _accEndDates, index);
}
/**
* Truncation constructor
* @param paymentDates
* @param accStartDates
* @param accEndDates
* @param index copy the date starting from this index
*/
private IsdaPremiumLegSchedule(
LocalDate[] nominalPaymentDates,
LocalDate[] paymentDates,
LocalDate[] accStartDates,
LocalDate[] accEndDates,
int index) {
ArgChecker.noNulls(nominalPaymentDates, "unadjustedDates");
ArgChecker.noNulls(paymentDates, "paymentDates");
ArgChecker.noNulls(accStartDates, "accStartDates");
ArgChecker.noNulls(accEndDates, "accEndDates");
int n = paymentDates.length;
_nPayments = n - index;
ArgChecker.isTrue(
n == nominalPaymentDates.length,
"nominalPaymentDates length of {} does not match paymentDates length of {}", nominalPaymentDates.length, _nPayments);
ArgChecker.isTrue(
n == accStartDates.length,
"accStartDates length of {} does not match paymentDates length of {}", accStartDates.length, _nPayments);
ArgChecker.isTrue(
n == accEndDates.length,
"accEndDates length of {} does not match paymentDates length of {}", accEndDates.length, _nPayments);
_nominalPaymentDates = new LocalDate[_nPayments];
_paymentDates = new LocalDate[_nPayments];
_accStartDates = new LocalDate[_nPayments];
_accEndDates = new LocalDate[_nPayments];
System.arraycopy(nominalPaymentDates, index, _nominalPaymentDates, 0, _nPayments);
System.arraycopy(paymentDates, index, _paymentDates, 0, _nPayments);
System.arraycopy(accStartDates, index, _accStartDates, 0, _nPayments);
System.arraycopy(accEndDates, index, _accEndDates, 0, _nPayments);
}
/**
* Mimics JpmcdsCdsFeeLegMake
* @param startDate The protection start date
* @param endDate The protection end date
* @param step The period or frequency at which payments are made (e.g. every three months)
* @param stubType The stub convention
* @param businessdayAdjustmentConvention options are 'following' or 'proceeding'
* @param calandar A holiday calendar
* @param protectionStart If true, protection starts are the beginning rather than end of day (protection still ends at end of day).
*/
public IsdaPremiumLegSchedule(
LocalDate startDate,
LocalDate endDate,
Period step,
StubConvention stubType,
BusinessDayConvention businessdayAdjustmentConvention,
HolidayCalendar calandar,
boolean protectionStart) {
this(getUnadjustedDates(startDate, endDate, step, stubType), businessdayAdjustmentConvention, calandar, protectionStart);
}
public IsdaPremiumLegSchedule(
LocalDate[] unadjustedDates,
BusinessDayConvention businessdayAdjustmentConvention,
HolidayCalendar calendar,
boolean protectionStart) {
_nPayments = unadjustedDates.length - 1;
_nominalPaymentDates = new LocalDate[_nPayments];
_paymentDates = new LocalDate[_nPayments];
_accStartDates = new LocalDate[_nPayments];
_accEndDates = new LocalDate[_nPayments];
LocalDate dPrev = unadjustedDates[0];
LocalDate dPrevAdj = dPrev; // first date is never adjusted
for (int i = 0; i < _nPayments; i++) {
LocalDate dNext = unadjustedDates[i + 1];
LocalDate dNextAdj = businessDayAdjustDate(dNext, calendar, businessdayAdjustmentConvention);
_accStartDates[i] = dPrevAdj;
_accEndDates[i] = dNextAdj;
_nominalPaymentDates[i] = dNext;
_paymentDates[i] = dNextAdj;
dPrev = dNext;
dPrevAdj = dNextAdj;
}
// the last accrual date is not adjusted for business-day
_accEndDates[_nPayments - 1] = getFinalAccEndDate(unadjustedDates[_nPayments], protectionStart);
}
public static LocalDate getFinalAccEndDate(LocalDate unadjustedDate, boolean protectionStart) {
ArgChecker.notNull(unadjustedDate, "unadjustedDate");
if (protectionStart) {
return unadjustedDate.plusDays(1); // extra day of accrued interest
} else {
return unadjustedDate;
}
}
public int getNumPayments() {
return _nPayments;
}
public LocalDate getAccStartDate(int index) {
return _accStartDates[index];
}
public LocalDate getAccEndDate(int index) {
return _accEndDates[index];
}
public LocalDate getPaymentDate(int index) {
return _paymentDates[index];
}
public LocalDate getNominalPaymentDate(int index) {
return _nominalPaymentDates[index];
}
/**
* finds the index in accStartDate that matches the given date, or if date is not a member of accStartDate returns (-insertionPoint -1)
* @see Arrays#binarySearch
* @param date The date to find
* @return index or code giving insertion point
*/
public int getAccStartDateIndex(LocalDate date) {
return Arrays.binarySearch(_accStartDates, date, null);
}
/**
* finds the index in paymentDate that matches the given date, or if date is not a member of paymentDate returns (-insertionPoint -1)
* @see Arrays#binarySearch
* @param date The date to find
* @return index or code giving insertion point
*/
public int getPaymentDateIndex(LocalDate date) {
return Arrays.binarySearch(_paymentDates, date, null);
}
public int getNominalPaymentDateIndex(LocalDate date) {
return Arrays.binarySearch(_nominalPaymentDates, date, null);
}
/**
* The accrual start date, end date and payment date at the given index
* @param index the index (from zero)
* @return array of LocalDate
*/
public LocalDate[] getAccPaymentDateTriplet(int index) {
return new LocalDate[] {_accStartDates[index], _accEndDates[index], _paymentDates[index]};
}
private LocalDate businessDayAdjustDate(LocalDate date, HolidayCalendar calendar, BusinessDayConvention convention) {
ArgChecker.notNull(date, "date");
ArgChecker.notNull(calendar, "HolidayCalendar");
ArgChecker.notNull(convention, "Business day adjustment");
return convention.adjust(date, calendar);
}
}