SwapAnnuity.java
/*
* (c) Copyright Christian P. Fries, Germany. Contact: email@christian-fries.de.
*
* Created on 10.02.2026
*/
package net.finmath.montecarlo.interestrate.products;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.Objects;
import net.finmath.exception.CalculationException;
import net.finmath.montecarlo.interestrate.TermStructureMonteCarloSimulationModel;
import net.finmath.stochastic.RandomVariable;
import net.finmath.stochastic.Scalar;
import net.finmath.time.FloatingpointDate;
import net.finmath.time.TimeDiscretization;
/**
* This class implements the valuation of a simple swap annuity, i.e.,
* the \( \sum_{i=1}^{n} \delta_{i} P(T_{i}) \) where \( \delta_{i} \) are given weights (period length) and
* \( T_{i} \) are given maturities.
*
* @author Christian Fries
* @version 1.2
*/
public class SwapAnnuity extends AbstractTermStructureMonteCarloProduct {
private final LocalDateTime referenceDate;
private final double[] maturities;
private final double[] periodLengths;
/**
* Create a swap annuity with maturity \( T_{i} \) and period length \( \delta_{i} \).
* Here \( T_{i} \) is specified as a double offset to a given reference date \( t = 0 \).
*
* @param referenceDate The date corresponding to \( t = 0 \).
* @param maturities The maturities given as doubles, following the
* {@link FloatingpointDate} convention as offsets to {@code referenceDate}.
* @param periodLengths The period lengths (cash-flow weights).
*/
public SwapAnnuity(final LocalDateTime referenceDate, final double[] maturities, final double[] periodLengths) {
super();
this.referenceDate = referenceDate;
validateSchedule(maturities, periodLengths);
this.maturities = maturities.clone();
this.periodLengths = periodLengths.clone();
}
/**
* @param maturities The maturities given as double.
* @param periodLengths The period lengths (cash-flow weights).
*/
public SwapAnnuity(final double[] maturities, final double[] periodLengths) {
this(null, maturities, periodLengths);
}
/**
* Creates a swap annuity from a tenor discretization. Each time step is one
* weighted payment at the end of the step.
*
* @param timeDiscretization The payment tenor.
*/
public SwapAnnuity(final TimeDiscretization timeDiscretization) {
this.referenceDate = null;
Objects.requireNonNull(timeDiscretization, "timeDiscretization must not be null.");
this.maturities = new double[timeDiscretization.getNumberOfTimeSteps()];
this.periodLengths = new double[timeDiscretization.getNumberOfTimeSteps()];
for(int timeIndex = 0; timeIndex<maturities.length; timeIndex++) {
maturities[timeIndex] = timeDiscretization.getTime(timeIndex+1);
periodLengths[timeIndex] = timeDiscretization.getTimeStep(timeIndex);
}
validateSchedule(maturities, periodLengths);
}
private static void validateSchedule(final double[] maturities, final double[] periodLengths) {
Objects.requireNonNull(maturities, "maturities must not be null.");
Objects.requireNonNull(periodLengths, "periodLengths must not be null.");
if(maturities.length != periodLengths.length) {
throw new IllegalArgumentException("maturities and periodLengths must have the same length.");
}
for(int periodIndex = 0; periodIndex < maturities.length; periodIndex++) {
if(!Double.isFinite(maturities[periodIndex])) {
throw new IllegalArgumentException("maturities must contain only finite values.");
}
if(!Double.isFinite(periodLengths[periodIndex]) || periodLengths[periodIndex] < 0.0) {
throw new IllegalArgumentException("periodLengths must contain only finite, non-negative values.");
}
}
}
/**
* This method returns the value random variable of the product within the specified model, evaluated at a given evalutationTime.
* Note: For a lattice this is often the value conditional to evalutationTime, for a Monte-Carlo simulation this is the (sum of) value discounted to evaluation time.
* Cashflows prior evaluationTime are not considered.
*
* @param evaluationTime The time on which this products value should be observed.
* @param model The model used to price the product.
* @return The random variable representing the value of the product discounted to evaluation time
* @throws net.finmath.exception.CalculationException Thrown if the valuation fails, specific cause may be available via the <code>cause()</code> method.
*/
@Override
public RandomVariable getValue(final double evaluationTime, final TermStructureMonteCarloSimulationModel model) throws CalculationException {
final double productToModelTimeOffset = getProductToModelTimeOffset(model);
// Aggregation of value
RandomVariable values = Scalar.of(0.0);
for(int periodIndex=0; periodIndex < maturities.length; periodIndex++) {
final double maturity = maturities[periodIndex];
final double paymentTime = productToModelTimeOffset + maturity;
if(paymentTime < evaluationTime - Bond.PAYMENT_TIME_TOLERANCE) {
continue;
}
// Get random variables
final RandomVariable numeraire = model.getNumeraire(paymentTime);
final RandomVariable monteCarloProbabilities = model.getMonteCarloWeights(paymentTime);
// Calculate numeraire relative value
RandomVariable valueOfPeriod = model.getRandomVariableForConstant(periodLengths[periodIndex]);
valueOfPeriod = valueOfPeriod.div(numeraire).mult(monteCarloProbabilities);
values = values.add(valueOfPeriod);
}
// Convert back to values
final RandomVariable numeraireAtEvaluationTime = model.getNumeraire(evaluationTime);
final RandomVariable monteCarloProbabilitiesAtEvaluationTime = model.getMonteCarloWeights(evaluationTime);
values = values.mult(numeraireAtEvaluationTime).div(monteCarloProbabilitiesAtEvaluationTime);
// Return values
return values;
}
double getProductToModelTimeOffset(final TermStructureMonteCarloSimulationModel model) {
if(referenceDate == null) {
return 0.0;
}
final LocalDateTime modelReferenceDate;
try {
modelReferenceDate = model.getReferenceDate();
}
catch(final UnsupportedOperationException exception) {
throw new IllegalArgumentException(
"The model must provide a reference date to value a date-based SwapAnnuity.",
exception);
}
if(modelReferenceDate == null) {
throw new IllegalArgumentException(
"The model must provide a reference date to value a date-based SwapAnnuity.");
}
return FloatingpointDate.getFloatingPointDateFromDate(modelReferenceDate, referenceDate);
}
public LocalDateTime getReferenceDate() {
return referenceDate;
}
/**
* @return Returns the maturities.
*/
public double[] getMaturities() {
return maturities.clone();
}
/**
* @return Returns the period lengths (cash-flow weights).
*/
public double[] getPeriodLengths() {
return periodLengths.clone();
}
@Override
public String toString() {
return "SwapAnnuity [referenceDate=" + referenceDate + ", maturities=" + Arrays.toString(maturities)
+ ", periodLengths=" + Arrays.toString(periodLengths) + "]";
}
}