package net.finmath.montecarlo.interestrate.products;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.IntToDoubleFunction;
import java.util.function.ToDoubleFunction;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import net.finmath.exception.CalculationException;
import net.finmath.modelling.products.Swaption;
import net.finmath.montecarlo.MonteCarloSimulationModel;
import net.finmath.montecarlo.RandomVariableFromDoubleArray;
import net.finmath.montecarlo.conditionalexpectation.MonteCarloConditionalExpectationLinearRegressionFactory;
import net.finmath.montecarlo.conditionalexpectation.MonteCarloConditionalExpectationRegressionFactory;
import net.finmath.montecarlo.conditionalexpectation.RegressionBasisFunctionsProvider;
import net.finmath.montecarlo.interestrate.LIBORModelMonteCarloSimulationModel;
import net.finmath.montecarlo.interestrate.TermStructureMonteCarloSimulationModel;
import net.finmath.montecarlo.process.ProcessTimeDiscretizationProvider;
import net.finmath.stochastic.ConditionalExpectationEstimator;
import net.finmath.stochastic.RandomVariable;
import net.finmath.stochastic.Scalar;
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;

/**
 * Implements the valuation of a Bermudan swaption under a <code>LIBORModelMonteCarloSimulationModel</code>
 *
 * @author Christian Fries
 * @version 1.4
 * @date 06.12.2009
 * @date 07.04.2019
 */
public class BermudanSwaptionFromSwapSchedules extends AbstractTermStructureMonteCarloProduct implements RegressionBasisFunctionsProvider, ProcessTimeDiscretizationProvider, Swaption {

	private static Logger logger = Logger.getLogger("net.finmath");

	public enum SwaptionType{
		PAYER,
		RECEIVER
	}

	private final LocalDateTime		referenceDate;
	private final SwaptionType 		swaptionType;
	private final LocalDate[] 		exerciseDates;
	private final LocalDate 		swapEndDate;
	private final double[]			swaprates;
	private final double[] 			notionals;
	private final Schedule[]  		fixSchedules;
	private final Schedule[]  		floatSchedules;
	private final RegressionBasisFunctionsProvider regressionBasisFunctionProvider;
	private final MonteCarloConditionalExpectationRegressionFactory conditionalExpectationRegressionFactory;
	private final boolean			isUseAnalyticSwapValuationAtExercise = true;

	/**
	 * Create a Bermudan swaption from an array of underlying swap schedules (fix leg and float leg), swap rates and notionals.
	 *
	 * This class implements the class backward algorithm using a provided factory for the
	 * determination of the conditional expectation.
	 *
	 * For <code>conditionalExpectationRegressionFactory</code> you may pass
	 * <code>new MonteCarloConditionalExpectationLinearRegressionFactory()</code> (default) or, e.g., <code>new MonteCarloConditionalExpectationLocalizedOnDependentRegressionFactory(2.0)</code>.
	 *
	 * @param referenceDate The date associated with the inception (t=0) of this product. (Not used).
	 * @param swaptionType The type of the underlying swap (PAYER, RECEIVER).
	 * @param exerciseDates A vector of exercise dates.
	 * @param swapEndDate The final maturity of the underlying swap.
	 * @param swaprates A vector of swap rates for the underlying swaps.
	 * @param notionals A vector of notionals for the underlying swaps.
	 * @param fixSchedules A vector of fix leg schedules for the underlying swaps.
	 * @param floatSchedules A vector of float leg schedules for the underlying swaps.
	 * @param conditionalExpectationRegressionFactory A object implementing a factory creating a conditional expectation estimator from given regression basis functions
	 * @param regressionBasisFunctionProvider An object implementing RegressionBasisFunctionsProvider to provide the basis functions for the estimation of conditional expectations.
	 */
	public BermudanSwaptionFromSwapSchedules(final LocalDateTime referenceDate, final SwaptionType swaptionType, final LocalDate[] exerciseDates,
			final LocalDate swapEndDate, final double[] swaprates, final double[] notionals, final Schedule[]  fixSchedules,
			final Schedule[]  floatSchedules, final MonteCarloConditionalExpectationRegressionFactory conditionalExpectationRegressionFactory, final RegressionBasisFunctionsProvider regressionBasisFunctionProvider) {
		this.referenceDate = referenceDate;
		this.swaptionType = swaptionType;
		this.swapEndDate = swapEndDate;
		this.swaprates = swaprates;
		this.notionals = notionals;
		this.exerciseDates = exerciseDates;
		this.fixSchedules = fixSchedules;
		this.floatSchedules = floatSchedules;

		this.regressionBasisFunctionProvider = regressionBasisFunctionProvider != null ? regressionBasisFunctionProvider : this;
		this.conditionalExpectationRegressionFactory = conditionalExpectationRegressionFactory;
	}

