DigitalOption.java
package net.finmath.finitedifference.assetderivativevaluation.products;
import java.util.function.DoubleUnaryOperator;
import net.finmath.finitedifference.FiniteDifferenceExerciseUtil;
import net.finmath.finitedifference.assetderivativevaluation.models.FDMBachelierModel;
import net.finmath.finitedifference.assetderivativevaluation.models.FDMBlackScholesModel;
import net.finmath.finitedifference.assetderivativevaluation.models.FDMCevModel;
import net.finmath.finitedifference.assetderivativevaluation.models.FiniteDifferenceEquityModel;
import net.finmath.finitedifference.grids.Grid;
import net.finmath.finitedifference.grids.SpaceTimeDiscretization;
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;
import net.finmath.modelling.products.DigitalPayoffType;
import net.finmath.time.TimeDiscretization;
/**
* Digital option supporting cash-or-nothing and asset-or-nothing payoffs.
*
* <p>
* The class supports multiple exercise styles through the generic
* {@link Exercise} field. European exercise is the default when no explicit
* exercise object is provided.
* </p>
*
* <p>
* For one-dimensional models, the terminal payoff is initialized using
* cell-averaged values in order to reduce grid bias caused by the payoff
* discontinuity at the strike.
* </p>
*
* <p>
* For Bermudan and American exercise in one-dimensional models, the maturity
* layer remains cell-averaged, while exercise projection remains pointwise.
* </p>
*
* <p>
* For the current two-dimensional models, the payoff is initialized pointwise
* on the first state variable, that is, on the asset dimension.
* </p>
*
* @author Alessandro Gnoatto
*/
public class DigitalOption implements FiniteDifferenceEquityProduct {
/**
* The underlying name.
*/
private final String underlyingName;
/**
* The maturity.
*/
private final double maturity;
/**
* The strike.
*/
private final double strike;
/**
* The call or put sign.
*/
private final CallOrPut callOrPutSign;
/**
* The digital payoff type.
*/
private final DigitalPayoffType digitalPayoffType;
/**
* The cash payoff.
*/
private final double cashPayoff;
/**
* The exercise.
*/
private final Exercise exercise;
/**
* Creates a digital option with explicit exercise specification.
*
* @param underlyingName Name of the underlying.
* @param maturity Option maturity.
* @param strike Option strike.
* @param callOrPutSign Call or put flag.
* @param digitalPayoffType Type of digital payoff.
* @param cashPayoff Cash payoff for cash-or-nothing products.
* @param exercise Exercise specification.
*/
public DigitalOption(
final String underlyingName,
final double maturity,
final double strike,
final CallOrPut callOrPutSign,
final DigitalPayoffType digitalPayoffType,
final double cashPayoff,
final Exercise exercise) {
if (callOrPutSign == null) {
throw new IllegalArgumentException("Option type must not be null.");
}
if (digitalPayoffType == null) {
throw new IllegalArgumentException("Digital payoff type must not be null.");
}
if (exercise == null) {
throw new IllegalArgumentException("Exercise must not be null.");
}
if (maturity < 0.0) {
throw new IllegalArgumentException("Maturity must be non-negative.");
}
if (digitalPayoffType == DigitalPayoffType.CASH_OR_NOTHING && cashPayoff < 0.0) {
throw new IllegalArgumentException("Cash payoff must be non-negative.");
}
this.underlyingName = underlyingName;
this.maturity = maturity;
this.strike = strike;
this.callOrPutSign = callOrPutSign;
this.digitalPayoffType = digitalPayoffType;
this.cashPayoff = digitalPayoffType == DigitalPayoffType.CASH_OR_NOTHING ? cashPayoff : Double.NaN;
this.exercise = exercise;
}
/**
* Creates a European digital option.
*
* @param underlyingName Name of the underlying.
* @param maturity Option maturity.
* @param strike Option strike.
* @param callOrPutSign Call or put flag.
* @param digitalPayoffType Type of digital payoff.
* @param cashPayoff Cash payoff for cash-or-nothing products.
*/
public DigitalOption(
final String underlyingName,
final double maturity,
final double strike,
final CallOrPut callOrPutSign,
final DigitalPayoffType digitalPayoffType,
final double cashPayoff) {
this(
underlyingName,
maturity,
strike,
callOrPutSign,
digitalPayoffType,
cashPayoff,
new EuropeanExercise(maturity)
);
}
/**
* Creates a digital option with explicit exercise specification.
*
* @param maturity Option maturity.
* @param strike Option strike.
* @param callOrPutSign Call or put flag.
* @param digitalPayoffType Type of digital payoff.
* @param cashPayoff Cash payoff for cash-or-nothing products.
* @param exercise Exercise specification.
*/
public DigitalOption(
final double maturity,
final double strike,
final CallOrPut callOrPutSign,
final DigitalPayoffType digitalPayoffType,
final double cashPayoff,
final Exercise exercise) {
this(null, maturity, strike, callOrPutSign, digitalPayoffType, cashPayoff, exercise);
}
/**
* Creates a European digital option.
*
* @param maturity Option maturity.
* @param strike Option strike.
* @param callOrPutSign Call or put flag.
* @param digitalPayoffType Type of digital payoff.
* @param cashPayoff Cash payoff for cash-or-nothing products.
*/
public DigitalOption(
final double maturity,
final double strike,
final CallOrPut callOrPutSign,
final DigitalPayoffType digitalPayoffType,
final double cashPayoff) {
this(null, maturity, strike, callOrPutSign, digitalPayoffType, cashPayoff, new EuropeanExercise(maturity));
}
/**
* Creates a digital option with explicit exercise specification.
*
* @param underlyingName Name of the underlying.
* @param maturity Option maturity.
* @param strike Option strike.
* @param callOrPutSign +1 for call, -1 for put.
* @param digitalPayoffType Type of digital payoff.
* @param cashPayoff Cash payoff for cash-or-nothing products.
* @param exercise Exercise specification.
*/
public DigitalOption(
final String underlyingName,
final double maturity,
final double strike,
final double callOrPutSign,
final DigitalPayoffType digitalPayoffType,
final double cashPayoff,
final Exercise exercise) {
this(
underlyingName,
maturity,
strike,
mapCallOrPut(callOrPutSign),
digitalPayoffType,
cashPayoff,
exercise
);
}
/**
* Creates a European digital option.
*
* @param underlyingName Name of the underlying.
* @param maturity Option maturity.
* @param strike Option strike.
* @param callOrPutSign +1 for call, -1 for put.
* @param digitalPayoffType Type of digital payoff.
* @param cashPayoff Cash payoff for cash-or-nothing products.
*/
public DigitalOption(
final String underlyingName,
final double maturity,
final double strike,
final double callOrPutSign,
final DigitalPayoffType digitalPayoffType,
final double cashPayoff) {
this(
underlyingName,
maturity,
strike,
mapCallOrPut(callOrPutSign),
digitalPayoffType,
cashPayoff,
new EuropeanExercise(maturity)
);
}
@Override
public double[] getValue(final double evaluationTime, final FiniteDifferenceEquityModel model) {
final SpaceTimeDiscretization discretization = getEffectiveSpaceTimeDiscretization(model);
final FDMSolver solver = FDMSolverFactory.createSolver(model, this, discretization, exercise);
if (isOneDimensionalModel(model)) {
final double[] terminalValues = buildCellAveragedTerminalValues(discretization);
if (exercise.isEuropean()) {
return solver.getValue(evaluationTime, maturity, terminalValues);
} else {
return solver.getValue(
evaluationTime,
maturity,
terminalValues,
getExercisePayoffFunction()
);
}
}
return solver.getValue(evaluationTime, maturity, getExercisePayoffFunction());
}
@Override
public double[][] getValues(final FiniteDifferenceEquityModel model) {
final SpaceTimeDiscretization discretization = getEffectiveSpaceTimeDiscretization(model);
final FDMSolver solver = FDMSolverFactory.createSolver(model, this, discretization, exercise);
if (isOneDimensionalModel(model)) {
final double[] terminalValues = buildCellAveragedTerminalValues(discretization);
if (exercise.isEuropean()) {
return solver.getValues(maturity, terminalValues);
} else {
return solver.getValues(
maturity,
terminalValues,
getExercisePayoffFunction()
);
}
}
return solver.getValues(maturity, getExercisePayoffFunction());
}
/**
* Returns the effective space-time discretization.
*
* <p>
* Bermudan exercise dates are inserted exactly into the time
* discretization.
* European and American exercise keep the model discretization unchanged.
* </p>
*
* @param model The finite-difference model.
* @return The effective space-time discretization.
*/
private SpaceTimeDiscretization getEffectiveSpaceTimeDiscretization(final FiniteDifferenceEquityModel model) {
final SpaceTimeDiscretization base = model.getSpaceTimeDiscretization();
if (!exercise.isBermudan()) {
return base;
}
final TimeDiscretization refinedTimeDiscretization =
FiniteDifferenceExerciseUtil.refineTimeDiscretization(
base.getTimeDiscretization(),
exercise
);
if (base.getNumberOfSpaceGrids() == 1) {
return new SpaceTimeDiscretization(
base.getSpaceGrid(0),
refinedTimeDiscretization,
base.getTheta(),
new double[] {base.getCenter(0) }
);
}
final int numberOfSpaceGrids = base.getNumberOfSpaceGrids();
final Grid[] spaceGrids = new Grid[numberOfSpaceGrids];
final double[] center = new double[numberOfSpaceGrids];
for (int i = 0; i < numberOfSpaceGrids; i++) {
spaceGrids[i] = base.getSpaceGrid(i);
center[i] = base.getCenter(i);
}
return new SpaceTimeDiscretization(
spaceGrids,
refinedTimeDiscretization,
base.getTheta(),
center
);
}
/**
* Builds the cell-averaged terminal values on the one-dimensional spot
* grid.
*
* @param discretization The effective discretization.
* @return Terminal values on the spot grid.
*/
private double[] buildCellAveragedTerminalValues(final SpaceTimeDiscretization discretization) {
final double[] sGrid = discretization.getSpaceGrid(0).getGrid();
final double[] terminalValues = new double[sGrid.length];
for (int i = 0; i < sGrid.length; i++) {
final double leftEdge = getLeftDualCellEdge(sGrid, i);
final double rightEdge = getRightDualCellEdge(sGrid, i);
terminalValues[i] = cellAveragedPayoff(leftEdge, rightEdge);
}
return terminalValues;
}
/**
* Returns the payoff function used for exercise projection.
*
* <p>
* This payoff is pointwise and uses strict inequalities at the strike
* in order to avoid a one-node bias exactly at the discontinuity.
* </p>
*
* @return Pointwise exercise payoff function.
*/
private DoubleUnaryOperator getExercisePayoffFunction() {
return this::pointwisePayoff;
}
/**
* Pointwise digital payoff used for exercise decisions.
*
* @param assetValue Underlying value.
* @return Pointwise payoff.
*/
private double pointwisePayoff(final double assetValue) {
final boolean inTheMoney =
callOrPutSign == CallOrPut.CALL
? assetValue > strike
: assetValue < strike;
if (!inTheMoney) {
return 0.0;
}
switch (digitalPayoffType) {
case CASH_OR_NOTHING:
return cashPayoff;
case ASSET_OR_NOTHING:
return assetValue;
default:
throw new IllegalStateException("Unsupported digital payoff type.");
}
}
/**
* Returns the payoff averaged over a cell.
*
* @param leftEdge Left cell edge.
* @param rightEdge Right cell edge.
* @return Cell-averaged payoff.
*/
private double cellAveragedPayoff(final double leftEdge, final double rightEdge) {
if (!(leftEdge < rightEdge)) {
throw new IllegalArgumentException("Require leftEdge < rightEdge.");
}
switch (digitalPayoffType) {
case CASH_OR_NOTHING:
return cellAveragedCashDigital(leftEdge, rightEdge);
case ASSET_OR_NOTHING:
return cellAveragedAssetDigital(leftEdge, rightEdge);
default:
throw new IllegalStateException("Unsupported digital payoff type.");
}
}
/**
* Cell average for a cash-or-nothing digital payoff.
*
* @param leftEdge Left cell edge.
* @param rightEdge Right cell edge.
* @return Cell-averaged payoff.
*/
private double cellAveragedCashDigital(final double leftEdge, final double rightEdge) {
final double cellLength = rightEdge - leftEdge;
if (callOrPutSign == CallOrPut.CALL) {
if (rightEdge <= strike) {
return 0.0;
}
if (leftEdge >= strike) {
return cashPayoff;
}
return cashPayoff * (rightEdge - strike) / cellLength;
} else {
if (rightEdge <= strike) {
return cashPayoff;
}
if (leftEdge >= strike) {
return 0.0;
}
return cashPayoff * (strike - leftEdge) / cellLength;
}
}
/**
* Cell average for an asset-or-nothing digital payoff.
*
* @param leftEdge Left cell edge.
* @param rightEdge Right cell edge.
* @return Cell-averaged payoff.
*/
private double cellAveragedAssetDigital(final double leftEdge, final double rightEdge) {
final double cellLength = rightEdge - leftEdge;
if (callOrPutSign == CallOrPut.CALL) {
if (rightEdge <= strike) {
return 0.0;
}
if (leftEdge >= strike) {
return 0.5 * (leftEdge + rightEdge);
}
return (rightEdge * rightEdge - strike * strike) / (2.0 * cellLength);
} else {
if (rightEdge <= strike) {
return 0.5 * (leftEdge + rightEdge);
}
if (leftEdge >= strike) {
return 0.0;
}
return (strike * strike - leftEdge * leftEdge) / (2.0 * cellLength);
}
}
/**
* Returns the left edge of the dual cell around node i.
*
* @param grid Spot grid.
* @param i Node index.
* @return Left dual-cell edge.
*/
private double getLeftDualCellEdge(final double[] grid, final int i) {
if (i == 0) {
return grid[0];
}
return 0.5 * (grid[i - 1] + grid[i]);
}
/**
* Returns the right edge of the dual cell around node i.
*
* @param grid Spot grid.
* @param i Node index.
* @return Right dual-cell edge.
*/
private double getRightDualCellEdge(final double[] grid, final int i) {
if (i == grid.length - 1) {
return grid[grid.length - 1];
}
return 0.5 * (grid[i] + grid[i + 1]);
}
/**
* Checks whether the model is one-dimensional.
*
* @param model The model.
* @return True if the model is one-dimensional.
*/
private boolean isOneDimensionalModel(final FiniteDifferenceEquityModel model) {
return model instanceof FDMBlackScholesModel
|| model instanceof FDMCevModel
|| model instanceof FDMBachelierModel;
}
private static CallOrPut mapCallOrPut(final double callOrPutSign) {
if (callOrPutSign == 1.0) {
return CallOrPut.CALL;
}
if (callOrPutSign == -1.0) {
return CallOrPut.PUT;
}
throw new IllegalArgumentException("Unknown option type.");
}
/**
* Returns the underlying name.
*
* @return The underlying name.
*/
public String getUnderlyingName() {
return underlyingName;
}
/**
* Returns the maturity.
*
* @return The maturity.
*/
public double getMaturity() {
return maturity;
}
/**
* Returns the strike.
*
* @return The strike.
*/
public double getStrike() {
return strike;
}
/**
* Returns call/put type.
*
* @return The option type.
*/
public CallOrPut getCallOrPut() {
return callOrPutSign;
}
/**
* Returns the digital payoff type.
*
* @return The digital payoff type.
*/
public DigitalPayoffType getDigitalPayoffType() {
return digitalPayoffType;
}
/**
* Returns the cash payoff amount.
*
* @return The cash payoff amount.
*/
public double getCashPayoff() {
return cashPayoff;
}
/**
* Returns the exercise specification.
*
* @return The exercise specification.
*/
public Exercise getExercise() {
return exercise;
}
}