SwapLeg.java

package net.finmath.finitedifference.interestrate.products;

import java.util.Arrays;

import net.finmath.finitedifference.interestrate.models.FiniteDifferenceInterestRateModel;
import net.finmath.time.Schedule;

/**
 * Finite-difference valuation of a reduced-scope swap leg.
 *
 * <p>
 * This class mirrors the role of the analytic and Monte Carlo swap-leg classes,
 * but is adapted to the current Markovian finite-difference rates framework.
 * </p>
 *
 * <p>
 * A leg is specified by
 * </p>
 * <ul>
 *   <li>a schedule,</li>
 *   <li>an optional forwarding-curve name,</li>
 *   <li>a notional for each period,</li>
 *   <li>a spread for each period,</li>
 *   <li>an optional flag for notional exchange.</li>
 * </ul>
 *
 * <p>
 * If {@code forwardCurveName} is {@code null} or empty, the leg is interpreted
 * as a fixed leg and the spread is the fixed coupon rate.
 * Otherwise the coupon in period {@code i} is
 * </p>
 *
 * <p>
 * <i>
 * \bigl(F(t;T_i^{\mathrm{fix}},T_i^{\mathrm{pay}}) + s_i\bigr)\delta_i.
 * </i>
 * </p>
 *
 * <p>
 * Reduced-scope Markovian assumption:
 * </p>
 * <ul>
 *   <li>fixed-leg periods with {@code periodStart < evaluationTime} are ignored,</li>
 *   <li>floating-leg periods with {@code fixingDate < evaluationTime} are ignored.</li>
 * </ul>
 *
 * <p>
 * This means the product is interpreted as the remaining forward-looking leg
 * from the current evaluation time onward. The class is therefore intended for
 * forward/running swap valuation in the PDE setting and for swaption
 * underlyings. It does not attempt to encode already-fixed-but-not-yet-paid
 * floating coupons as additional state variables.
 * </p>
 *
 * <p>
 * Since the product is linear, valuation is performed directly from discount
 * bonds and forward rates. No PDE solver is required.
 * </p>
 *
 * @author Alessandro Gnoatto
 */
public class SwapLeg implements FiniteDifferenceInterestRateProduct {

	/**
	 * The time tolerance.
	 */
	private static final double TIME_TOLERANCE = 1E-12;

	/**
	 * The leg schedule.
	 */
	private final Schedule legSchedule;
	/**
	 * The forward curve name.
	 */
	private final String forwardCurveName;
	/**
	 * The notionals.
	 */
	private final double[] notionals;
	/**
	 * The spreads.
	 */
	private final double[] spreads;
	/**
	 * The is notional exchanged.
	 */
	private final boolean isNotionalExchanged;

	/**
	 * Creates a swap leg.
	 *
	 * @param legSchedule The leg schedule.
	 * @param forwardCurveName The forwarding-curve name. If {@code null} or empty,
	 *        the leg is interpreted as fixed.
	 * @param notionals The notionals for all periods.
	 * @param spreads The spreads or fixed coupon rates for all periods.
	 * @param isNotionalExchanged If true, notional exchange is included.
	 */
	public SwapLeg(
			final Schedule legSchedule,
			final String forwardCurveName,
			final double[] notionals,
			final double[] spreads,
			final boolean isNotionalExchanged) {

		if (legSchedule == null) {
			throw new IllegalArgumentException("legSchedule must not be null.");
		}
		if (legSchedule.getNumberOfPeriods() <= 0) {
			throw new IllegalArgumentException("legSchedule must contain at least one period.");
		}
		if (notionals == null || notionals.length != legSchedule.getNumberOfPeriods()) {
			throw new IllegalArgumentException(
					"notionals must have the same length as the number of schedule periods."
					);
		}
		if (spreads == null || spreads.length != legSchedule.getNumberOfPeriods()) {
			throw new IllegalArgumentException(
					"spreads must have the same length as the number of schedule periods."
					);
		}

		this.legSchedule = legSchedule;
		this.forwardCurveName = forwardCurveName;
		this.notionals = notionals.clone();
		this.spreads = spreads.clone();
		this.isNotionalExchanged = isNotionalExchanged;

		validateInputs();
	}