	/**
	 * Create a Bermudan swaption.
	 *
	 * @param referenceDate The date associated with the inception (t=0) of this product. (Not used).
	 * @param swaptionType The type of the underlying swap (PAYER, RECEIVER).
	 * @param exerciseDates A vector of exercise dates.
	 * @param swapEndDate The final maturity of the underlying swap.
	 * @param swaprates A vector of swap rates for the underlying swaps.
	 * @param notionals A vector of notionals for the underlying swaps.
	 * @param fixSchedules A vector of fix leg schedules for the underlying swaps.
	 * @param floatSchedules A vector of float leg schedules for the underlying swaps.
	 * @param regressionBasisFunctionProvider An object implementing RegressionBasisFunctionsProvider to provide the basis functions for the estimation of conditional expectations.
	 */
	public BermudanSwaptionFromSwapSchedules(final LocalDateTime referenceDate, final SwaptionType swaptionType, final LocalDate[] exerciseDates,
			final LocalDate swapEndDate, final double[] swaprates, final double[] notionals, final Schedule[]  fixSchedules,
			final Schedule[]  floatSchedules, final RegressionBasisFunctionsProvider regressionBasisFunctionProvider) {
		this(referenceDate, swaptionType, exerciseDates, swapEndDate, swaprates,notionals, fixSchedules, floatSchedules, new MonteCarloConditionalExpectationLinearRegressionFactory(), regressionBasisFunctionProvider);
	}

	/**
	 * Create a Bermudan swaption.
	 *
	 * @param referenceDate The date associated with the inception (t=0) of this product. (Not used).
	 * @param swaptionType The type of the underlying swap (PAYER, RECEIVER).
	 * @param exerciseDates A vector of exercise dates.
	 * @param swapEndDate The final maturity of the underlying swap.
	 * @param swaprates A vector of swap rates for the underlying swaps.
	 * @param notionals A vector of notionals for the underlying swaps.
	 * @param fixSchedules A vector of fix leg schedules for the underlying swaps.
	 * @param floatSchedules A vector of float leg schedules for the underlying swaps.
	 */
	public BermudanSwaptionFromSwapSchedules(final LocalDateTime referenceDate, final SwaptionType swaptionType, final LocalDate[] exerciseDates,
			final LocalDate swapEndDate, final double[] swaprates, final double[] notionals, final Schedule[]  fixSchedules,
			final Schedule[]  floatSchedules) {
		this(referenceDate, swaptionType, exerciseDates, swapEndDate, swaprates,notionals, fixSchedules, floatSchedules, null);
	}

