Cap.java

/*
 * (c) Copyright Christian P. Fries, Germany. Contact: email@christian-fries.de.
 *
 * Created on 22.06.2014
 */

package net.finmath.marketdata.products;

import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;

import net.finmath.functions.AnalyticFormulas;
import net.finmath.marketdata.model.AnalyticModel;
import net.finmath.marketdata.model.curves.DiscountCurve;
import net.finmath.marketdata.model.curves.ForwardCurve;
import net.finmath.marketdata.model.volatilities.CapletVolatilities;
import net.finmath.marketdata.model.volatilities.VolatilitySurface;
import net.finmath.marketdata.model.volatilities.VolatilitySurface.QuotingConvention;
import net.finmath.optimizer.GoldenSectionSearch;
import net.finmath.time.Period;
import net.finmath.time.Schedule;
import net.finmath.time.ScheduleFromPeriods;

/**
 * Implements the valuation of a cap via an analytic model,
 * i.e. the specification of a forward curve, discount curve and volatility surface.
 *
 * A cap is a portfolio of Caplets with a common strike, i.e., the strike is the same for all Caplets.
 *
 * The class can value a caplet with a given strike or given moneyness. If moneyness is given,
 * the class calculates the ATM forward. Note that this is done by omitting the first (fixed) period,
 * see {@link #getATMForward(AnalyticModel, boolean)}.
 *
 * Note: A fixing in arrears is not handled correctly since a convexity adjustment is currently not applied.
 *
 * @TODO Support convexity adjustment if fixing is in arrears.
 *
 * @author Christian Fries
 * @version 1.0
 */
public class Cap extends AbstractAnalyticProduct {

	private final Schedule			schedule;
	private final String					forwardCurveName;
	private final double					strike;
	private final boolean					isStrikeMoneyness;
	private final String					discountCurveName;
	private final String					volatiltiySufaceName;

	private final VolatilitySurface.QuotingConvention quotingConvention;

	private transient double cachedATMForward = Double.NaN;
	private transient SoftReference<AnalyticModel> cacheStateModel;
	private transient boolean cacheStateIsFirstPeriodIncluded;

	/**
	 * Create a Caplet with a given schedule, strike on a given forward curve (by name)
	 * with a given discount curve and volatility surface (by name).
	 *
	 * The valuation is performed using analytic valuation formulas for the underlying caplets.
	 *
	 * @param schedule A given payment schedule, i.e., a collection of <code>Period</code>s with fixings, payments and period length.
	 * @param forwardCurveName The forward curve to be used for the forward of the index.
	 * @param strike The given strike (or moneyness).
	 * @param isStrikeMoneyness If true, then the strike argument is interpreted as moneyness, i.e. we calculate an ATM forward from the schedule.
	 * @param discountCurveName The discount curve to be used for discounting.
	 * @param volatilitySurfaceName The volatility surface to be used.
	 * @param quotingConvention The quoting convention of the value returned by the {@link #getValue}-method.
	 */
	public Cap(final Schedule schedule, final String forwardCurveName, final double strike, final boolean isStrikeMoneyness, final String discountCurveName, final String volatilitySurfaceName, final QuotingConvention quotingConvention) {
		super();
		this.schedule = schedule;
		this.forwardCurveName = forwardCurveName;
		this.strike = strike;
		this.isStrikeMoneyness = isStrikeMoneyness;
		this.discountCurveName = discountCurveName;
		volatiltiySufaceName = volatilitySurfaceName;
		this.quotingConvention = quotingConvention;
	}

	/**
	 * Create a Caplet with a given schedule, strike on a given forward curve (by name)
	 * with a given discount curve and volatility surface (by name).
	 *
	 * The valuation is performed using analytic valuation formulas for the underlying caplets.
	 *
	 * @param schedule A given payment schedule, i.e., a collection of <code>Period</code>s with fixings, payments and period length.
	 * @param forwardCurveName The forward curve to be used for the forward of the index.
	 * @param strike The given strike (or moneyness).
	 * @param isStrikeMoneyness If true, then the strike argument is interpreted as moneyness, i.e. we calculate an ATM forward from the schedule.
	 * @param discountCurveName The discount curve to be used for discounting.
	 * @param volatilitySurfaceName The volatility surface to be used.
	 */
	public Cap(final Schedule schedule, final String forwardCurveName, final double strike, final boolean isStrikeMoneyness, final String discountCurveName, final String volatilitySurfaceName) {
		this(schedule, forwardCurveName, strike, isStrikeMoneyness, discountCurveName, volatilitySurfaceName, QuotingConvention.PRICE);
	}