	/**
	 * Creates a swap leg with constant notional equal to one and constant spread.
	 *
	 * @param legSchedule The leg schedule.
	 * @param forwardCurveName The forwarding-curve name. If {@code null} or empty,
	 *        the leg is interpreted as fixed.
	 * @param spread The constant spread or fixed coupon rate.
	 * @param isNotionalExchanged If true, notional exchange is included.
	 */
	public SwapLeg(
			final Schedule legSchedule,
			final String forwardCurveName,
			final double spread,
			final boolean isNotionalExchanged) {
		this(
				legSchedule,
				forwardCurveName,
				createConstantArray(legSchedule, 1.0),
				createConstantArray(legSchedule, spread),
				isNotionalExchanged
				);
	}

	/**
	 * Creates a swap leg with constant notional equal to one, constant spread,
	 * and no notional exchange.
	 *
	 * @param legSchedule The leg schedule.
	 * @param forwardCurveName The forwarding-curve name. If {@code null} or empty,
	 *        the leg is interpreted as fixed.
	 * @param spread The constant spread or fixed coupon rate.
	 */
	public SwapLeg(
			final Schedule legSchedule,
			final String forwardCurveName,
			final double spread) {
		this(legSchedule, forwardCurveName, spread, false);
	}

	@Override
	public double[] getValue(
			final double evaluationTime,
			final FiniteDifferenceInterestRateModel model) {

		validateModel(model);

		final double[] xGrid = model.getSpaceTimeDiscretization().getSpaceGrid(0).getGrid();
		final double[] values = new double[xGrid.length];

		for (int i = 0; i < xGrid.length; i++) {
			values[i] = getValueAt(evaluationTime, xGrid[i], model);
		}

		return values;
	}

	@Override
	public double[][] getValues(final FiniteDifferenceInterestRateModel model) {

		validateModel(model);

		final double[] xGrid = model.getSpaceTimeDiscretization().getSpaceGrid(0).getGrid();
		final int numberOfSpacePoints = xGrid.length;
		final int numberOfTimes = model.getSpaceTimeDiscretization().getTimeDiscretization().getNumberOfTimes();

		final double[][] values = new double[numberOfSpacePoints][numberOfTimes];

		for (int timeIndex = 0; timeIndex < numberOfTimes; timeIndex++) {
			final double evaluationTime = model.getSpaceTimeDiscretization().getTimeDiscretization().getTime(timeIndex);

			for (int spaceIndex = 0; spaceIndex < numberOfSpacePoints; spaceIndex++) {
				values[spaceIndex][timeIndex] = getValueAt(evaluationTime, xGrid[spaceIndex], model);
			}
		}

		return values;
	}

	/**
	 * Returns the leg value at the given evaluation time and state.
	 *
	 * @param evaluationTime The evaluation time.
	 * @param stateVariable The state variable.
	 * @param model The finite-difference interest-rate model.
	 * @return The leg value.
	 */
	public double getValueAt(
			final double evaluationTime,
			final double stateVariable,
			final FiniteDifferenceInterestRateModel model) {

		double value = 0.0;

		for (int periodIndex = 0; periodIndex < legSchedule.getNumberOfPeriods(); periodIndex++) {

			final double periodStart = legSchedule.getPeriodStart(periodIndex);
			final double fixingDate = legSchedule.getFixing(periodIndex);
			final double paymentDate = legSchedule.getPayment(periodIndex);
			final double periodLength = legSchedule.getPeriodLength(periodIndex);
			final double notional = notionals[periodIndex];

			if (isFixedLeg()) {
				if (periodStart < evaluationTime - TIME_TOLERANCE) {
					continue;
				}
			} else {
				if (fixingDate < evaluationTime - TIME_TOLERANCE) {
					continue;
				}
			}

			if (paymentDate <= evaluationTime + TIME_TOLERANCE) {
				continue;
			}

			double couponRate = spreads[periodIndex];
			if (!isFixedLeg()) {
				couponRate += model.getForwardRate(
						forwardCurveName,
						evaluationTime,
						fixingDate,
						paymentDate,
						stateVariable
						);
			}

			value += notional
					* couponRate
					* periodLength
					* model.getDiscountBond(evaluationTime, paymentDate, stateVariable);

			if (isNotionalExchanged) {
				if (periodStart > evaluationTime + TIME_TOLERANCE) {
					value -= notional * model.getDiscountBond(evaluationTime, periodStart, stateVariable);
				}
				if (legSchedule.getPeriodEnd(periodIndex) > evaluationTime + TIME_TOLERANCE) {
					value += notional * model.getDiscountBond(
							evaluationTime,
							legSchedule.getPeriodEnd(periodIndex),
							stateVariable
							);
				}
			}
		}

		return value;
	}

