BasketOption.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 linear basket option on two assets.
 *
 * <p>
 * Let {@code S1} and {@code S2} denote the two underlying assets, let
 * {@code q1} and {@code q2} be the signed asset quantities, let {@code K} be
 * the strike, and let {@code \omega \in \{+1,-1\}} denote the call/put sign.
 * The payoff is
 * </p>
 *
 * <p>
 * <i>
 * \left( \omega \left( q_1 S_1(T) + q_2 S_2(T) - K \right) \right)^{+}.
 * </i>
 * </p>
 *
 * <p>
 * This single class covers a number of important special cases:
 * </p>
 * <ul>
 *   <li>arithmetic basket options, when both quantities are positive,</li>
 *   <li>spread options, for example with quantities {@code (1,-1)},</li>
 * <li>exchange options, for example with quantities {@code (1,-1)} and strike
 * {@code 0}.</li>
 * </ul>
 *
 * <p>
 * The class is written in a dimension-agnostic style through the quantity
 * vector,
 * but the current finite-difference implementation supports only the two-asset
 * case, that is, exactly two quantities and a two-dimensional model state.
 * </p>
 *
 * <p>
 * The current implementation supports only European exercise.
 * </p>
 *
 * @author Alessandro Gnoatto
 */
public class BasketOption implements FiniteDifferenceEquityProduct {

	/**
	 * The underlying names.
	 */
	private final String[] underlyingNames;
	/**
	 * The maturity.
	 */
	private final double maturity;
	/**
	 * The quantities.
	 */
	private final double[] quantities;
	/**
	 * The strike.
	 */
	private final double strike;
	/**
	 * The call or put.
	 */
	private final CallOrPut callOrPut;
	/**
	 * The exercise.
	 */
	private final Exercise exercise;

	/**
	 * Creates a European linear basket option.
	 *
	 * @param underlyingNames Names of the underlyings. May be {@code null}. If
	 *        provided, the array length must match the basket dimension.
	 * @param maturity Maturity {@code T}.
	 * @param quantities Signed asset quantities.
	 * @param strike Real strike {@code K}.
	 * @param callOrPut Option type.
	 */
	public BasketOption(
			final String[] underlyingNames,
			final double maturity,
			final double[] quantities,
			final double strike,
			final CallOrPut callOrPut) {

		if (quantities == null || quantities.length == 0) {
			throw new IllegalArgumentException("quantities must contain at least one entry.");
		}
		if (underlyingNames != null && underlyingNames.length != quantities.length) {
			throw new IllegalArgumentException(
					"underlyingNames must have the same length as quantities.");
		}
		if (callOrPut == null) {
			throw new IllegalArgumentException("callOrPut must not be null.");
		}
		if (maturity < 0.0) {
			throw new IllegalArgumentException("maturity must be non-negative.");
		}

		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.quantities = quantities.clone();
		this.strike = strike;
		this.callOrPut = callOrPut;
		this.exercise = new EuropeanExercise(maturity);
	}

	/**
	 * Creates a European linear basket option.
	 *
	 * @param maturity Maturity {@code T}.
	 * @param quantities Signed asset quantities.
	 * @param strike Real strike {@code K}.
	 * @param callOrPut Option type.
	 */
	public BasketOption(
			final double maturity,
			final double[] quantities,
			final double strike,
			final CallOrPut callOrPut) {
		this(null, maturity, quantities, strike, callOrPut);
	}

	/**
	 * Creates a European linear basket option.
	 *
	 * @param underlyingNames Names of the underlyings. May be {@code null}. If
	 *        provided, the array length must match the basket dimension.
	 * @param maturity Maturity {@code T}.
	 * @param quantities Signed asset quantities.
	 * @param strike Real strike {@code K}.
	 * @param callOrPutSign Payoff sign, where {@code 1.0} corresponds to a call
	 *        and {@code -1.0} corresponds to a put.
	 */
	public BasketOption(
			final String[] underlyingNames,
			final double maturity,
			final double[] quantities,
			final double strike,
			final double callOrPutSign) {
		this(
				underlyingNames,
				maturity,
				quantities,
				strike,
				mapCallOrPut(callOrPutSign)
				);
	}

	/**
	 * Creates a European linear basket option.
	 *
	 * @param maturity Maturity {@code T}.
	 * @param quantities Signed asset quantities.
	 * @param strike Real strike {@code K}.
	 * @param callOrPutSign Payoff sign, where {@code 1.0} corresponds to a call
	 *        and {@code -1.0} corresponds to a put.
	 */
	public BasketOption(
			final double maturity,
			final double[] quantities,
			final double strike,
			final double callOrPutSign) {
		this(null, maturity, quantities, strike, mapCallOrPut(callOrPutSign));
	}

	@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
				);
	}

	private double terminalPayoff(final double firstAssetValue, final double secondAssetValue) {
		final double basketValue =
				quantities[0] * firstAssetValue + quantities[1] * secondAssetValue - strike;

		return Math.max(callOrPut.toInteger() * basketValue, 0.0);
	}

	private void validateModel(final FiniteDifferenceEquityModel model) {
		if (model == null) {
			throw new IllegalArgumentException("model must not be null.");
		}

		if (quantities.length != 2) {
			throw new IllegalArgumentException(
					"BasketOption currently supports only the two-asset case.");
		}

		if (model.getSpaceTimeDiscretization().getNumberOfSpaceGrids() != 2) {
			throw new IllegalArgumentException(
					"BasketOption currently requires a two-dimensional space discretization.");
		}

		if (model.getInitialValue() == null || model.getInitialValue().length != 2) {
			throw new IllegalArgumentException(
					"BasketOption currently requires a two-dimensional model state.");
		}
	}

	private static CallOrPut mapCallOrPut(final double sign) {
		if (sign == 1.0) {
			return CallOrPut.CALL;
		}
		if (sign == -1.0) {
			return CallOrPut.PUT;
		}
		throw new IllegalArgumentException("Unknown option type.");
	}

	/**
	 * Returns the underlying names.
	 *
	 * @return The underlying names, 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 signed asset quantities.
	 *
	 * @return The signed asset quantities.
	 */
	public double[] getQuantities() {
		return quantities.clone();
	}

	/**
	 * Returns the strike.
	 *
	 * @return The strike.
	 */
	public double getStrike() {
		return strike;
	}

	/**
	 * 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 "BasketOption [maturity=" + maturity
				+ ", strike=" + strike
				+ ", callOrPut=" + callOrPut
				+ ", quantities=" + Arrays.toString(quantities)
				+ "]";
	}
}