	@Override
	public double getValue(final double evaluationTime, final AnalyticModel model) {
		if(quotingConvention == QuotingConvention.PRICE) {
			return getValueAsPrice(evaluationTime, model);
		} else {
			return getImpliedVolatility(evaluationTime, model, quotingConvention);
		}
	}

	/**
	 * Returns the value of this product under the given model.
	 *
	 * @param evaluationTime Evaluation time.
	 * @param model The model.
	 * @return Value of this product und the given model.
	 */
	public double getValueAsPrice(final double evaluationTime, final AnalyticModel model) {
		final ForwardCurve	forwardCurve	= model.getForwardCurve(forwardCurveName);
		final DiscountCurve	discountCurve	= model.getDiscountCurve(discountCurveName);

		DiscountCurve	discountCurveForForward = null;
		if(forwardCurve == null && forwardCurveName != null && forwardCurveName.length() > 0) {
			// User might like to get forward from discount curve.
			discountCurveForForward	= model.getDiscountCurve(forwardCurveName);

			if(discountCurveForForward == null) {
				// User specified a name for the forward curve, but no curve was found.
				throw new IllegalArgumentException("No curve of the name " + forwardCurveName + " was found in the model.");
			}
		}

		double value = 0.0;
		for(int periodIndex=0; periodIndex<schedule.getNumberOfPeriods(); periodIndex++) {
			final double fixingDate	= schedule.getFixing(periodIndex);
			final double paymentDate	= schedule.getPayment(periodIndex);
			final double periodLength	= schedule.getPeriodLength(periodIndex);

			/*
			 * We do not count empty periods.
			 * Since empty periods are an indication for a ill-specified product,
			 * it might be reasonable to throw an illegal argument exception instead.
			 */
			if(periodLength == 0) {
				continue;
			}

			double forward = 0.0;
			if(forwardCurve != null) {
				forward			+= forwardCurve.getForward(model, fixingDate, paymentDate-fixingDate);
			}
			else if(discountCurveForForward != null) {
				/*
				 * Classical single curve case: using a discount curve as a forward curve.
				 * This is only implemented for demonstration purposes (an exception would also be appropriate :-)
				 */
				if(fixingDate != paymentDate) {
					forward			+= (discountCurveForForward.getDiscountFactor(fixingDate) / discountCurveForForward.getDiscountFactor(paymentDate) - 1.0) / (paymentDate-fixingDate);
				}
			}

			final double discountFactor	= paymentDate > evaluationTime ? discountCurve.getDiscountFactor(model, paymentDate) : 0.0;
			final double payoffUnit = discountFactor * periodLength;

			double effektiveStrike = strike;
			if(isStrikeMoneyness) {
				effektiveStrike += getATMForward(model, true);
			}

			final VolatilitySurface volatilitySurface	= model.getVolatilitySurface(volatiltiySufaceName);
			if(volatilitySurface == null) {
				throw new IllegalArgumentException("Volatility surface not found in model: " + volatiltiySufaceName);
			}
			if(volatilitySurface.getQuotingConvention() == QuotingConvention.VOLATILITYLOGNORMAL) {
				final double volatility = volatilitySurface.getValue(model, fixingDate, effektiveStrike, VolatilitySurface.QuotingConvention.VOLATILITYLOGNORMAL);
				value += AnalyticFormulas.blackScholesGeneralizedOptionValue(forward, volatility, fixingDate, effektiveStrike, payoffUnit);
			}
			else {
				// Default to normal volatility as quoting convention
				final double volatility = volatilitySurface.getValue(model, fixingDate, effektiveStrike, VolatilitySurface.QuotingConvention.VOLATILITYNORMAL);
				value += AnalyticFormulas.bachelierOptionValue(forward, volatility, fixingDate, effektiveStrike, payoffUnit);
			}
		}

		return value / discountCurve.getDiscountFactor(model, evaluationTime);
	}