	/**
	 * Returns the schedule.
	 *
	 * @return The schedule.
	 */
	public Schedule getSchedule() {
		return legSchedule;
	}

	/**
	 * Returns the forwarding-curve name.
	 *
	 * @return The forwarding-curve name, possibly {@code null}.
	 */
	public String getForwardCurveName() {
		return forwardCurveName;
	}

	/**
	 * Returns the notionals.
	 *
	 * @return The notionals.
	 */
	public double[] getNotionals() {
		return notionals.clone();
	}

	/**
	 * Returns the spreads or fixed coupon rates.
	 *
	 * @return The spreads.
	 */
	public double[] getSpreads() {
		return spreads.clone();
	}

	/**
	 * Returns whether notional exchange is included.
	 *
	 * @return True if notional exchange is included.
	 */
	public boolean isNotionalExchanged() {
		return isNotionalExchanged;
	}

	/**
	 * Returns whether the leg is fixed.
	 *
	 * @return True if the leg is fixed.
	 */
	public boolean isFixedLeg() {
		return forwardCurveName == null || forwardCurveName.isEmpty();
	}

	@Override
	public String toString() {
		return "SwapLeg [legSchedule=" + legSchedule
				+ ", forwardCurveName=" + forwardCurveName
				+ ", notionals=" + Arrays.toString(notionals)
				+ ", spreads=" + Arrays.toString(spreads)
				+ ", isNotionalExchanged=" + isNotionalExchanged
				+ "]";
	}

	private void validateInputs() {
		for (int periodIndex = 0; periodIndex < legSchedule.getNumberOfPeriods(); periodIndex++) {
			if (notionals[periodIndex] < 0.0) {
				throw new IllegalArgumentException("All notionals must be non-negative.");
			}
			if (legSchedule.getFixing(periodIndex) < 0.0) {
				throw new IllegalArgumentException("Schedule fixing times must be non-negative.");
			}
			if (legSchedule.getPayment(periodIndex) <= legSchedule.getFixing(periodIndex)) {
				throw new IllegalArgumentException("Each payment time must be strictly later than fixing.");
			}
			if (legSchedule.getPeriodLength(periodIndex) < 0.0) {
				throw new IllegalArgumentException("Schedule period lengths must be non-negative.");
			}
		}
	}

	private void validateModel(final FiniteDifferenceInterestRateModel model) {
		if (model == null) {
			throw new IllegalArgumentException("model must not be null.");
		}
		if (model.getSpaceTimeDiscretization().getNumberOfSpaceGrids() != 1) {
			throw new IllegalArgumentException(
					"SwapLeg currently supports only one-dimensional finite-difference interest-rate models."
					);
		}
	}

	private static double[] createConstantArray(final Schedule schedule, final double value) {
		if (schedule == null) {
			throw new IllegalArgumentException("schedule must not be null.");
		}

		final double[] array = new double[schedule.getNumberOfPeriods()];
		Arrays.fill(array, value);
		return array;
	}
}