SwaptionFromSwapSchedules.java

package net.finmath.montecarlo.interestrate.products;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import net.finmath.exception.CalculationException;
import net.finmath.functions.AnalyticFormulas;
import net.finmath.marketdata.products.SwapAnnuity;
import net.finmath.montecarlo.interestrate.LIBORModelMonteCarloSimulationModel;
import net.finmath.montecarlo.interestrate.TermStructureMonteCarloSimulationModel;
import net.finmath.montecarlo.process.ProcessTimeDiscretizationProvider;
import net.finmath.stochastic.RandomVariable;
import net.finmath.time.FloatingpointDate;
import net.finmath.time.Period;
import net.finmath.time.Schedule;
import net.finmath.time.TimeDiscretization;
import net.finmath.time.TimeDiscretizationFromArray;

/**
 * Implementation of a Monte-Carlo valuation of a swaption valuation being compatible with AAD.
 *
 * The valuation internally uses an analytic valuation of a swap such that the {@link #getValue(double, TermStructureMonteCarloSimulationModel)} method
 * returns an valuation being \( \mathcal{F}_{t} \}-measurable where \( t \) is the evaluationTime argument.
 *
 * @author Christian Fries
 */
public class SwaptionFromSwapSchedules extends AbstractTermStructureMonteCarloProduct implements ProcessTimeDiscretizationProvider, net.finmath.modelling.products.Swaption {

	public enum SwaptionType{
		PAYER,
		RECEIVER
	}

	private final SwaptionType swaptionType;
	private final LocalDate exerciseDate;
	private final Schedule scheduleFixedLeg;
	private final Schedule scheduleFloatLeg;
	private final double swaprate;
	private final double notional;
	private final ValueUnit valueUnit;
	private final LocalDateTime referenceDate;

	public SwaptionFromSwapSchedules(
			final LocalDateTime referenceDate,
			final SwaptionType swaptionType,
			final LocalDate exerciseDate,
			final Schedule scheduleFixedLeg,
			final Schedule scheduleFloatLeg,
			final double swaprate,
			final double notional,
			final ValueUnit valueUnit) {

		this.referenceDate = referenceDate;
		this.swaptionType = swaptionType;
		this.exerciseDate = exerciseDate;
		this.scheduleFixedLeg = scheduleFixedLeg;
		this.scheduleFloatLeg = scheduleFloatLeg;
		this.swaprate = swaprate;
		this.notional = notional;
		this.valueUnit = valueUnit;
	}

	@Override
	public RandomVariable getValue(final double evaluationTime, final TermStructureMonteCarloSimulationModel model)
			throws CalculationException {

		LocalDate modelReferenceDate = null;
		try {
			modelReferenceDate = model.getReferenceDate().toLocalDate();
			if(modelReferenceDate == null) {
				modelReferenceDate = referenceDate.toLocalDate();
			}
		}
		catch(final UnsupportedOperationException e) {}

		final double exerciseTime = FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, exerciseDate);

		final RandomVariable discountedCashflowFixLeg		= getValueOfLegAnalytic(exerciseTime, model, scheduleFixedLeg, false, swaprate, notional);
		final RandomVariable discountedCashflowFloatingLeg	= getValueOfLegAnalytic(exerciseTime, model, scheduleFloatLeg, true, 0.0, notional);

		// Distinguish whether the swaption is of type "Payer" or "Receiver":
		RandomVariable values;
		if(swaptionType.equals(SwaptionType.PAYER)){
			values = discountedCashflowFloatingLeg.sub(discountedCashflowFixLeg);
		}
		else if(swaptionType.equals(SwaptionType.RECEIVER)){
			values = discountedCashflowFixLeg.sub(discountedCashflowFloatingLeg);
		}
		else {
			throw new IllegalArgumentException("Unkown swaptionType " + swaptionType);
		}

		// Floor at zero
		values = values.floor(0.0);

		final RandomVariable	numeraire = model.getNumeraire(exerciseTime);
		final RandomVariable	monteCarloProbabilities	= model.getMonteCarloWeights(exerciseTime);
		values = values.div(numeraire).mult(monteCarloProbabilities);

		// Note that values is a relative price - no numeraire division is required
		final RandomVariable	numeraireAtZero	= model.getNumeraire(evaluationTime);
		final RandomVariable	monteCarloProbabilitiesAtZero = model.getMonteCarloWeights(evaluationTime);
		values = values.mult(numeraireAtZero).div(monteCarloProbabilitiesAtZero);

		if(valueUnit == ValueUnit.VALUE) {
			return values;
		}

		/*
		 * Need to convert value to different value unit.
		 */

