OptionSurfaceDataInterpolated.java
package net.finmath.marketdata.model.volatilities;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import net.finmath.functions.AnalyticFormulas;
import net.finmath.interpolation.RationalFunctionInterpolation;
import net.finmath.interpolation.RationalFunctionInterpolation.ExtrapolationMethod;
import net.finmath.interpolation.RationalFunctionInterpolation.InterpolationMethod;
import net.finmath.marketdata.model.AnalyticModel;
import net.finmath.marketdata.model.curves.DiscountCurve;
/**
* An interpolated equity option surface based on irregular option quotes.
* <p>
* The class extends {@link OptionSurfaceData} by adding finmath-native
* interpolation
* and quoting-convention conversion. The underlying market data may be supplied
* either as individual {@link OptionData} quotes or as {@link OptionSmileData}
* objects.
* </p>
* <p>
* The surface does not assume a rectangular grid. Different maturities may have
* different strike grids and different numbers of option quotes.
* </p>
* <p>
* Internally, the market data are represented as nodal points
* {@code (maturity, strike, value)}. The sorted representation is ordered by
* maturity first and by strike within maturity. The interpolated value at
* {@code (T,K)} is obtained by:
* </p>
* <ol>
* <li>interpolating each maturity smile in strike at {@code K};</li>
* <li>interpolating the resulting values in maturity at {@code T}.</li>
* </ol>
* <p>
* This class is intentionally responsible only for interpolation and
* quoting-convention conversion.
* </p>
*
* @author Alessandro Gnoatto
*/
public class OptionSurfaceDataInterpolated extends OptionSurfaceData implements VolatilitySurface {
/**
* The default strike interpolation method.
*/
private static final InterpolationMethod DEFAULT_STRIKE_INTERPOLATION_METHOD = InterpolationMethod.CUBIC_SPLINE;
/**
* The default strike extrapolation method.
*/
private static final ExtrapolationMethod DEFAULT_STRIKE_EXTRAPOLATION_METHOD = ExtrapolationMethod.CONSTANT;
/**
* The default maturity interpolation method.
*/
private static final InterpolationMethod DEFAULT_MATURITY_INTERPOLATION_METHOD = InterpolationMethod.LINEAR;
/**
* The default maturity extrapolation method.
*/
private static final ExtrapolationMethod DEFAULT_MATURITY_EXTRAPOLATION_METHOD = ExtrapolationMethod.LINEAR;
/**
* The option quote comparator.
*/
private static final Comparator<OptionData> OPTION_QUOTE_COMPARATOR =
Comparator.comparingDouble(OptionData::getMaturity)
.thenComparingDouble(OptionData::getStrike);
/**
* The option smile comparator.
*/
private static final Comparator<OptionSmileData> OPTION_SMILE_COMPARATOR =
Comparator.comparingDouble(OptionSmileData::getMaturity);
/**
* The smiles.
*/
private final OptionSmileData[] smiles;
/**
* The node maturities.
*/
private final double[] nodeMaturities;
/**
* The node strikes.
*/
private final double[] nodeStrikes;
/**
* The node values.
*/
private final double[] nodeValues;
/**
* The unique maturities.
*/
private final double[] uniqueMaturities;
/**
* The strike interpolation method.
*/
private final InterpolationMethod strikeInterpolationMethod;
/**
* The strike extrapolation method.
*/
private final ExtrapolationMethod strikeExtrapolationMethod;
/**
* The maturity interpolation method.
*/
private final InterpolationMethod maturityInterpolationMethod;
/**
* The maturity extrapolation method.
*/
private final ExtrapolationMethod maturityExtrapolationMethod;
/**
* The smile interpolators.
*/
private final SmileInterpolator[] smileInterpolators;
/**
* Creates an interpolated surface from individual option quotes.
* <p>
* The input quotes are required to be sorted by maturity and by strike
* within
* each maturity. Use {@link #ofUnsorted(OptionData[], DiscountCurve,
* DiscountCurve)}
* if the input quotes are not sorted.
* </p>
*
* @param optionQuotes The individual option quotes.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
*/
public OptionSurfaceDataInterpolated(
final OptionData[] optionQuotes,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve) {
this(
prepareFromOptionQuotes(optionQuotes, false),
discountCurve,
equityForwardCurve,
DEFAULT_STRIKE_INTERPOLATION_METHOD,
DEFAULT_STRIKE_EXTRAPOLATION_METHOD,
DEFAULT_MATURITY_INTERPOLATION_METHOD,
DEFAULT_MATURITY_EXTRAPOLATION_METHOD
);
}
/**
* Creates an interpolated surface from individual option quotes.
* <p>
* The input quotes are required to be sorted by maturity and by strike
* within
* each maturity. Use {@link #ofUnsorted(OptionData[], DiscountCurve,
* DiscountCurve, InterpolationMethod, ExtrapolationMethod,
* InterpolationMethod, ExtrapolationMethod)}
* if the input quotes are not sorted.
* </p>
*
* @param optionQuotes The individual option quotes.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @param strikeInterpolationMethod The interpolation method in strike
* direction.
* @param strikeExtrapolationMethod The extrapolation method in strike
* direction.
* @param maturityInterpolationMethod The interpolation method in maturity
* direction.
* @param maturityExtrapolationMethod The extrapolation method in maturity
* direction.
*/
public OptionSurfaceDataInterpolated(
final OptionData[] optionQuotes,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve,
final InterpolationMethod strikeInterpolationMethod,
final ExtrapolationMethod strikeExtrapolationMethod,
final InterpolationMethod maturityInterpolationMethod,
final ExtrapolationMethod maturityExtrapolationMethod) {
this(
prepareFromOptionQuotes(optionQuotes, false),
discountCurve,
equityForwardCurve,
strikeInterpolationMethod,
strikeExtrapolationMethod,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
}
/**
* Creates an interpolated surface from option smiles.
* <p>
* The input smiles are required to be sorted by maturity. The strikes
* inside
* each smile are required to be sorted increasingly and to be unique. Use
* {@link #ofUnsorted(OptionSmileData[], DiscountCurve, DiscountCurve)} if
* the
* input smiles or strikes are not sorted.
* </p>
*
* @param smiles The option smiles.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
*/
public OptionSurfaceDataInterpolated(
final OptionSmileData[] smiles,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve) {
this(
prepareFromSmiles(smiles, false),
discountCurve,
equityForwardCurve,
DEFAULT_STRIKE_INTERPOLATION_METHOD,
DEFAULT_STRIKE_EXTRAPOLATION_METHOD,
DEFAULT_MATURITY_INTERPOLATION_METHOD,
DEFAULT_MATURITY_EXTRAPOLATION_METHOD
);
}
/**
* Creates an interpolated surface from option smiles.
* <p>
* The input smiles are required to be sorted by maturity. The strikes
* inside
* each smile are required to be sorted increasingly and to be unique. Use
* {@link #ofUnsorted(OptionSmileData[], DiscountCurve, DiscountCurve,
* InterpolationMethod, ExtrapolationMethod, InterpolationMethod,
* ExtrapolationMethod)}
* if the input smiles or strikes are not sorted.
* </p>
*
* @param smiles The option smiles.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @param strikeInterpolationMethod The interpolation method in strike
* direction.
* @param strikeExtrapolationMethod The extrapolation method in strike
* direction.
* @param maturityInterpolationMethod The interpolation method in maturity
* direction.
* @param maturityExtrapolationMethod The extrapolation method in maturity
* direction.
*/
public OptionSurfaceDataInterpolated(
final OptionSmileData[] smiles,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve,
final InterpolationMethod strikeInterpolationMethod,
final ExtrapolationMethod strikeExtrapolationMethod,
final InterpolationMethod maturityInterpolationMethod,
final ExtrapolationMethod maturityExtrapolationMethod) {
this(
prepareFromSmiles(smiles, false),
discountCurve,
equityForwardCurve,
strikeInterpolationMethod,
strikeExtrapolationMethod,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
}
/**
* Internal constructor from a prepared, sorted surface.
*
* @param preparedSurface The prepared surface.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @param strikeInterpolationMethod The interpolation method in strike
* direction.
* @param strikeExtrapolationMethod The extrapolation method in strike
* direction.
* @param maturityInterpolationMethod The interpolation method in maturity
* direction.
* @param maturityExtrapolationMethod The extrapolation method in maturity
* direction.
*/
private OptionSurfaceDataInterpolated(
final PreparedSurface preparedSurface,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve,
final InterpolationMethod strikeInterpolationMethod,
final ExtrapolationMethod strikeExtrapolationMethod,
final InterpolationMethod maturityInterpolationMethod,
final ExtrapolationMethod maturityExtrapolationMethod) {
super(preparedSurface.smiles, discountCurve, equityForwardCurve);
this.smiles = preparedSurface.smiles;
this.nodeMaturities = preparedSurface.nodeMaturities;
this.nodeStrikes = preparedSurface.nodeStrikes;
this.nodeValues = preparedSurface.nodeValues;
this.uniqueMaturities = preparedSurface.uniqueMaturities;
this.strikeInterpolationMethod = Objects.requireNonNull(strikeInterpolationMethod, "strikeInterpolationMethod");
this.strikeExtrapolationMethod = Objects.requireNonNull(strikeExtrapolationMethod, "strikeExtrapolationMethod");
this.maturityInterpolationMethod = Objects.requireNonNull(maturityInterpolationMethod, "maturityInterpolationMethod");
this.maturityExtrapolationMethod = Objects.requireNonNull(maturityExtrapolationMethod, "maturityExtrapolationMethod");
this.smileInterpolators = buildSmileInterpolators(preparedSurface.smiles);
}
/**
* Creates an interpolated surface from sorted individual option quotes.
*
* @param optionQuotes The individual option quotes, sorted by maturity and
* strike.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated of(
final OptionData[] optionQuotes,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve) {
return new OptionSurfaceDataInterpolated(optionQuotes, discountCurve, equityForwardCurve);
}
/**
* Creates an interpolated surface from sorted individual option quotes.
*
* @param optionQuotes The individual option quotes, sorted by maturity and
* strike.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @param strikeInterpolationMethod The interpolation method in strike
* direction.
* @param strikeExtrapolationMethod The extrapolation method in strike
* direction.
* @param maturityInterpolationMethod The interpolation method in maturity
* direction.
* @param maturityExtrapolationMethod The extrapolation method in maturity
* direction.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated of(
final OptionData[] optionQuotes,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve,
final InterpolationMethod strikeInterpolationMethod,
final ExtrapolationMethod strikeExtrapolationMethod,
final InterpolationMethod maturityInterpolationMethod,
final ExtrapolationMethod maturityExtrapolationMethod) {
return new OptionSurfaceDataInterpolated(
optionQuotes,
discountCurve,
equityForwardCurve,
strikeInterpolationMethod,
strikeExtrapolationMethod,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
}
/**
* Creates an interpolated surface from unsorted individual option quotes.
* <p>
* The input quotes are sorted by maturity and by strike within maturity.
* Duplicate nodes are not allowed.
* </p>
*
* @param optionQuotes The individual option quotes.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated ofUnsorted(
final OptionData[] optionQuotes,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve) {
return new OptionSurfaceDataInterpolated(
prepareFromOptionQuotes(optionQuotes, true),
discountCurve,
equityForwardCurve,
DEFAULT_STRIKE_INTERPOLATION_METHOD,
DEFAULT_STRIKE_EXTRAPOLATION_METHOD,
DEFAULT_MATURITY_INTERPOLATION_METHOD,
DEFAULT_MATURITY_EXTRAPOLATION_METHOD
);
}
/**
* Creates an interpolated surface from unsorted individual option quotes.
* <p>
* The input quotes are sorted by maturity and by strike within maturity.
* Duplicate nodes are not allowed.
* </p>
*
* @param optionQuotes The individual option quotes.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @param strikeInterpolationMethod The interpolation method in strike
* direction.
* @param strikeExtrapolationMethod The extrapolation method in strike
* direction.
* @param maturityInterpolationMethod The interpolation method in maturity
* direction.
* @param maturityExtrapolationMethod The extrapolation method in maturity
* direction.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated ofUnsorted(
final OptionData[] optionQuotes,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve,
final InterpolationMethod strikeInterpolationMethod,
final ExtrapolationMethod strikeExtrapolationMethod,
final InterpolationMethod maturityInterpolationMethod,
final ExtrapolationMethod maturityExtrapolationMethod) {
return new OptionSurfaceDataInterpolated(
prepareFromOptionQuotes(optionQuotes, true),
discountCurve,
equityForwardCurve,
strikeInterpolationMethod,
strikeExtrapolationMethod,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
}
/**
* Creates an interpolated surface from sorted option smiles.
*
* @param smiles The option smiles, sorted by maturity and with sorted
* strikes.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated of(
final OptionSmileData[] smiles,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve) {
return new OptionSurfaceDataInterpolated(smiles, discountCurve, equityForwardCurve);
}
/**
* Creates an interpolated surface from sorted option smiles.
*
* @param smiles The option smiles, sorted by maturity and with sorted
* strikes.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @param strikeInterpolationMethod The interpolation method in strike
* direction.
* @param strikeExtrapolationMethod The extrapolation method in strike
* direction.
* @param maturityInterpolationMethod The interpolation method in maturity
* direction.
* @param maturityExtrapolationMethod The extrapolation method in maturity
* direction.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated of(
final OptionSmileData[] smiles,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve,
final InterpolationMethod strikeInterpolationMethod,
final ExtrapolationMethod strikeExtrapolationMethod,
final InterpolationMethod maturityInterpolationMethod,
final ExtrapolationMethod maturityExtrapolationMethod) {
return new OptionSurfaceDataInterpolated(
smiles,
discountCurve,
equityForwardCurve,
strikeInterpolationMethod,
strikeExtrapolationMethod,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
}
/**
* Creates an interpolated surface from unsorted option smiles.
* <p>
* The smiles are sorted by maturity and the strikes inside each smile are
* sorted increasingly. Duplicate maturities or duplicate strikes inside a
* smile are not allowed.
* </p>
*
* @param smiles The option smiles.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated ofUnsorted(
final OptionSmileData[] smiles,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve) {
return new OptionSurfaceDataInterpolated(
prepareFromSmiles(smiles, true),
discountCurve,
equityForwardCurve,
DEFAULT_STRIKE_INTERPOLATION_METHOD,
DEFAULT_STRIKE_EXTRAPOLATION_METHOD,
DEFAULT_MATURITY_INTERPOLATION_METHOD,
DEFAULT_MATURITY_EXTRAPOLATION_METHOD
);
}
/**
* Creates an interpolated surface from unsorted option smiles.
* <p>
* The smiles are sorted by maturity and the strikes inside each smile are
* sorted increasingly. Duplicate maturities or duplicate strikes inside a
* smile are not allowed.
* </p>
*
* @param smiles The option smiles.
* @param discountCurve The discount curve.
* @param equityForwardCurve The equity forward curve.
* @param strikeInterpolationMethod The interpolation method in strike
* direction.
* @param strikeExtrapolationMethod The extrapolation method in strike
* direction.
* @param maturityInterpolationMethod The interpolation method in maturity
* direction.
* @param maturityExtrapolationMethod The extrapolation method in maturity
* direction.
* @return The interpolated option surface.
*/
public static OptionSurfaceDataInterpolated ofUnsorted(
final OptionSmileData[] smiles,
final DiscountCurve discountCurve,
final DiscountCurve equityForwardCurve,
final InterpolationMethod strikeInterpolationMethod,
final ExtrapolationMethod strikeExtrapolationMethod,
final InterpolationMethod maturityInterpolationMethod,
final ExtrapolationMethod maturityExtrapolationMethod) {
return new OptionSurfaceDataInterpolated(
prepareFromSmiles(smiles, true),
discountCurve,
equityForwardCurve,
strikeInterpolationMethod,
strikeExtrapolationMethod,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
}
/**
* Returns the value in the default quoting convention of the surface.
*
* @param maturity The option maturity.
* @param strike The option strike.
* @return The interpolated value.
*/
@Override
public double getValue(final double maturity, final double strike) {
return getValue(maturity, strike, getQuotingConvention());
}
/**
* Returns the value in the requested quoting convention.
*
* @param maturity The option maturity.
* @param strike The option strike.
* @param quotingConvention The requested quoting convention.
* @return The interpolated and, if necessary, converted value.
*/
@Override
public double getValue(
final double maturity,
final double strike,
final QuotingConvention quotingConvention) {
return getValue(null, maturity, strike, quotingConvention);
}
/**
* Returns the value in the requested quoting convention.
* <p>
* The model argument is currently not used, since the discount curve and
* the
* equity forward curve are stored in the surface.
* </p>
*
* @param model An analytic model.
* @param maturity The option maturity.
* @param strike The option strike.
* @param quotingConvention The requested quoting convention.
* @return The interpolated and, if necessary, converted value.
*/
@Override
public double getValue(
final AnalyticModel model,
final double maturity,
final double strike,
final QuotingConvention quotingConvention) {
final double valueInSurfaceConvention = getInterpolatedValue(maturity, strike);
if (quotingConvention.equals(getQuotingConvention())) {
return valueInSurfaceConvention;
}
return convertFromSurfaceConvention(maturity, strike, valueInSurfaceConvention, quotingConvention);
}
/**
* Returns the interpolated value in the surface's native quoting
* convention.
*
* @param maturity The option maturity.
* @param strike The option strike.
* @return The interpolated value in the surface's native quoting
* convention.
*/
public double getInterpolatedValue(final double maturity, final double strike) {
final double[] valuesAtStrike = new double[uniqueMaturities.length];
for (int maturityIndex = 0; maturityIndex < uniqueMaturities.length; maturityIndex++) {
valuesAtStrike[maturityIndex] = smileInterpolators[maturityIndex].getValue(strike);
}
if (uniqueMaturities.length == 1) {
return valuesAtStrike[0];
}
final RationalFunctionInterpolation maturityInterpolator =
new RationalFunctionInterpolation(
uniqueMaturities,
valuesAtStrike,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
return maturityInterpolator.getValue(maturity);
}
/**
* Creates a clone of this surface with another quoting convention.
* <p>
* The conversion is performed at the original surface nodes. The
* interpolation
* settings are preserved.
* </p>
*
* @param newQuotingConvention The new quoting convention.
* @return A surface with the requested quoting convention.
*/
public OptionSurfaceDataInterpolated getCloneForQuotingConvention(final QuotingConvention newQuotingConvention) {
Objects.requireNonNull(newQuotingConvention, "newQuotingConvention");
if (newQuotingConvention.equals(getQuotingConvention())) {
return this;
}
final List<OptionSmileData> convertedSmiles = new ArrayList<>();
for (final OptionSmileData smile : smiles) {
final double maturity = smile.getMaturity();
final double[] strikes = smile.getStrikes().clone();
final double[] convertedValues = new double[strikes.length];
for (int strikeIndex = 0; strikeIndex < strikes.length; strikeIndex++) {
final double strike = strikes[strikeIndex];
final double originalValue = smile.getSmile().get(strike).getValue();
convertedValues[strikeIndex] =
convertFromSurfaceConvention(
maturity,
strike,
originalValue,
newQuotingConvention
);
}
convertedSmiles.add(
new OptionSmileData(
smile.getUnderlying(),
smile.getReferenceDate(),
strikes,
maturity,
convertedValues,
newQuotingConvention
)
);
}
return new OptionSurfaceDataInterpolated(
convertedSmiles.toArray(new OptionSmileData[0]),
getDiscountCurve(),
getEquityForwardCurve(),
strikeInterpolationMethod,
strikeExtrapolationMethod,
maturityInterpolationMethod,
maturityExtrapolationMethod
);
}
/**
* Returns the sorted smiles used by this surface.
*
* @return The sorted smiles.
*/
public OptionSmileData[] getSmiles() {
return smiles.clone();
}
/**
* Returns the flattened node maturities.
*
* @return The node maturities.
*/
public double[] getNodeMaturities() {
return nodeMaturities.clone();
}
/**
* Returns the flattened node strikes.
*
* @return The node strikes.
*/
public double[] getNodeStrikes() {
return nodeStrikes.clone();
}
/**
* Returns the flattened node values in the native quoting convention.
*
* @return The node values.
*/
public double[] getNodeValues() {
return nodeValues.clone();
}
/**
* Returns the sorted unique maturities.
*
* @return The sorted unique maturities.
*/
@Override
public double[] getMaturities() {
return uniqueMaturities.clone();
}
/**
* Returns the interpolation method in strike direction.
*
* @return The strike interpolation method.
*/
public InterpolationMethod getStrikeInterpolationMethod() {
return strikeInterpolationMethod;
}
/**
* Returns the extrapolation method in strike direction.
*
* @return The strike extrapolation method.
*/
public ExtrapolationMethod getStrikeExtrapolationMethod() {
return strikeExtrapolationMethod;
}
/**
* Returns the interpolation method in maturity direction.
*
* @return The maturity interpolation method.
*/
public InterpolationMethod getMaturityInterpolationMethod() {
return maturityInterpolationMethod;
}
/**
* Returns the extrapolation method in maturity direction.
*
* @return The maturity extrapolation method.
*/
public ExtrapolationMethod getMaturityExtrapolationMethod() {
return maturityExtrapolationMethod;
}
private double convertFromSurfaceConvention(
final double maturity,
final double strike,
final double value,
final QuotingConvention targetConvention) {
final QuotingConvention sourceConvention = getQuotingConvention();
if (sourceConvention.equals(targetConvention)) {
return value;
}
final double forward = getEquityForwardCurve().getValue(maturity);
final double discountFactor = getDiscountCurve().getValue(maturity);
if (sourceConvention == QuotingConvention.VOLATILITYLOGNORMAL
&& targetConvention == QuotingConvention.PRICE) {
return AnalyticFormulas.blackScholesGeneralizedOptionValue(
forward,
value,
maturity,
strike,
discountFactor
);
}
if (sourceConvention == QuotingConvention.PRICE
&& targetConvention == QuotingConvention.VOLATILITYLOGNORMAL) {
return AnalyticFormulas.blackScholesOptionImpliedVolatility(
forward,
maturity,
strike,
discountFactor,
value
);
}
if (sourceConvention == QuotingConvention.VOLATILITYNORMAL
&& targetConvention == QuotingConvention.PRICE) {
return AnalyticFormulas.bachelierOptionValue(
forward,
value,
maturity,
strike,
discountFactor
);
}
if (sourceConvention == QuotingConvention.PRICE
&& targetConvention == QuotingConvention.VOLATILITYNORMAL) {
return AnalyticFormulas.bachelierOptionImpliedVolatility(
forward,
maturity,
strike,
discountFactor,
value
);
}
if (sourceConvention == QuotingConvention.VOLATILITYLOGNORMAL
&& targetConvention == QuotingConvention.VOLATILITYNORMAL) {
final double price = AnalyticFormulas.blackScholesGeneralizedOptionValue(
forward,
value,
maturity,
strike,
discountFactor
);
return AnalyticFormulas.bachelierOptionImpliedVolatility(
forward,
maturity,
strike,
discountFactor,
price
);
}
if (sourceConvention == QuotingConvention.VOLATILITYNORMAL
&& targetConvention == QuotingConvention.VOLATILITYLOGNORMAL) {
final double price = AnalyticFormulas.bachelierOptionValue(
forward,
value,
maturity,
strike,
discountFactor
);
return AnalyticFormulas.blackScholesOptionImpliedVolatility(
forward,
maturity,
strike,
discountFactor,
price
);
}
throw new IllegalArgumentException(
"Unsupported quoting convention conversion from "
+ sourceConvention + " to " + targetConvention
);
}
private SmileInterpolator[] buildSmileInterpolators(final OptionSmileData[] smiles) {
final SmileInterpolator[] interpolators = new SmileInterpolator[smiles.length];
for (int maturityIndex = 0; maturityIndex < smiles.length; maturityIndex++) {
final OptionSmileData smile = smiles[maturityIndex];
final double[] strikes = smile.getStrikes().clone();
final double[] values = new double[strikes.length];
for (int strikeIndex = 0; strikeIndex < strikes.length; strikeIndex++) {
values[strikeIndex] = smile.getSmile().get(strikes[strikeIndex]).getValue();
}
interpolators[maturityIndex] =
new SmileInterpolator(
strikes,
values,
strikeInterpolationMethod,
strikeExtrapolationMethod
);
}
return interpolators;
}
private static PreparedSurface prepareFromOptionQuotes(
final OptionData[] optionQuotes,
final boolean sort) {
checkOptionQuotes(optionQuotes);
final OptionData[] quotes = optionQuotes.clone();
if (sort) {
Arrays.sort(quotes, OPTION_QUOTE_COMPARATOR);
} else {
checkOptionQuotesSorted(quotes);
}
checkOptionQuotesConsistencyAndDuplicates(quotes);
final List<OptionSmileData> smiles = new ArrayList<>();
int startIndex = 0;
while (startIndex < quotes.length) {
final double maturity = quotes[startIndex].getMaturity();
int endIndex = startIndex + 1;
while (endIndex < quotes.length
&& Double.compare(quotes[endIndex].getMaturity(), maturity) == 0) {
endIndex++;
}
final int numberOfQuotesInSmile = endIndex - startIndex;
final double[] strikes = new double[numberOfQuotesInSmile];
final double[] values = new double[numberOfQuotesInSmile];
for (int quoteIndex = startIndex; quoteIndex < endIndex; quoteIndex++) {
final int smileIndex = quoteIndex - startIndex;
strikes[smileIndex] = quotes[quoteIndex].getStrike();
values[smileIndex] = quotes[quoteIndex].getValue();
}
smiles.add(
new OptionSmileData(
quotes[startIndex].getUnderlying(),
quotes[startIndex].getReferenceDate(),
strikes,
maturity,
values,
quotes[startIndex].getConvention()
)
);
startIndex = endIndex;
}
return prepareFromSmiles(smiles.toArray(new OptionSmileData[0]), false);
}
private static PreparedSurface prepareFromSmiles(
final OptionSmileData[] inputSmiles,
final boolean sort) {
checkSmiles(inputSmiles);
final OptionSmileData[] smiles = inputSmiles.clone();
if (sort) {
Arrays.sort(smiles, OPTION_SMILE_COMPARATOR);
} else {
checkSmilesSorted(smiles);
}
checkSmilesConsistencyAndDuplicates(smiles);
final OptionSmileData[] normalizedSmiles = new OptionSmileData[smiles.length];
for (int maturityIndex = 0; maturityIndex < smiles.length; maturityIndex++) {
normalizedSmiles[maturityIndex] = normalizeSmile(smiles[maturityIndex], sort);
}
checkSmilesConsistencyAndDuplicates(normalizedSmiles);
return flatten(normalizedSmiles);
}
private static OptionSmileData normalizeSmile(
final OptionSmileData smile,
final boolean sort) {
final double[] strikes = smile.getStrikes().clone();
if (sort) {
Arrays.sort(strikes);
} else {
checkStrictlyIncreasing(strikes, "strikes");
}
checkStrictlyIncreasing(strikes, "strikes");
final double[] values = new double[strikes.length];
for (int strikeIndex = 0; strikeIndex < strikes.length; strikeIndex++) {
final OptionData option = smile.getSmile().get(strikes[strikeIndex]);
if (option == null) {
throw new IllegalArgumentException(
"Smile for maturity " + smile.getMaturity()
+ " does not contain a quote for strike " + strikes[strikeIndex]
);
}
values[strikeIndex] = option.getValue();
}
return new OptionSmileData(
smile.getUnderlying(),
smile.getReferenceDate(),
strikes,
smile.getMaturity(),
values,
smile.getSmile().get(strikes[0]).getConvention()
);
}
private static PreparedSurface flatten(final OptionSmileData[] smiles) {
int numberOfNodes = 0;
for (final OptionSmileData smile : smiles) {
numberOfNodes += smile.getStrikes().length;
}
if (numberOfNodes < 2) {
throw new IllegalArgumentException("The surface must contain at least two option quotes.");
}
final double[] nodeMaturities = new double[numberOfNodes];
final double[] nodeStrikes = new double[numberOfNodes];
final double[] nodeValues = new double[numberOfNodes];
final double[] uniqueMaturities = new double[smiles.length];
int nodeIndex = 0;
for (int maturityIndex = 0; maturityIndex < smiles.length; maturityIndex++) {
final OptionSmileData smile = smiles[maturityIndex];
final double maturity = smile.getMaturity();
final double[] strikes = smile.getStrikes();
uniqueMaturities[maturityIndex] = maturity;
for (final double strike : strikes) {
nodeMaturities[nodeIndex] = maturity;
nodeStrikes[nodeIndex] = strike;
nodeValues[nodeIndex] = smile.getSmile().get(strike).getValue();
nodeIndex++;
}
}
return new PreparedSurface(
smiles,
nodeMaturities,
nodeStrikes,
nodeValues,
uniqueMaturities
);
}
private static void checkOptionQuotes(final OptionData[] optionQuotes) {
if (optionQuotes == null || optionQuotes.length == 0) {
throw new IllegalArgumentException("The option quote array must not be null or empty.");
}
for (int quoteIndex = 0; quoteIndex < optionQuotes.length; quoteIndex++) {
final OptionData quote = optionQuotes[quoteIndex];
if (quote == null) {
throw new IllegalArgumentException("The option quote at index " + quoteIndex + " is null.");
}
checkFinite(quote.getMaturity(), "maturity");
checkFinite(quote.getStrike(), "strike");
checkFinite(quote.getValue(), "value");
if (quote.getMaturity() < 0.0) {
throw new IllegalArgumentException("Option maturity must be non-negative.");
}
if (quote.getStrike() <= 0.0) {
throw new IllegalArgumentException("Option strike must be positive.");
}
}
}
private static void checkOptionQuotesSorted(final OptionData[] quotes) {
for (int quoteIndex = 1; quoteIndex < quotes.length; quoteIndex++) {
if (OPTION_QUOTE_COMPARATOR.compare(quotes[quoteIndex - 1], quotes[quoteIndex]) > 0) {
throw new IllegalArgumentException(
"Option quotes must be sorted by maturity and strike. "
+ "Use ofUnsorted(...) for unsorted input."
);
}
}
}
private static void checkOptionQuotesConsistencyAndDuplicates(final OptionData[] quotes) {
final OptionData firstQuote = quotes[0];
for (int quoteIndex = 0; quoteIndex < quotes.length; quoteIndex++) {
final OptionData quote = quotes[quoteIndex];
if (!Objects.equals(quote.getUnderlying(), firstQuote.getUnderlying())) {
throw new IllegalArgumentException("All option quotes must have the same underlying.");
}
if (!Objects.equals(quote.getReferenceDate(), firstQuote.getReferenceDate())) {
throw new IllegalArgumentException("All option quotes must have the same reference date.");
}
if (!Objects.equals(quote.getConvention(), firstQuote.getConvention())) {
throw new IllegalArgumentException("All option quotes must have the same quoting convention.");
}
if (quoteIndex > 0
&& Double.compare(quotes[quoteIndex - 1].getMaturity(), quote.getMaturity()) == 0
&& Double.compare(quotes[quoteIndex - 1].getStrike(), quote.getStrike()) == 0) {
throw new IllegalArgumentException(
"Duplicate option quote for maturity "
+ quote.getMaturity()
+ " and strike "
+ quote.getStrike()
);
}
}
}
private static void checkSmiles(final OptionSmileData[] smiles) {
if (smiles == null || smiles.length == 0) {
throw new IllegalArgumentException("The option smile array must not be null or empty.");
}
for (int smileIndex = 0; smileIndex < smiles.length; smileIndex++) {
final OptionSmileData smile = smiles[smileIndex];
if (smile == null) {
throw new IllegalArgumentException("The option smile at index " + smileIndex + " is null.");
}
checkFinite(smile.getMaturity(), "maturity");
if (smile.getMaturity() < 0.0) {
throw new IllegalArgumentException("Option maturity must be non-negative.");
}
final double[] strikes = smile.getStrikes();
if (strikes == null || strikes.length == 0) {
throw new IllegalArgumentException("Each smile must contain at least one strike.");
}
for (final double strike : strikes) {
checkFinite(strike, "strike");
if (strike <= 0.0) {
throw new IllegalArgumentException("Option strike must be positive.");
}
final OptionData option = smile.getSmile().get(strike);
if (option == null) {
throw new IllegalArgumentException(
"Smile for maturity " + smile.getMaturity()
+ " does not contain a quote for strike " + strike
);
}
checkFinite(option.getValue(), "value");
}
}
}
private static void checkSmilesSorted(final OptionSmileData[] smiles) {
for (int smileIndex = 1; smileIndex < smiles.length; smileIndex++) {
if (smiles[smileIndex].getMaturity() < smiles[smileIndex - 1].getMaturity()) {
throw new IllegalArgumentException(
"Option smiles must be sorted by maturity. "
+ "Use ofUnsorted(...) for unsorted input."
);
}
}
for (final OptionSmileData smile : smiles) {
checkStrictlyIncreasing(smile.getStrikes(), "strikes");
}
}
private static void checkSmilesConsistencyAndDuplicates(final OptionSmileData[] smiles) {
final OptionSmileData firstSmile = smiles[0];
final String underlying = firstSmile.getUnderlying();
final LocalDate referenceDate = firstSmile.getReferenceDate();
final QuotingConvention convention = firstSmile.getSmile().get(firstSmile.getStrikes()[0]).getConvention();
for (int smileIndex = 0; smileIndex < smiles.length; smileIndex++) {
final OptionSmileData smile = smiles[smileIndex];
if (!Objects.equals(smile.getUnderlying(), underlying)) {
throw new IllegalArgumentException("All smiles must have the same underlying.");
}
if (!Objects.equals(smile.getReferenceDate(), referenceDate)) {
throw new IllegalArgumentException("All smiles must have the same reference date.");
}
if (smileIndex > 0
&& Double.compare(smiles[smileIndex - 1].getMaturity(), smile.getMaturity()) == 0) {
throw new IllegalArgumentException(
"Duplicate option smile for maturity " + smile.getMaturity()
);
}
final double[] strikes = smile.getStrikes();
checkStrictlyIncreasing(strikes, "strikes");
for (final double strike : strikes) {
final OptionData option = smile.getSmile().get(strike);
if (!Objects.equals(option.getConvention(), convention)) {
throw new IllegalArgumentException("All smiles must have the same quoting convention.");
}
if (!Objects.equals(option.getUnderlying(), underlying)) {
throw new IllegalArgumentException("All option quotes inside smiles must have the same underlying.");
}
if (!Objects.equals(option.getReferenceDate(), referenceDate)) {
throw new IllegalArgumentException("All option quotes inside smiles must have the same reference date.");
}
if (Double.compare(option.getMaturity(), smile.getMaturity()) != 0) {
throw new IllegalArgumentException(
"Option quote maturity does not match smile maturity."
);
}
}
}
}
private static void checkStrictlyIncreasing(final double[] points, final String name) {
if (points == null || points.length == 0) {
throw new IllegalArgumentException(name + " must not be null or empty.");
}
for (int index = 1; index < points.length; index++) {
if (points[index] <= points[index - 1]) {
throw new IllegalArgumentException(
name + " must be strictly increasing. Received: "
+ Arrays.toString(points)
);
}
}
}
private static void checkFinite(final double value, final String name) {
if (Double.isNaN(value) || Double.isInfinite(value)) {
throw new IllegalArgumentException(name + " must be finite.");
}
}
private static final class PreparedSurface {
/**
* The smiles.
*/
private final OptionSmileData[] smiles;
/**
* The node maturities.
*/
private final double[] nodeMaturities;
/**
* The node strikes.
*/
private final double[] nodeStrikes;
/**
* The node values.
*/
private final double[] nodeValues;
/**
* The unique maturities.
*/
private final double[] uniqueMaturities;
private PreparedSurface(
final OptionSmileData[] smiles,
final double[] nodeMaturities,
final double[] nodeStrikes,
final double[] nodeValues,
final double[] uniqueMaturities) {
this.smiles = smiles;
this.nodeMaturities = nodeMaturities;
this.nodeStrikes = nodeStrikes;
this.nodeValues = nodeValues;
this.uniqueMaturities = uniqueMaturities;
}
}
private static final class SmileInterpolator {
/**
* The strikes.
*/
private final double[] strikes;
/**
* The values.
*/
private final double[] values;
/**
* The interpolation.
*/
private final RationalFunctionInterpolation interpolation;
private SmileInterpolator(
final double[] strikes,
final double[] values,
final InterpolationMethod interpolationMethod,
final ExtrapolationMethod extrapolationMethod) {
this.strikes = strikes.clone();
this.values = values.clone();
if (strikes.length != values.length) {
throw new IllegalArgumentException("Number of strikes and values must agree.");
}
if (strikes.length == 1) {
this.interpolation = null;
} else {
this.interpolation =
new RationalFunctionInterpolation(
this.strikes,
this.values,
interpolationMethod,
extrapolationMethod
);
}
}
private double getValue(final double strike) {
if (values.length == 1) {
return values[0];
}
return interpolation.getValue(strike);
}
}
}