
com.opengamma.strata.product.swaption.SwaptionExercise Maven / Gradle / Ivy
Show all versions of strata-product Show documentation
/*
* Copyright (C) 2021 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.product.swaption;
import static com.opengamma.strata.collect.Guavate.toImmutableList;
import static com.opengamma.strata.collect.Guavate.zip;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Optional;
import java.util.stream.LongStream;
import org.joda.beans.Bean;
import org.joda.beans.BeanBuilder;
import org.joda.beans.ImmutableBean;
import org.joda.beans.JodaBeanUtils;
import org.joda.beans.MetaBean;
import org.joda.beans.MetaProperty;
import org.joda.beans.gen.BeanDefinition;
import org.joda.beans.gen.ImmutableValidator;
import org.joda.beans.gen.PropertyDefinition;
import org.joda.beans.impl.direct.DirectMetaBean;
import org.joda.beans.impl.direct.DirectMetaProperty;
import org.joda.beans.impl.direct.DirectMetaPropertyMap;
import org.joda.beans.impl.direct.DirectPrivateBeanBuilder;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Ordering;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.date.AdjustableDate;
import com.opengamma.strata.basics.date.AdjustableDates;
import com.opengamma.strata.basics.date.BusinessDayAdjustment;
import com.opengamma.strata.basics.date.DateAdjuster;
import com.opengamma.strata.basics.date.DaysAdjustment;
import com.opengamma.strata.basics.schedule.Frequency;
import com.opengamma.strata.collect.ArgChecker;
/**
* Details as to when a swaption can be exercised.
*
* A swaption can have three different kinds of exercise - European, American and Bermudan.
* A European swaption has one exercise date, an American can exercise on any date, and a Bermudan
* can exercise on a fixed set of dates.
*/
@BeanDefinition(builderScope = "private")
public final class SwaptionExercise
implements ImmutableBean, Serializable {
/**
* An explicit list of exercise dates.
*
* A European swaption has one date in the list.
* A Bermudan swaption has at least two dates in the list.
* An American swaption has at exactly two dates in the list, the earliest and latest dates.
*/
@PropertyDefinition(validate = "notNull")
private final AdjustableDates dateDefinition;
/**
* The frequency of exercise between the earliest and latest dates.
*
* An American swaption must set this to one day.
*
* A Bermudan swaption might set this to a specific frequency instead of pre-calculating the dates.
* If it does this, there must only be two dates in the list.
* The intermediate dates will be calculated by adding multiples of the frequency to the earliest date.
*/
@PropertyDefinition(get = "optional")
private final Frequency frequency;
/**
* The offset to the swap start date.
*
* Each adjusted exercise date has this offset applied to get the start date of the underlying swap.
*/
@PropertyDefinition(validate = "notNull")
private final DaysAdjustment swapStartDateOffset;
//-------------------------------------------------------------------------
/**
* Obtains an instance for a European swaption.
*
* @param exerciseDate the exercise date
* @param swapStartDateOffset the swap start date offset
* @return the exercise
*/
public static SwaptionExercise ofEuropean(AdjustableDate exerciseDate, DaysAdjustment swapStartDateOffset) {
AdjustableDates dates = AdjustableDates.of(exerciseDate.getAdjustment(), exerciseDate.getUnadjusted());
return new SwaptionExercise(dates, null, swapStartDateOffset);
}
/**
* Obtains an instance for an American swaption.
*
* @param earliestExerciseDate the earliest exercise date
* @param latestExerciseDate the latest exercise date
* @param dateAdjustment the date adjustment
* @param swapStartDateOffset the swap start date offset
* @return the exercise
*/
public static SwaptionExercise ofAmerican(
LocalDate earliestExerciseDate,
LocalDate latestExerciseDate,
BusinessDayAdjustment dateAdjustment,
DaysAdjustment swapStartDateOffset) {
AdjustableDates dates = AdjustableDates.of(dateAdjustment, earliestExerciseDate, latestExerciseDate);
return new SwaptionExercise(dates, Frequency.P1D, swapStartDateOffset);
}
/**
* Obtains an instance for a Bermudan swaption.
*
* @param exerciseDates the exercise dates
* @param swapStartDateOffset the swap start date offset
* @return the exercise
*/
public static SwaptionExercise ofBermudan(AdjustableDates exerciseDates, DaysAdjustment swapStartDateOffset) {
return new SwaptionExercise(exerciseDates, null, swapStartDateOffset);
}
/**
* Obtains an instance for a Bermudan swaption where the dates are calculated.
*
* For example, if the dates represent a 5 year period and the frequency is yearly then
* the Bermudan swaption can be exercised each year in the period. The exact dates are
* calculated by adding multiples of the frequency to the earliest date.
*
* @param earliestExerciseDate the earliest exercise date
* @param latestExerciseDate the latest exercise date
* @param dateAdjustment the date adjustment
* @param frequency the frequency
* @param swapStartDateOffset the swap start date offset
* @return the exercise
*/
public static SwaptionExercise ofBermudan(
LocalDate earliestExerciseDate,
LocalDate latestExerciseDate,
BusinessDayAdjustment dateAdjustment,
Frequency frequency,
DaysAdjustment swapStartDateOffset) {
AdjustableDates dates = AdjustableDates.of(dateAdjustment, earliestExerciseDate, latestExerciseDate);
Frequency normalizedFrequency = Frequency.of(frequency.getPeriod().normalized());
return new SwaptionExercise(dates, normalizedFrequency, swapStartDateOffset);
}
//-------------------------------------------------------------------------
@ImmutableValidator
private void validate() {
ArgChecker.isTrue(
Ordering.natural().isStrictlyOrdered(dateDefinition.getUnadjusted()),
"Dates must be in order and without duplicates");
if (frequency != null && dateDefinition.getUnadjusted().size() != 2) {
throw new IllegalArgumentException("Frequency can only be used when there two exercise dates are defined");
}
}
//-------------------------------------------------------------------------
/**
* Checks if the exercise is European.
*
* @return true if European exercise on a single date
*/
public boolean isEuropean() {
return dateDefinition.getUnadjusted().size() == 1;
}
/**
* Checks if the exercise is American.
*
* @return true if American exercise on any date
*/
public boolean isAmerican() {
return dateDefinition.getUnadjusted().size() == 2 && Frequency.P1D.equals(frequency);
}
/**
* Checks if the exercise is Bermudan.
*
* @return true if Bermudan exercise on a specific set of dates
*/
public boolean isBermudan() {
return !isEuropean() && !isAmerican();
}
//-------------------------------------------------------------------------
/**
* Gets the calculated list of exercise dates.
*
* This could be a large list in the case of an American exercise.
*
* @return the exercise dates
*/
public AdjustableDates calculateDates() {
if (frequency != null) {
LocalDate start = dateDefinition.getUnadjusted().get(0);
LocalDate end = dateDefinition.getUnadjusted().get(1);
if (frequency.equals(Frequency.P1D)) {
ImmutableList dates = LongStream.rangeClosed(start.toEpochDay(), end.toEpochDay())
.mapToObj(LocalDate::ofEpochDay)
.collect(toImmutableList());
return AdjustableDates.of(dateDefinition.getAdjustment(), dates);
}
ImmutableList.Builder dates = ImmutableList.builder();
dates.add(start);
for (int i = 1; ; i++) {
LocalDate date = start.plus(frequency.getPeriod().multipliedBy(i));
if (date.isBefore(end)) {
dates.add(date);
} else {
dates.add(end);
break;
}
}
return AdjustableDates.of(dateDefinition.getAdjustment(), dates.build());
} else {
return dateDefinition;
}
}
//-------------------------------------------------------------------------
// resolves the date definition
SwaptionExerciseDates resolve(ReferenceData refData) {
AdjustableDates defn = isBermudan() ? calculateDates() : dateDefinition;
ImmutableList unadjusted = defn.getUnadjusted();
ImmutableList adjusted = defn.adjusted(refData);
DateAdjuster startDateOffset = swapStartDateOffset.resolve(refData);
ImmutableList dates = zip(adjusted.stream(), unadjusted.stream())
.map(pair -> SwaptionExerciseDate.builder()
.exerciseDate(pair.getFirst())
.unadjustedExerciseDate(pair.getSecond())
.swapStartDate(startDateOffset.adjust(pair.getFirst()))
.build())
.collect(toImmutableList());
return SwaptionExerciseDates.of(dates, isAmerican());
}
//-------------------------------------------------------------------------
/**
* Selects a single exercise date based on the proposed date.
*
* This validates the proposed exercise date and returns it.
*
* The date is matched as an adjusted date first, then as an unadjusted date.
* If the date can only be an adjusted date, the result will use {@link BusinessDayAdjustment#NONE}.
*
* @param proposedExerciseDate the proposed exercise date
* @param refData the reference data
* @return the exercise dates
* @throws IllegalArgumentException if the proposed exercise date is not valid
*/
public AdjustableDate selectDate(LocalDate proposedExerciseDate, ReferenceData refData) {
DateAdjuster adjuster = dateDefinition.getAdjustment().resolve(refData);
if (Frequency.P1D.equals(frequency)) {
return selectAmerican(proposedExerciseDate, adjuster);
} else {
return selectStandard(proposedExerciseDate, adjuster);
}
}
// American (avoid calculating the whole set of dates)
private AdjustableDate selectAmerican(LocalDate proposedExerciseDate, DateAdjuster adjuster) {
LocalDate start = dateDefinition.getUnadjusted().get(0);
LocalDate end = dateDefinition.getUnadjusted().get(1);
// search adjusted dates
for (LocalDate unadjusted = end; !unadjusted.isBefore(start); unadjusted = unadjusted.minusDays(1)) {
if (adjuster.adjust(unadjusted).equals(proposedExerciseDate)) {
return unadjusted.equals(proposedExerciseDate) ?
AdjustableDate.of(proposedExerciseDate, dateDefinition.getAdjustment()) :
AdjustableDate.of(proposedExerciseDate);
}
}
// search unadjusted dates
if (!proposedExerciseDate.isBefore(start) && !proposedExerciseDate.isAfter(end)) {
return AdjustableDate.of(proposedExerciseDate, dateDefinition.getAdjustment());
}
throw new IllegalArgumentException("Invalid exercise date: " + proposedExerciseDate);
}
// Bermudan or European
private AdjustableDate selectStandard(LocalDate proposedExerciseDate, DateAdjuster adjuster) {
AdjustableDates dates = calculateDates();
// search adjusted dates
for (LocalDate unadjusted : dates.getUnadjusted()) {
if (adjuster.adjust(unadjusted).equals(proposedExerciseDate)) {
return unadjusted.equals(proposedExerciseDate) ?
AdjustableDate.of(proposedExerciseDate, dateDefinition.getAdjustment()) :
AdjustableDate.of(proposedExerciseDate);
}
}
// search unadjusted dates
if (dates.getUnadjusted().contains(proposedExerciseDate)) {
return AdjustableDate.of(proposedExerciseDate, dateDefinition.getAdjustment());
}
throw new IllegalArgumentException("Invalid exercise date: " + proposedExerciseDate);
}
//------------------------- AUTOGENERATED START -------------------------
/**
* The meta-bean for {@code SwaptionExercise}.
* @return the meta-bean, not null
*/
public static SwaptionExercise.Meta meta() {
return SwaptionExercise.Meta.INSTANCE;
}
static {
MetaBean.register(SwaptionExercise.Meta.INSTANCE);
}
/**
* The serialization version id.
*/
private static final long serialVersionUID = 1L;
private SwaptionExercise(
AdjustableDates dateDefinition,
Frequency frequency,
DaysAdjustment swapStartDateOffset) {
JodaBeanUtils.notNull(dateDefinition, "dateDefinition");
JodaBeanUtils.notNull(swapStartDateOffset, "swapStartDateOffset");
this.dateDefinition = dateDefinition;
this.frequency = frequency;
this.swapStartDateOffset = swapStartDateOffset;
validate();
}
@Override
public SwaptionExercise.Meta metaBean() {
return SwaptionExercise.Meta.INSTANCE;
}
//-----------------------------------------------------------------------
/**
* Gets an explicit list of exercise dates.
*
* A European swaption has one date in the list.
* A Bermudan swaption has at least two dates in the list.
* An American swaption has at exactly two dates in the list, the earliest and latest dates.
* @return the value of the property, not null
*/
public AdjustableDates getDateDefinition() {
return dateDefinition;
}
//-----------------------------------------------------------------------
/**
* Gets the frequency of exercise between the earliest and latest dates.
*
* An American swaption must set this to one day.
*
* A Bermudan swaption might set this to a specific frequency instead of pre-calculating the dates.
* If it does this, there must only be two dates in the list.
* The intermediate dates will be calculated by adding multiples of the frequency to the earliest date.
* @return the optional value of the property, not null
*/
public Optional getFrequency() {
return Optional.ofNullable(frequency);
}
//-----------------------------------------------------------------------
/**
* Gets the offset to the swap start date.
*
* Each adjusted exercise date has this offset applied to get the start date of the underlying swap.
* @return the value of the property, not null
*/
public DaysAdjustment getSwapStartDateOffset() {
return swapStartDateOffset;
}
//-----------------------------------------------------------------------
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (obj != null && obj.getClass() == this.getClass()) {
SwaptionExercise other = (SwaptionExercise) obj;
return JodaBeanUtils.equal(dateDefinition, other.dateDefinition) &&
JodaBeanUtils.equal(frequency, other.frequency) &&
JodaBeanUtils.equal(swapStartDateOffset, other.swapStartDateOffset);
}
return false;
}
@Override
public int hashCode() {
int hash = getClass().hashCode();
hash = hash * 31 + JodaBeanUtils.hashCode(dateDefinition);
hash = hash * 31 + JodaBeanUtils.hashCode(frequency);
hash = hash * 31 + JodaBeanUtils.hashCode(swapStartDateOffset);
return hash;
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder(128);
buf.append("SwaptionExercise{");
buf.append("dateDefinition").append('=').append(JodaBeanUtils.toString(dateDefinition)).append(',').append(' ');
buf.append("frequency").append('=').append(JodaBeanUtils.toString(frequency)).append(',').append(' ');
buf.append("swapStartDateOffset").append('=').append(JodaBeanUtils.toString(swapStartDateOffset));
buf.append('}');
return buf.toString();
}
//-----------------------------------------------------------------------
/**
* The meta-bean for {@code SwaptionExercise}.
*/
public static final class Meta extends DirectMetaBean {
/**
* The singleton instance of the meta-bean.
*/
static final Meta INSTANCE = new Meta();
/**
* The meta-property for the {@code dateDefinition} property.
*/
private final MetaProperty dateDefinition = DirectMetaProperty.ofImmutable(
this, "dateDefinition", SwaptionExercise.class, AdjustableDates.class);
/**
* The meta-property for the {@code frequency} property.
*/
private final MetaProperty frequency = DirectMetaProperty.ofImmutable(
this, "frequency", SwaptionExercise.class, Frequency.class);
/**
* The meta-property for the {@code swapStartDateOffset} property.
*/
private final MetaProperty swapStartDateOffset = DirectMetaProperty.ofImmutable(
this, "swapStartDateOffset", SwaptionExercise.class, DaysAdjustment.class);
/**
* The meta-properties.
*/
private final Map> metaPropertyMap$ = new DirectMetaPropertyMap(
this, null,
"dateDefinition",
"frequency",
"swapStartDateOffset");
/**
* Restricted constructor.
*/
private Meta() {
}
@Override
protected MetaProperty> metaPropertyGet(String propertyName) {
switch (propertyName.hashCode()) {
case 257736609: // dateDefinition
return dateDefinition;
case -70023844: // frequency
return frequency;
case 1366770128: // swapStartDateOffset
return swapStartDateOffset;
}
return super.metaPropertyGet(propertyName);
}
@Override
public BeanBuilder extends SwaptionExercise> builder() {
return new SwaptionExercise.Builder();
}
@Override
public Class extends SwaptionExercise> beanType() {
return SwaptionExercise.class;
}
@Override
public Map> metaPropertyMap() {
return metaPropertyMap$;
}
//-----------------------------------------------------------------------
/**
* The meta-property for the {@code dateDefinition} property.
* @return the meta-property, not null
*/
public MetaProperty dateDefinition() {
return dateDefinition;
}
/**
* The meta-property for the {@code frequency} property.
* @return the meta-property, not null
*/
public MetaProperty frequency() {
return frequency;
}
/**
* The meta-property for the {@code swapStartDateOffset} property.
* @return the meta-property, not null
*/
public MetaProperty swapStartDateOffset() {
return swapStartDateOffset;
}
//-----------------------------------------------------------------------
@Override
protected Object propertyGet(Bean bean, String propertyName, boolean quiet) {
switch (propertyName.hashCode()) {
case 257736609: // dateDefinition
return ((SwaptionExercise) bean).getDateDefinition();
case -70023844: // frequency
return ((SwaptionExercise) bean).frequency;
case 1366770128: // swapStartDateOffset
return ((SwaptionExercise) bean).getSwapStartDateOffset();
}
return super.propertyGet(bean, propertyName, quiet);
}
@Override
protected void propertySet(Bean bean, String propertyName, Object newValue, boolean quiet) {
metaProperty(propertyName);
if (quiet) {
return;
}
throw new UnsupportedOperationException("Property cannot be written: " + propertyName);
}
}
//-----------------------------------------------------------------------
/**
* The bean-builder for {@code SwaptionExercise}.
*/
private static final class Builder extends DirectPrivateBeanBuilder {
private AdjustableDates dateDefinition;
private Frequency frequency;
private DaysAdjustment swapStartDateOffset;
/**
* Restricted constructor.
*/
private Builder() {
}
//-----------------------------------------------------------------------
@Override
public Object get(String propertyName) {
switch (propertyName.hashCode()) {
case 257736609: // dateDefinition
return dateDefinition;
case -70023844: // frequency
return frequency;
case 1366770128: // swapStartDateOffset
return swapStartDateOffset;
default:
throw new NoSuchElementException("Unknown property: " + propertyName);
}
}
@Override
public Builder set(String propertyName, Object newValue) {
switch (propertyName.hashCode()) {
case 257736609: // dateDefinition
this.dateDefinition = (AdjustableDates) newValue;
break;
case -70023844: // frequency
this.frequency = (Frequency) newValue;
break;
case 1366770128: // swapStartDateOffset
this.swapStartDateOffset = (DaysAdjustment) newValue;
break;
default:
throw new NoSuchElementException("Unknown property: " + propertyName);
}
return this;
}
@Override
public SwaptionExercise build() {
return new SwaptionExercise(
dateDefinition,
frequency,
swapStartDateOffset);
}
//-----------------------------------------------------------------------
@Override
public String toString() {
StringBuilder buf = new StringBuilder(128);
buf.append("SwaptionExercise.Builder{");
buf.append("dateDefinition").append('=').append(JodaBeanUtils.toString(dateDefinition)).append(',').append(' ');
buf.append("frequency").append('=').append(JodaBeanUtils.toString(frequency)).append(',').append(' ');
buf.append("swapStartDateOffset").append('=').append(JodaBeanUtils.toString(swapStartDateOffset));
buf.append('}');
return buf.toString();
}
}
//-------------------------- AUTOGENERATED END --------------------------
}