		// Use analytic formula to calculate the options black/bachelier implied vol using the MC price from above:
		final double atmSwaprate 	= net.finmath.marketdata.products.Swap.getForwardSwapRate(scheduleFixedLeg, scheduleFloatLeg, model.getModel().getForwardRateCurve(), model.getModel().getAnalyticModel());
		final double forward = atmSwaprate;
		final double optionStrike = swaprate;
		final double optionMaturity = FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, exerciseDate);

		final double[] swapTenor = new double[scheduleFixedLeg.getNumberOfPeriods() + 1];
		for(int i = 0; i < scheduleFixedLeg.getNumberOfPeriods(); i++) {
			swapTenor[i] = scheduleFixedLeg.getFixing(i);
		}
		swapTenor[scheduleFixedLeg.getNumberOfPeriods()] = scheduleFixedLeg.getPayment(scheduleFixedLeg.getNumberOfPeriods() - 1);

		final double swapAnnuity      = SwapAnnuity.getSwapAnnuity(new TimeDiscretizationFromArray(swapTenor), model.getModel().getDiscountCurve());

		switch(valueUnit) {
		case VALUE:
			return values;
		case VOLATILITYNORMAL:
			final double volatitliy = AnalyticFormulas.bachelierOptionImpliedVolatility(forward, optionMaturity, optionStrike, swapAnnuity, values.getAverage());
			return model.getRandomVariableForConstant(volatitliy);
		case VOLATILITYLOGNORMAL:
			return model.getRandomVariableForConstant(AnalyticFormulas.blackScholesOptionImpliedVolatility(forward, optionMaturity, optionStrike, swapAnnuity, values.getAverage()));
		case VOLATILITYNORMALATM:
			RandomVariable annuity = getValueOfLegAnalytic(evaluationTime, model, scheduleFixedLeg, false, 1.0, notional);
			annuity = annuity.div(numeraire).mult(monteCarloProbabilities).mult(numeraireAtZero).div(monteCarloProbabilitiesAtZero);
			return values.div(Math.sqrt(optionMaturity / Math.PI / 2.0)).div(annuity.average());
		default:
			throw new IllegalArgumentException("Unsupported valueUnit " + valueUnit.name());
		}
	}

	@Override
	public TimeDiscretization getProcessTimeDiscretization(final LocalDateTime referenceDate) {
		final Set<Double> times = new HashSet<>();

		times.add(FloatingpointDate.getFloatingPointDateFromDate(referenceDate, exerciseDate.atStartOfDay()));

		final Function<Period, Double> periodToTime = new Function<Period, Double>() {
			@Override
			public Double apply(final Period period) { return FloatingpointDate.getFloatingPointDateFromDate(referenceDate, period.getPayment().atStartOfDay()); }
		};
		times.addAll(scheduleFixedLeg.getPeriods().stream().map(periodToTime).collect(Collectors.toList()));
		times.addAll(scheduleFloatLeg.getPeriods().stream().map(periodToTime).collect(Collectors.toList()));

		return new TimeDiscretizationFromArray(times);
	}

	/**
	 * @return the exercise date
	 */
	public LocalDate getExerciseDate() {
		return exerciseDate;
	}


	/**
	 * Determines the time \( t \)-measurable value of a swap leg (can handle fix or float).
	 *
	 * @param evaluationTime The time \( t \) conditional to which the value is calculated.
	 * @param model The model implmeneting LIBORModelMonteCarloSimulationModel.
	 * @param schedule The schedule of the leg.
	 * @param paysFloatingRate If true, the leg will pay {@link LIBORModelMonteCarloSimulationModel#getForwardRate(double, double, double)}
	 * @param fixRate The fixed rate (if any)
	 * @param notional The notional
	 * @return The time \( t \)-measurable value
	 * @throws CalculationException Thrown is model failed to provide the required quantities.
	 */
	public static RandomVariable getValueOfLegAnalytic(final double evaluationTime, final TermStructureMonteCarloSimulationModel model, final Schedule schedule, final boolean paysFloatingRate, final double fixRate, final double notional) throws CalculationException {

		LocalDate modelReferenceDate = null;
		try {
			modelReferenceDate = model.getReferenceDate().toLocalDate();
			if(modelReferenceDate == null) {
				modelReferenceDate = schedule.getReferenceDate();
			}
		}
		catch(final UnsupportedOperationException e) {}

		RandomVariable discountedCashflowFloatingLeg	= model.getRandomVariableForConstant(0.0);
		for(int peridIndex = schedule.getNumberOfPeriods() - 1; peridIndex >= 0; peridIndex--) {
			final Period period = schedule.getPeriod(peridIndex);
			final double paymentTime		= FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, period.getPayment());
			final double fixingTime		= FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, period.getFixing());
			final double periodLength		= schedule.getPeriodLength(peridIndex);

			/*
			 * Note that it is important that getForwardDiscountBond and getLIBOR are called with evaluationTime = exerciseTime.
			 */
			final RandomVariable discountBond = model.getModel().getForwardDiscountBond(model.getProcess(), evaluationTime, paymentTime);
			if(paysFloatingRate) {
				final RandomVariable libor	= model.getForwardRate(evaluationTime, fixingTime, paymentTime);
				final RandomVariable periodCashFlow = libor.mult(periodLength).mult(notional);
				discountedCashflowFloatingLeg = discountedCashflowFloatingLeg.add(periodCashFlow.mult(discountBond));
			}
			if(fixRate != 0) {
				final RandomVariable periodCashFlow = model.getRandomVariableForConstant(fixRate * periodLength * notional);
				discountedCashflowFloatingLeg = discountedCashflowFloatingLeg.add(periodCashFlow.mult(discountBond));
			}
		}
		return discountedCashflowFloatingLeg;

	}

	@Override
	public String toString() {
		return "SwaptionConditionalAnalytic [swaptionType=" + swaptionType + ", referenceDate=" + referenceDate + ", exerciseDate="
				+ exerciseDate + ", swaprate=" + swaprate + ", valueUnit=" + valueUnit
				+ ", fixMetaSchedule=" + scheduleFixedLeg + ", floatMetaSchedule="
				+ scheduleFloatLeg + ", notional=" + notional + "]";
	}
}