Bond.java
package net.finmath.finitedifference.interestrate.products;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import net.finmath.finitedifference.grids.SpaceTimeDiscretization;
import net.finmath.finitedifference.interestrate.models.FiniteDifferenceInterestRateModel;
import net.finmath.finitedifference.solvers.FDMThetaMethod1D;
import net.finmath.modelling.EuropeanExercise;
import net.finmath.time.Period;
import net.finmath.time.Schedule;
/**
* Finite-difference valuation of a deterministic-cashflow bond.
*
* <p>
* This class is the reduced-scope PDE analogue of the analytic bond products in
* finmath. It is intentionally restricted to deterministic cashflows:
* </p>
* <ul>
* <li>zero-coupon bonds,</li>
* <li>fixed-coupon bonds,</li>
* <li>deterministic redemption at maturity.</li>
* </ul>
*
* <p>
* The class does <b>not</b> yet support floating coupons, credit-risk survival
* curves, basis-factor curves, or recovery payments.
* </p>
*
* <p>
* Let {@code T_i} denote the payment dates of the schedule, {@code \delta_i}
* the corresponding accrual factors, {@code c} the fixed coupon rate,
* {@code N} the notional, and {@code R} the redemption paid on the final
* payment date. The deterministic cashflow paid at {@code T_i} is
* </p>
*
* <p>
* <i>
* C_i = N c \delta_i
* </i>
* </p>
*
* <p>
* for intermediate periods, and
* </p>
*
* <p>
* <i>
* C_n = N c \delta_n + R
* </i>
* </p>
*
* <p>
* on the final payment date.
* </p>
*
* <p>
* In the event-based finite-difference framework this is represented through
* jumps of the value function at payment dates:
* </p>
*
* <p>
* <i>
* V(T_i^{-},x) = V(T_i^{+},x) + C_i.
* </i>
* </p>
*
* <p>
* The current implementation uses the one-dimensional theta-method solver and
* therefore requires a one-dimensional finite-difference interest-rate model.
* </p>
*
* @author Alessandro Gnoatto
*/
public class Bond implements FiniteDifferenceInterestRateProduct {
/**
* The event time tolerance.
*/
private static final double EVENT_TIME_TOLERANCE = 1E-12;
/**
* The schedule.
*/
private final Schedule schedule;
/**
* The fixed coupon.
*/
private final double fixedCoupon;
/**
* The notional.
*/
private final double notional;
/**
* The redemption.
*/
private final double redemption;
/**
* The maturity.
*/
private final double maturity;
/**
* Creates a fixed-coupon bond with redemption equal to the notional.
*
* @param schedule The payment schedule.
* @param fixedCoupon The fixed coupon rate.
* @param notional The notional.
*/
public Bond(
final Schedule schedule,
final double fixedCoupon,
final double notional) {
this(schedule, fixedCoupon, notional, notional);
}
/**
* Creates a deterministic-cashflow bond.
*
* @param schedule The payment schedule.
* @param fixedCoupon The fixed coupon rate.
* @param notional The notional.
* @param redemption The redemption paid on the final payment date.
*/
public Bond(
final Schedule schedule,
final double fixedCoupon,
final double notional,
final double redemption) {
if (schedule == null) {
throw new IllegalArgumentException("schedule must not be null.");
}
if (schedule.getNumberOfPeriods() <= 0) {
throw new IllegalArgumentException("schedule must contain at least one period.");
}
if (fixedCoupon < 0.0) {
throw new IllegalArgumentException("fixedCoupon must be non-negative.");
}
if (notional < 0.0) {
throw new IllegalArgumentException("notional must be non-negative.");
}
if (redemption < 0.0) {
throw new IllegalArgumentException("redemption must be non-negative.");
}
this.schedule = schedule;
this.fixedCoupon = fixedCoupon;
this.notional = notional;
this.redemption = redemption;
this.maturity = schedule.getPayment(schedule.getNumberOfPeriods() - 1);
validateSchedule();
}
/**
* Creates a zero-coupon bond with unit notional.
*
* @param maturity The maturity.
* @return A zero-coupon bond.
*/
public static Bond ofZeroCouponBond(final double maturity) {
return ofZeroCouponBond(maturity, 1.0);
}
/**
* Creates a zero-coupon bond.
*
* @param maturity The maturity.
* @param notional The notional paid at maturity.
* @return A zero-coupon bond.
*/
public static Bond ofZeroCouponBond(final double maturity, final double notional) {
final Schedule schedule = new SinglePaymentSchedule(maturity);
return new Bond(schedule, 0.0, notional, notional);
}
@Override
public double[] getValue(
final double evaluationTime,
final FiniteDifferenceInterestRateModel model) {
validateModel(model);
final FDMThetaMethod1D solver = new FDMThetaMethod1D(
model,
this,
model.getSpaceTimeDiscretization(),
new EuropeanExercise(maturity)
);
return solver.getValue(
evaluationTime,
maturity,
buildZeroTerminalValues(model)
);
}
@Override
public double[][] getValues(final FiniteDifferenceInterestRateModel model) {
validateModel(model);
final FDMThetaMethod1D solver = new FDMThetaMethod1D(
model,
this,
model.getSpaceTimeDiscretization(),
new EuropeanExercise(maturity)
);
return solver.getValues(
maturity,
buildZeroTerminalValues(model)
);
}
@Override
public double[] getEventTimes() {
final double[] eventTimes = new double[schedule.getNumberOfPeriods()];
for (int periodIndex = 0; periodIndex < schedule.getNumberOfPeriods(); periodIndex++) {
eventTimes[periodIndex] = schedule.getPayment(periodIndex);
}
return eventTimes;
}
@Override
public double[] applyEventCondition(
final double time,
final double[] valuesAfterEvent,
final FiniteDifferenceInterestRateModel model) {
if (valuesAfterEvent == null) {
throw new IllegalArgumentException("valuesAfterEvent must not be null.");
}
final double payment = getCashflowAt(time);
if (payment == 0.0) {
return valuesAfterEvent;
}
final double[] valuesBeforeEvent = valuesAfterEvent.clone();
for (int i = 0; i < valuesBeforeEvent.length; i++) {
valuesBeforeEvent[i] += payment;
}
return valuesBeforeEvent;
}
/**
* Returns the payment schedule.
*
* @return The payment schedule.
*/
public Schedule getSchedule() {
return schedule;
}
/**
* Returns the fixed coupon rate.
*
* @return The fixed coupon rate.
*/
public double getFixedCoupon() {
return fixedCoupon;
}
/**
* Returns the notional.
*
* @return The notional.
*/
public double getNotional() {
return notional;
}
/**
* Returns the redemption amount paid on the final payment date.
*
* @return The redemption amount.
*/
public double getRedemption() {
return redemption;
}
/**
* Returns the maturity, equal to the last payment date of the schedule.
*
* @return The maturity.
*/
public double getMaturity() {
return maturity;
}
/**
* Returns the deterministic cashflow paid at the given period index.
*
* @param periodIndex The period index.
* @return The cashflow paid at the corresponding payment date.
*/
public double getCashflow(final int periodIndex) {
validatePeriodIndex(periodIndex);
double cashflow = notional * fixedCoupon * schedule.getPeriodLength(periodIndex);
if (periodIndex == schedule.getNumberOfPeriods() - 1) {
cashflow += redemption;
}
return cashflow;
}
@Override
public String toString() {
return "Bond [schedule=" + schedule
+ ", fixedCoupon=" + fixedCoupon
+ ", notional=" + notional
+ ", redemption=" + redemption
+ ", eventTimes=" + Arrays.toString(getEventTimes())
+ "]";
}
private void validateSchedule() {
double previousPayment = -1.0;
for (int periodIndex = 0; periodIndex < schedule.getNumberOfPeriods(); periodIndex++) {
final double payment = schedule.getPayment(periodIndex);
final double periodLength = schedule.getPeriodLength(periodIndex);
if (payment < 0.0) {
throw new IllegalArgumentException("Schedule payment times must be non-negative.");
}
if (periodLength < 0.0) {
throw new IllegalArgumentException("Schedule period lengths must be non-negative.");
}
if (periodIndex > 0 && payment <= previousPayment) {
throw new IllegalArgumentException("Schedule payment times must be strictly increasing.");
}
previousPayment = payment;
}
}
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(
"Bond currently supports only one-dimensional finite-difference interest-rate models."
);
}
}
private void validatePeriodIndex(final int periodIndex) {
if (periodIndex < 0 || periodIndex >= schedule.getNumberOfPeriods()) {
throw new IllegalArgumentException("periodIndex out of bounds.");
}
}
private double getCashflowAt(final double time) {
double totalCashflow = 0.0;
for (int periodIndex = 0; periodIndex < schedule.getNumberOfPeriods(); periodIndex++) {
if (Math.abs(time - schedule.getPayment(periodIndex)) <= EVENT_TIME_TOLERANCE) {
totalCashflow += getCashflow(periodIndex);
}
}
return totalCashflow;
}
private double[] buildZeroTerminalValues(final FiniteDifferenceInterestRateModel model) {
final SpaceTimeDiscretization discretization = model.getSpaceTimeDiscretization();
final double[] xGrid = discretization.getSpaceGrid(0).getGrid();
return new double[xGrid.length];
}
/**
* Minimal schedule used for the zero-coupon-bond factory.
*
* <p>
* It represents one single period ending at the payment date.
* </p>
*
* @author Alessandro Gnoatto
*/
private static final class SinglePaymentSchedule implements Schedule {
/**
* The maturity.
*/
private final double maturity;
private SinglePaymentSchedule(final double maturity) {
if (maturity < 0.0) {
throw new IllegalArgumentException("maturity must be non-negative.");
}
this.maturity = maturity;
}
@Override
public int getNumberOfPeriods() {
return 1;
}
@Override
public double getFixing(final int periodIndex) {
validatePeriodIndex(periodIndex);
return maturity;
}
@Override
public double getPayment(final int periodIndex) {
validatePeriodIndex(periodIndex);
return maturity;
}
@Override
public double getPeriodStart(final int periodIndex) {
validatePeriodIndex(periodIndex);
return 0.0;
}
@Override
public double getPeriodEnd(final int periodIndex) {
validatePeriodIndex(periodIndex);
return maturity;
}
@Override
public double getPeriodLength(final int periodIndex) {
validatePeriodIndex(periodIndex);
return 0.0;
}
@Override
public int getPeriodIndex(final double time) {
return Math.abs(time - maturity) <= EVENT_TIME_TOLERANCE ? 0 : -1;
}
@Override
public int getPeriodIndex(final java.time.LocalDate date) {
throw new UnsupportedOperationException("SinglePaymentSchedule does not support LocalDate lookup.");
}
@Override
public net.finmath.time.Period getPeriod(final int periodIndex) {
throw new UnsupportedOperationException("SinglePaymentSchedule does not provide Period objects.");
}
@Override
public java.time.LocalDate getReferenceDate() {
return null;
}
@Override
public net.finmath.time.daycount.DayCountConvention getDaycountconvention() {
return null;
}
@Override
public List<Period> getPeriods() {
throw new UnsupportedOperationException("SinglePaymentSchedule does not provide Period objects.");
}
@Override
public Iterator<Period> iterator() {
throw new UnsupportedOperationException("SinglePaymentSchedule does not provide Period iteration.");
}
private void validatePeriodIndex(final int periodIndex) {
if (periodIndex != 0) {
throw new IllegalArgumentException("SinglePaymentSchedule supports only period index 0.");
}
}
}
}