DigitalBasketOption.java
package net.finmath.finitedifference.assetderivativevaluation.products;
import java.util.Arrays;
import net.finmath.finitedifference.assetderivativevaluation.models.FiniteDifferenceEquityModel;
import net.finmath.finitedifference.solvers.FDMSolver;
import net.finmath.finitedifference.solvers.FDMSolverFactory;
import net.finmath.modelling.EuropeanExercise;
import net.finmath.modelling.Exercise;
import net.finmath.modelling.products.CallOrPut;
/**
* Finite-difference valuation of a European cash-or-nothing digital option on
* two assets.
*
* <p>
* The class unifies three practically relevant two-asset digital payoffs:
* </p>
*
* <ul>
* <li>linear-combination digital,</li>
* <li>best-of digital,</li>
* <li>worst-of digital.</li>
* </ul>
*
* <p>
* Let {@code S1} and {@code S2} denote the two asset values at maturity
* {@code T}, let {@code K} be the strike, let {@code C >= 0} be the cash
* payoff, and let {@code \omega \in \{+1,-1\}} denote the call/put sign.
* </p>
*
* <p>
* In {@link BasketDigitalType#LINEAR_COMBINATION} mode, with signed quantities
* {@code q1} and {@code q2}, the payoff is
* </p>
*
* <p>
* <i>
* C \mathbf{1}_{\{\omega (q_1 S_1(T) + q_2 S_2(T) - K) > 0\}}.
* </i>
* </p>
*
* <p>
* In {@link BasketDigitalType#BEST_OF} mode, the payoff is
* </p>
*
* <p>
* <i>
* C \mathbf{1}_{\{\omega (\max(S_1(T),S_2(T)) - K) > 0\}}.
* </i>
* </p>
*
* <p>
* In {@link BasketDigitalType#WORST_OF} mode, the payoff is
* </p>
*
* <p>
* <i>
* C \mathbf{1}_{\{\omega (\min(S_1(T),S_2(T)) - K) > 0\}}.
* </i>
* </p>
*
* <p>
* The current implementation supports only European exercise and requires a
* two-dimensional model state.
* </p>
*
* @author Alessandro Gnoatto
*/
public class DigitalBasketOption implements FiniteDifferenceEquityProduct {
/**
* Type of two-asset digital payoff.
*/
public enum BasketDigitalType {
/**
* The linear combination.
*/
LINEAR_COMBINATION,
/**
* The best of.
*/
BEST_OF,
/**
* The worst of.
*/
WORST_OF
}
/**
* The underlying names.
*/
private final String[] underlyingNames;
/**
* The maturity.
*/
private final double maturity;
/**
* The basket digital type.
*/
private final BasketDigitalType basketDigitalType;
/**
* The quantities.
*/
private final double[] quantities;
/**
* The strike.
*/
private final double strike;
/**
* The cash payoff.
*/
private final double cashPayoff;
/**
* The call or put.
*/
private final CallOrPut callOrPut;
/**
* The exercise.
*/
private final Exercise exercise;
/**
* Creates a digital basket option.
*
* @param underlyingNames Names of the underlyings. May be {@code null}. If
* provided, the array length must be 2.
* @param maturity Maturity.
* @param basketDigitalType Payoff type.
* @param quantities Signed quantities for
* {@link BasketDigitalType#LINEAR_COMBINATION}. Ignored otherwise and
* may be {@code null}.
* @param strike Strike.
* @param cashPayoff Cash payoff.
* @param callOrPut Option type.
*/
public DigitalBasketOption(
final String[] underlyingNames,
final double maturity,
final BasketDigitalType basketDigitalType,
final double[] quantities,
final double strike,
final double cashPayoff,
final CallOrPut callOrPut) {
if (basketDigitalType == null) {
throw new IllegalArgumentException("basketDigitalType must not be null.");
}
if (callOrPut == null) {
throw new IllegalArgumentException("callOrPut must not be null.");
}
if (maturity < 0.0) {
throw new IllegalArgumentException("maturity must be non-negative.");
}
if (cashPayoff < 0.0) {
throw new IllegalArgumentException("cashPayoff must be non-negative.");
}
if (underlyingNames != null && underlyingNames.length != 2) {
throw new IllegalArgumentException("underlyingNames must have length 2.");
}
if (basketDigitalType == BasketDigitalType.LINEAR_COMBINATION) {
if (quantities == null || quantities.length != 2) {
throw new IllegalArgumentException(
"quantities must have length 2 for LINEAR_COMBINATION mode.");
}
boolean allZero = true;
for (final double quantity : quantities) {
if (Math.abs(quantity) > 0.0) {
allZero = false;
break;
}
}
if (allZero) {
throw new IllegalArgumentException("At least one quantity must be non-zero.");
}
}
this.underlyingNames = underlyingNames != null ? underlyingNames.clone() : null;
this.maturity = maturity;
this.basketDigitalType = basketDigitalType;
this.quantities = quantities != null ? quantities.clone() : null;
this.strike = strike;
this.cashPayoff = cashPayoff;
this.callOrPut = callOrPut;
this.exercise = new EuropeanExercise(maturity);
}
/**
* Creates a digital basket option.
*
* @param maturity Maturity.
* @param basketDigitalType Payoff type.
* @param quantities Signed quantities for
* {@link BasketDigitalType#LINEAR_COMBINATION}. Ignored otherwise and
* may be {@code null}.
* @param strike Strike.
* @param cashPayoff Cash payoff.
* @param callOrPut Option type.
*/
public DigitalBasketOption(
final double maturity,
final BasketDigitalType basketDigitalType,
final double[] quantities,
final double strike,
final double cashPayoff,
final CallOrPut callOrPut) {
this(null, maturity, basketDigitalType, quantities, strike, cashPayoff, callOrPut);
}
/**
* Creates a linear-combination digital option.
*
* @param underlyingNames Names of the underlyings.
* @param maturity Maturity.
* @param quantities Signed quantities.
* @param strike Strike.
* @param cashPayoff Cash payoff.
* @param callOrPut Option type.
* @return The product.
*/
public static DigitalBasketOption linearCombination(
final String[] underlyingNames,
final double maturity,
final double[] quantities,
final double strike,
final double cashPayoff,
final CallOrPut callOrPut) {
return new DigitalBasketOption(
underlyingNames,
maturity,
BasketDigitalType.LINEAR_COMBINATION,
quantities,
strike,
cashPayoff,
callOrPut
);
}
/**
* Creates a best-of digital option.
*
* @param underlyingNames Names of the underlyings.
* @param maturity Maturity.
* @param strike Strike.
* @param cashPayoff Cash payoff.
* @param callOrPut Option type.
* @return The product.
*/
public static DigitalBasketOption bestOf(
final String[] underlyingNames,
final double maturity,
final double strike,
final double cashPayoff,
final CallOrPut callOrPut) {
return new DigitalBasketOption(
underlyingNames,
maturity,
BasketDigitalType.BEST_OF,
null,
strike,
cashPayoff,
callOrPut
);
}
/**
* Creates a worst-of digital option.
*
* @param underlyingNames Names of the underlyings.
* @param maturity Maturity.
* @param strike Strike.
* @param cashPayoff Cash payoff.
* @param callOrPut Option type.
* @return The product.
*/
public static DigitalBasketOption worstOf(
final String[] underlyingNames,
final double maturity,
final double strike,
final double cashPayoff,
final CallOrPut callOrPut) {
return new DigitalBasketOption(
underlyingNames,
maturity,
BasketDigitalType.WORST_OF,
null,
strike,
cashPayoff,
callOrPut
);
}
@Override
public double[] getValue(final double evaluationTime, final FiniteDifferenceEquityModel model) {
validateModel(model);
final FDMSolver solver = FDMSolverFactory.createSolver(model, this, exercise);
return solver.getValue(
evaluationTime,
maturity,
this::terminalPayoff
);
}
@Override
public double[][] getValues(final FiniteDifferenceEquityModel model) {
validateModel(model);
final FDMSolver solver = FDMSolverFactory.createSolver(model, this, exercise);
return solver.getValues(
maturity,
this::terminalPayoff
);
}
/**
* Returns the terminal payoff.
*
* @param firstAssetValue Value of the first asset.
* @param secondAssetValue Value of the second asset.
* @return The terminal payoff.
*/
private double terminalPayoff(final double firstAssetValue, final double secondAssetValue) {
final double underlyingValue;
switch (basketDigitalType) {
case LINEAR_COMBINATION:
underlyingValue = quantities[0] * firstAssetValue + quantities[1] * secondAssetValue;
break;
case BEST_OF:
underlyingValue = Math.max(firstAssetValue, secondAssetValue);
break;
case WORST_OF:
underlyingValue = Math.min(firstAssetValue, secondAssetValue);
break;
default:
throw new IllegalStateException("Unsupported basket digital type.");
}
return callOrPut.toInteger() * (underlyingValue - strike) > 0.0 ? cashPayoff : 0.0;
}
/**
* Validates model compatibility.
*
* @param model The finite-difference model.
*/
private void validateModel(final FiniteDifferenceEquityModel model) {
if (model == null) {
throw new IllegalArgumentException("model must not be null.");
}
if (model.getSpaceTimeDiscretization().getNumberOfSpaceGrids() != 2) {
throw new IllegalArgumentException(
"DigitalBasketOption currently requires a two-dimensional space discretization.");
}
if (model.getInitialValue() == null || model.getInitialValue().length != 2) {
throw new IllegalArgumentException(
"DigitalBasketOption currently requires a two-dimensional model state.");
}
}
/**
* Returns the names of the underlyings.
*
* @return The names of the underlyings, or {@code null} if unspecified.
*/
public String[] getUnderlyingNames() {
return underlyingNames != null ? underlyingNames.clone() : null;
}
/**
* Returns the maturity.
*
* @return The maturity.
*/
public double getMaturity() {
return maturity;
}
/**
* Returns the payoff type.
*
* @return The payoff type.
*/
public BasketDigitalType getBasketDigitalType() {
return basketDigitalType;
}
/**
* Returns the signed quantities.
*
* @return The signed quantities, or {@code null} if not applicable.
*/
public double[] getQuantities() {
return quantities != null ? quantities.clone() : null;
}
/**
* Returns the strike.
*
* @return The strike.
*/
public double getStrike() {
return strike;
}
/**
* Returns the cash payoff.
*
* @return The cash payoff.
*/
public double getCashPayoff() {
return cashPayoff;
}
/**
* Returns the option type.
*
* @return The option type.
*/
public CallOrPut getCallOrPut() {
return callOrPut;
}
/**
* Returns the exercise specification.
*
* @return The exercise specification.
*/
public Exercise getExercise() {
return exercise;
}
@Override
public String toString() {
return "DigitalBasketOption [maturity=" + maturity
+ ", basketDigitalType=" + basketDigitalType
+ ", quantities=" + Arrays.toString(quantities)
+ ", strike=" + strike
+ ", cashPayoff=" + cashPayoff
+ ", callOrPut=" + callOrPut
+ "]";
}
}