	/**
	 * Create a Bermudan swaption.
	 *
	 * @param referenceDate The date associated with the inception (t=0) of this product.
	 * @param swaptionType The type of the underlying swap (PAYER, RECEIVER).
	 * @param exerciseDates A vector of exercise dates.
	 * @param swapEndDate The final maturity of the underlying swap.
	 * @param swaprate A constant swaprate applying to all underlying swaps.
	 * @param notional A constant notional applying to all underlying swaps.
	 * @param fixSchedules A vector of fix leg schedules for the underlying swaps.
	 * @param floatSchedules A vector of float leg schedules for the underlying swaps.
	 */
	public BermudanSwaptionFromSwapSchedules(final LocalDateTime referenceDate, final SwaptionType swaptionType, final LocalDate[] exerciseDates,
			final LocalDate swapEndDate, final double swaprate, final double notional, final Schedule[] fixSchedules, final Schedule[]  floatSchedules) {
		this(referenceDate, swaptionType, exerciseDates, swapEndDate, IntStream.range(0, exerciseDates.length).mapToDouble(new IntToDoubleFunction() {
			@Override
			public double applyAsDouble(final int i) {
				return swaprate;
			}
		}).toArray(), IntStream.range(0, exerciseDates.length).mapToDouble(new IntToDoubleFunction() {
			@Override
			public double applyAsDouble(final int i) {
				return notional;
			}
		}).toArray(), fixSchedules, floatSchedules);
	}

	/**
	 * Returns the exercise dates.
	 *
	 * @return the exercise dates
	 */
	public LocalDate[] getExerciseDates() {
		return exerciseDates;
	}

	/**
	 * @return the swaptionType
	 */
	public SwaptionType getSwaptionType() {
		return swaptionType;
	}

	/**
	 * @return the swapEndDate
	 */
	public LocalDate getSwapEndDate() {
		return swapEndDate;
	}

	@Override
	public Map<String, Object> getValues(final double evaluationTime, final TermStructureMonteCarloSimulationModel model) throws CalculationException {

		final LocalDate modelReferenceDate = model.getReferenceDate().toLocalDate();

		RandomVariable values			= model.getRandomVariableForConstant(0.0);
		RandomVariable exerciseTimes	= new Scalar(Double.POSITIVE_INFINITY);

		RandomVariable valuesUnderlying	= model.getRandomVariableForConstant(0.0);
		for(int exerciseIndex = exerciseDates.length - 1; exerciseIndex >=0; exerciseIndex--) {
			final double exerciseTime = FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, exerciseDates[exerciseIndex]);

			final RandomVariable discountedCashflowFixLeg		= getValueUnderlyingNumeraireRelative(model, fixSchedules[exerciseIndex], false, swaprates[exerciseIndex], notionals[exerciseIndex]);
			final RandomVariable discountedCashflowFloatingLeg	= getValueUnderlyingNumeraireRelative(model, floatSchedules[exerciseIndex], true, 0.0, notionals[exerciseIndex]);

			// Distinguish whether the swaption is of type "Payer" or "Receiver":
			if(swaptionType.equals(SwaptionType.PAYER)) {
				final RandomVariable discountedPayoff = discountedCashflowFloatingLeg.sub(discountedCashflowFixLeg);
				valuesUnderlying = discountedPayoff;//valuesUnderlying.add(discountedPayoff);
			} else if(swaptionType.equals(SwaptionType.RECEIVER)){
				final RandomVariable discountedPayoff = discountedCashflowFixLeg.sub(discountedCashflowFloatingLeg);
				valuesUnderlying = discountedPayoff;//valuesUnderlying.add(discountedPayoff);
			}

			final RandomVariable discountedTriggerValues = values.sub(valuesUnderlying);

			// Remove foresight through condition expectation
			final ConditionalExpectationEstimator conditionalExpectationOperator = getConditionalExpectationEstimator(exerciseTime, model);

			// Calculate conditional expectation. Note that no discounting (numeraire division) is required!
			final RandomVariable triggerValues = discountedTriggerValues.getConditionalExpectation(conditionalExpectationOperator);

			// Apply the exercise criteria
			// if triggerValues(omega) <=0 choose valuesUnderlying else values
			values = triggerValues.choose(values, valuesUnderlying);
			exerciseTimes = triggerValues.choose(exerciseTimes, new Scalar(exerciseTime));
		}