	/**
	 * Return the ATM forward for this cap.
	 * The ATM forward is the fixed rate K such that the value of the payoffs
	 * \( F(t_i) - K \)
	 * is zero, where \( F(t_i) \) is the ATM forward of the i-th caplet.
	 * Note however that the is a convention to determine the ATM forward of a cap
	 * from the payoffs excluding the first one. The reason here is that for non-forward starting
	 * cap, the first period is already fixed, i.e. it has no vega.
	 *
	 * @param model The model to retrieve the forward curve from (by name).
	 * @param isFirstPeriodIncluded If true, the forward will be determined by considering the periods after removal of the first periods (except, if the Cap consists only of 1 period).
	 * @return The ATM forward of this cap.
	 */
	public double getATMForward(final AnalyticModel model, final boolean isFirstPeriodIncluded) {
		if(!Double.isNaN(cachedATMForward) && cacheStateModel.get() == model && cacheStateIsFirstPeriodIncluded == isFirstPeriodIncluded) {
			return cachedATMForward;
		}

		Schedule remainderSchedule = schedule;
		if(!isFirstPeriodIncluded) {
			final ArrayList<Period> periods = new ArrayList<>();
			periods.addAll(schedule.getPeriods());

			if(periods.size() > 1) {
				periods.remove(0);
			}
			remainderSchedule = new ScheduleFromPeriods(schedule.getReferenceDate(), periods, schedule.getDaycountconvention());
		}

		final SwapLeg floatLeg = new SwapLeg(remainderSchedule, forwardCurveName, 0.0, discountCurveName, false);
		final SwapLeg annuityLeg = new SwapLeg(remainderSchedule, null, 1.0, discountCurveName, false);

		cachedATMForward = floatLeg.getValue(model) / annuityLeg.getValue(model);
		cacheStateModel = new SoftReference<>(model);
		cacheStateIsFirstPeriodIncluded = isFirstPeriodIncluded;

		return cachedATMForward;
	}

	/**
	 * Returns the value of this cap in terms of an implied volatility (of a flat caplet surface).
	 *
	 * @param evaluationTime The evaluation time as double. Cash flows prior and including this time are not considered.
	 * @param model The model under which the product is valued.
	 * @param quotingConvention The quoting convention requested for the return value.
	 * @return The value of the product using the given model in terms of a implied volatility.
	 */
	public double getImpliedVolatility(final double evaluationTime, final AnalyticModel model, final VolatilitySurface.QuotingConvention quotingConvention) {
		double lowerBound = Double.MAX_VALUE;
		double upperBound = -Double.MAX_VALUE;
		for(int periodIndex=0; periodIndex<schedule.getNumberOfPeriods(); periodIndex++) {
			final double fixingDate	= schedule.getFixing(periodIndex);
			final double periodLength	= schedule.getPeriodLength(periodIndex);

			if(periodLength == 0) {
				continue;
			}

			double effektiveStrike = strike;
			if(isStrikeMoneyness) {
				effektiveStrike += getATMForward(model, true);
			}

			final VolatilitySurface volatilitySurface	= model.getVolatilitySurface(volatiltiySufaceName);
			final double volatility = volatilitySurface.getValue(model, fixingDate, effektiveStrike, quotingConvention);
			lowerBound = Math.min(volatility, lowerBound);
			upperBound = Math.max(volatility, upperBound);
		}

		final double value = getValueAsPrice(evaluationTime, model);

		final int		maxIterations	= 100;
		final double	maxAccuracy		= 0.0;
		final GoldenSectionSearch solver = new GoldenSectionSearch(lowerBound, upperBound);
		while(solver.getAccuracy() > maxAccuracy && !solver.isDone() && solver.getNumberOfIterations() < maxIterations) {
			final double volatility = solver.getNextPoint();
			final double[] maturities		= { 1.0 };
			final double[] strikes		= { 0.0 };
			final double[] volatilities	= { volatility };
			final VolatilitySurface flatSurface = new CapletVolatilities(model.getVolatilitySurface(volatiltiySufaceName).getName(), model.getVolatilitySurface(volatiltiySufaceName).getReferenceDate(), model.getForwardCurve(forwardCurveName), maturities, strikes, volatilities, quotingConvention, model.getDiscountCurve(discountCurveName));
			AnalyticModel flatModel = model.clone();
			flatModel = flatModel.addVolatilitySurfaces(flatSurface);
			final double flatModelValue = this.getValueAsPrice(evaluationTime, flatModel);
			final double error = value-flatModelValue;
			solver.setValue(error*error);
		}
		return solver.getBestPoint();
	}

	/**
	 * Returns the name of the forward curve references by this cap.
	 *
	 * @return the forward curve name.
	 */
	public String getForwardCurveName() {
		return forwardCurveName;
	}

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

	/**
	 * Returns the name of the discount curve referenced by this cap.
	 *
	 * @return the discount curve name
	 */
	public String getDiscountCurveName() {
		return discountCurveName;
	}

	@Override
	public String toString() {
		return "Cap [schedule=" + schedule + ", forwardCurveName="
				+ forwardCurveName + ", strike=" + strike
				+ ", discountCurveName=" + discountCurveName
				+ ", volatiltiySufaceName=" + volatiltiySufaceName + "]";
	}

	private void readObject(final java.io.ObjectInputStream in) throws ClassNotFoundException, IOException {
		in.defaultReadObject();
		// initialization of transients
		cachedATMForward = Double.NaN;
	}
}