		// Logging the exercise probabilities for every exercise time.
		if(logger.isLoggable(Level.FINE)) {
			logger.fine("Exercise probabilitie " + getExerciseProbabilitiesFromTimes(model.getReferenceDate(), exerciseTimes));
			double probabilityToExercise = 1.0;
			for(int exerciseIndex = 0; exerciseIndex < exerciseDates.length; exerciseIndex++) {
				final double exerciseTime = FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, exerciseDates[exerciseIndex]);
				final double probabilityToExerciseAfter = exerciseTimes.sub(exerciseTime+1.0/365.0).choose(new Scalar(1.0), new Scalar(0.0)).getAverage();
				final double probability = probabilityToExercise - probabilityToExerciseAfter;
				probabilityToExercise = probabilityToExerciseAfter;

				logger.finer("Exercise " + (exerciseIndex+1) + " on " + exerciseDates[exerciseIndex] + " with probability " + probability);
			}
			logger.finer("No exercise with probability " + probabilityToExercise);
		}

		// 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);

		final Map<String, Object> results = new HashMap<>();
		results.put("values", values);
		results.put("exerciseTimes", exerciseTimes);
		return results;
	}

	@Override
	public RandomVariable getValue(final double evaluationTime, final TermStructureMonteCarloSimulationModel model) throws CalculationException {
		return (RandomVariable) getValues(evaluationTime, model).get("values");
	}

	/**
	 * Determines the vector of exercise probabilities for a given {@link RandomVariable} of exerciseTimes.
	 * The exerciseTimes is a random variable of {@link FloatingpointDate} offsets from a given referenceDate.
	 *
	 * @param localDateTime A given reference date.
	 * @param exerciseTimes A {@link RandomVariable} of exercise times given as {@link FloatingpointDate} offsets from the given referenceDate.
	 * @return A vector of exercise probabilities. The length of the vector is <code>exerciseDates.length+1</code>. The last entry is the probability that no exercise occurs.
	 */
	public double[] getExerciseProbabilitiesFromTimes(final LocalDateTime localDateTime, final RandomVariable exerciseTimes) {
		final double[] exerciseProbabilities = new double[exerciseDates.length+1];

		double probabilityToExercise = 1.0;
		for(int exerciseIndex = 0; exerciseIndex < exerciseDates.length; exerciseIndex++) {
			final double exerciseTime = FloatingpointDate.getFloatingPointDateFromDate(localDateTime, exerciseDates[exerciseIndex].atStartOfDay());
			final double probabilityToExerciseAfter = exerciseTimes.sub(exerciseTime+1.0/365.0).choose(new Scalar(1.0), new Scalar(0.0)).getAverage();

			exerciseProbabilities[exerciseIndex] = probabilityToExercise - probabilityToExerciseAfter;
			probabilityToExercise = probabilityToExerciseAfter;
		}
		exerciseProbabilities[exerciseDates.length] = probabilityToExercise;

		return exerciseProbabilities;
	}

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

		for(int exerciseDateIndex = 0; exerciseDateIndex < exerciseDates.length; exerciseDateIndex++) {
			times.add(FloatingpointDate.getFloatingPointDateFromDate(referenceDate, exerciseDates[exerciseDateIndex].atStartOfDay()));

			final Schedule scheduleFixedLeg = fixSchedules[exerciseDateIndex];
			final Schedule scheduleFloatLeg = floatSchedules[exerciseDateIndex];

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

	/**
	 * Calculated the numeraire relative value of an underlying swap leg.
	 *
	 * @param model The Monte Carlo model.
	 * @param legSchedule The schedule of the leg.
	 * @param paysFloat If true a floating rate is payed.
	 * @param swaprate The swaprate. May be 0.0 for pure floating leg.
	 * @param notional The notional.
	 * @return The sum of the numeraire relative cash flows.
	 * @throws CalculationException Thrown if underlying model failed to calculate stochastic process.
	 */
	private RandomVariable getValueUnderlyingNumeraireRelative(final TermStructureMonteCarloSimulationModel model, final Schedule legSchedule, final boolean paysFloat, final double swaprate, final double notional) throws CalculationException {

		if(isUseAnalyticSwapValuationAtExercise) {
			final double valuationTime = FloatingpointDate.getFloatingPointDateFromDate(model.getReferenceDate().toLocalDate(), legSchedule.getPeriod(0).getFixing());
			final RandomVariable numeraireAtValuationTime  = model.getNumeraire(valuationTime);
			final RandomVariable monteCarloProbabilitiesAtValuationTime = model.getMonteCarloWeights(valuationTime);
			RandomVariable value = SwaptionFromSwapSchedules.getValueOfLegAnalytic(valuationTime, model, legSchedule, paysFloat, swaprate, notional);
			value = value.div(model.getNumeraire(valuationTime)).mult(monteCarloProbabilitiesAtValuationTime);
			return value;
		}
		else {

			RandomVariable value	= model.getRandomVariableForConstant(0.0);

			for(int periodIndex = legSchedule.getNumberOfPeriods() - 1; periodIndex >= 0; periodIndex--) {

				final double fixingTime = FloatingpointDate.getFloatingPointDateFromDate(model.getReferenceDate().toLocalDate(), legSchedule.getPeriod(periodIndex).getFixing());
				final double paymentTime = FloatingpointDate.getFloatingPointDateFromDate(model.getReferenceDate().toLocalDate(), legSchedule.getPeriod(periodIndex).getPayment());
				final double periodLength	= legSchedule.getPeriodLength(periodIndex);

				final RandomVariable	numeraireAtPayment  = model.getNumeraire(paymentTime);
				final RandomVariable	monteCarloProbabilitiesAtPayment = model.getMonteCarloWeights(paymentTime);
				if(swaprate != 0.0) {
					final RandomVariable periodCashFlowFix = model.getRandomVariableForConstant(swaprate * periodLength * notional).div(numeraireAtPayment).mult(monteCarloProbabilitiesAtPayment);
					value = value.add(periodCashFlowFix);
				}
				if(paysFloat) {
					final RandomVariable libor = model.getForwardRate(fixingTime, fixingTime, paymentTime);
					final RandomVariable periodCashFlowFloat = libor.mult(periodLength).mult(notional).div(numeraireAtPayment).mult(monteCarloProbabilitiesAtPayment);
					value = value.add(periodCashFlowFloat);
				}
			}
			return value;
		}
	}

	/**
	 * The conditional expectation is calculated using a Monte-Carlo regression technique.
	 *
	 * @param exerciseTime The exercise time
	 * @param model The valuation model
	 * @return The condition expectation estimator
	 * @throws CalculationException Thrown if underlying model failed to calculate stochastic process.
	 */
	public ConditionalExpectationEstimator getConditionalExpectationEstimator(final double exerciseTime, final TermStructureMonteCarloSimulationModel model) throws CalculationException {
		final RandomVariable[] regressionBasisFunctions = regressionBasisFunctionProvider.getBasisFunctions(exerciseTime, model);
		return conditionalExpectationRegressionFactory.getConditionalExpectationEstimator(regressionBasisFunctions, regressionBasisFunctions);
	}


	@Override
	public RandomVariable[] getBasisFunctions(final double evaluationTime, final MonteCarloSimulationModel model) throws CalculationException {
		final LIBORModelMonteCarloSimulationModel liborModel = (LIBORModelMonteCarloSimulationModel)model;
		return getBasisFunctions(evaluationTime, liborModel);
	}

	/**
	 * Provides a set of \( \mathcal{F}_{t} \)-measurable random variables which can serve as regression basis functions.
	 *
	 * @param evaluationTime The evaluation time \( t \) at which the basis function should be observed.
	 * @param model The Monte-Carlo model used to derive the basis function.
	 * @return An \( \mathcal{F}_{t} \)-measurable random variable.
	 * @throws CalculationException Thrown if derivation of the basis function fails.
	 */
	public RandomVariable[] getBasisFunctions(final double evaluationTime, final LIBORModelMonteCarloSimulationModel model) throws CalculationException {

		final LocalDateTime modelReferenceDate = model.getReferenceDate();

		final double[] regressionBasisfunctionTimes = Stream.concat(Arrays.stream(exerciseDates),Stream.of(swapEndDate)).mapToDouble(new ToDoubleFunction<LocalDate>() {
			@Override
			public double applyAsDouble(final LocalDate date) { return FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, date.atStartOfDay()); }
		}).sorted().toArray();

		final ArrayList<RandomVariable> basisFunctions = new ArrayList<>();

		final double exerciseTime = evaluationTime;

		int exerciseIndex = Arrays.binarySearch(regressionBasisfunctionTimes, exerciseTime);
		if(exerciseIndex < 0) {
			exerciseIndex = -exerciseIndex;
		}
		if(exerciseIndex >= exerciseDates.length) {
			exerciseIndex = exerciseDates.length-1;
		}

		// Constant
		final RandomVariable one = new RandomVariableFromDoubleArray(1.0);
		basisFunctions.add(one);

		// Numeraire (adapted to multicurve framework)
		final RandomVariable discountFactor = model.getNumeraire(exerciseTime).invert();
		basisFunctions.add(discountFactor);

		/*
		 * Add swap rates of underlyings.
		 */
		for(int exerciseIndexUnderlying = exerciseIndex; exerciseIndexUnderlying<exerciseDates.length; exerciseIndexUnderlying++) {
			final RandomVariable floatLeg = SwaptionFromSwapSchedules.getValueOfLegAnalytic(exerciseTime, model, floatSchedules[exerciseIndexUnderlying], true, 0.0, 1.0);
			final RandomVariable annuity = SwaptionFromSwapSchedules.getValueOfLegAnalytic(exerciseTime, model, fixSchedules[exerciseIndexUnderlying], false, 1.0, 1.0);
			final RandomVariable swapRate = floatLeg.div(annuity);
			final RandomVariable basisFunction = swapRate.mult(discountFactor);
			basisFunctions.add(basisFunction);
			basisFunctions.add(basisFunction.squared());
		}

		// forward rate to the next period
		final RandomVariable rateShort = model.getForwardRate(exerciseTime, exerciseTime, regressionBasisfunctionTimes[exerciseIndex + 1]);
		basisFunctions.add(rateShort.mult(discountFactor));
		basisFunctions.add(rateShort.mult(discountFactor).pow(2.0));

		return basisFunctions.toArray(new RandomVariable[basisFunctions.size()]);
	}

	/*
	 * Some popular variants to create regression basis functions
	 */

	public RegressionBasisFunctionsProvider getBasisFunctionsProviderWithSwapRates() {
		return new RegressionBasisFunctionsProvider() {
			@Override
			public RandomVariable[] getBasisFunctions(final double evaluationTime, final MonteCarloSimulationModel monteCarloModel) throws CalculationException {
				final LIBORModelMonteCarloSimulationModel model = (LIBORModelMonteCarloSimulationModel)monteCarloModel;

				final LocalDateTime modelReferenceDate = model.getReferenceDate();

				final double[] regressionBasisfunctionTimes = Stream.concat(Arrays.stream(exerciseDates),Stream.of(swapEndDate)).mapToDouble(new ToDoubleFunction<LocalDate>() {
					@Override
					public double applyAsDouble(final LocalDate date) { return FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, date.atStartOfDay()); }
				}).sorted().toArray();

				final ArrayList<RandomVariable> basisFunctions = new ArrayList<>();

				final double exerciseTime = evaluationTime;

				int exerciseIndex = Arrays.binarySearch(regressionBasisfunctionTimes, exerciseTime);
				if(exerciseIndex < 0) {
					exerciseIndex = -exerciseIndex;
				}
				if(exerciseIndex >= exerciseDates.length) {
					exerciseIndex = exerciseDates.length-1;
				}

				// Constant
				final RandomVariable one = new RandomVariableFromDoubleArray(1.0);
				final RandomVariable basisFunction = one;
				basisFunctions.add(basisFunction);

				/*
				 * Add swap rates of underlyings.
				 */
				for(int exerciseIndexUnderlying = exerciseIndex; exerciseIndexUnderlying<exerciseDates.length; exerciseIndexUnderlying++) {
					final RandomVariable floatLeg = SwaptionFromSwapSchedules.getValueOfLegAnalytic(exerciseTime, model, floatSchedules[exerciseIndexUnderlying], true, 0.0, 1.0);
					final RandomVariable annuity = SwaptionFromSwapSchedules.getValueOfLegAnalytic(exerciseTime, model, fixSchedules[exerciseIndexUnderlying], false, 1.0, 1.0);
					final RandomVariable swapRate = floatLeg.div(annuity);
					basisFunctions.add(swapRate);
				}

				// forward rate to the next period
				final RandomVariable rateShort = model.getForwardRate(exerciseTime, exerciseTime, regressionBasisfunctionTimes[exerciseIndex + 1]);
				basisFunctions.add(rateShort);
				basisFunctions.add(rateShort.pow(2.0));

				// Numeraire (adapted to multicurve framework)
				final RandomVariable discountFactor = model.getNumeraire(exerciseTime).invert();
				basisFunctions.add(discountFactor);

				return basisFunctions.toArray(new RandomVariable[basisFunctions.size()]);
			}
		};
	}

	public RegressionBasisFunctionsProvider getBasisFunctionsProviderWithForwardRates() {
		return new RegressionBasisFunctionsProvider() {
			@Override
			public RandomVariable[] getBasisFunctions(final double evaluationTime, final MonteCarloSimulationModel monteCarloModel) throws CalculationException {
				final LIBORModelMonteCarloSimulationModel model = (LIBORModelMonteCarloSimulationModel)monteCarloModel;

				final LocalDateTime modelReferenceDate = model.getReferenceDate();

				final double[] regressionBasisfunctionTimes = Stream.concat(Arrays.stream(exerciseDates),Stream.of(swapEndDate)).mapToDouble(new ToDoubleFunction<LocalDate>() {
					@Override
					public double applyAsDouble(final LocalDate date) { return FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, date.atStartOfDay()); }
				}).sorted().toArray();

				final ArrayList<RandomVariable> basisFunctions = new ArrayList<>();

				final double swapMaturity = FloatingpointDate.getFloatingPointDateFromDate(referenceDate, swapEndDate.atStartOfDay());

				final double exerciseTime = evaluationTime;

				int exerciseIndex = Arrays.binarySearch(regressionBasisfunctionTimes, exerciseTime);
				if(exerciseIndex < 0) {
					exerciseIndex = -exerciseIndex;
				}
				if(exerciseIndex >= exerciseDates.length) {
					exerciseIndex = exerciseDates.length-1;
				}

				// Constant
				final RandomVariable one = new RandomVariableFromDoubleArray(1.0);

				final RandomVariable basisFunction = one;
				basisFunctions.add(basisFunction);

				// forward rate to the next period
				final RandomVariable rateShort = model.getForwardRate(exerciseTime, exerciseTime, regressionBasisfunctionTimes[exerciseIndex + 1]);
				basisFunctions.add(rateShort);
				basisFunctions.add(rateShort.pow(2.0));

				// forward rate to the end of the product
				final RandomVariable rateLong = model.getForwardRate(exerciseTime, regressionBasisfunctionTimes[exerciseIndex], swapMaturity);
				basisFunctions.add(rateLong);
				basisFunctions.add(rateLong.pow(2.0));

				// Numeraire (adapted to multicurve framework)
				final RandomVariable discountFactor = model.getNumeraire(exerciseTime).invert();
				basisFunctions.add(discountFactor);

				// Cross
				basisFunctions.add(rateLong.mult(discountFactor));

				return basisFunctions.toArray(new RandomVariable[basisFunctions.size()]);
			}
		};
	}

	@Override
	public String toString() {
		return "BermudanSwaptionFromSwapSchedules[type: "  + swaptionType.toString() + ", " +"exerciseDate: "  + Arrays.toString(exerciseDates)  +", " + "swapEndDate: " + swapEndDate + ", " + "strike: " + Arrays.toString(swaprates) + ", " + "floatingTenor: " + Arrays.toString(floatSchedules) + ", " + "fixTenor: " + Arrays.toString(fixSchedules) + "]";
	}
}


