com.opengamma.strata.pricer.capfloor.SurfaceIborCapletFloorletVolatilityBootstrapper Maven / Gradle / Ivy
/*
* Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.capfloor;
import static com.opengamma.strata.market.ValueType.NORMAL_VOLATILITY;
import java.time.LocalDate;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import com.opengamma.strata.basics.ReferenceData;
import com.opengamma.strata.basics.index.IborIndex;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.array.DoubleArray;
import com.opengamma.strata.collect.array.DoubleMatrix;
import com.opengamma.strata.market.curve.Curve;
import com.opengamma.strata.market.param.CurrencyParameterSensitivities;
import com.opengamma.strata.market.sensitivity.PointSensitivities;
import com.opengamma.strata.market.surface.InterpolatedNodalSurface;
import com.opengamma.strata.market.surface.Surface;
import com.opengamma.strata.market.surface.SurfaceMetadata;
import com.opengamma.strata.market.surface.Surfaces;
import com.opengamma.strata.pricer.impl.option.GenericImpliedVolatiltySolver;
import com.opengamma.strata.pricer.option.RawOptionData;
import com.opengamma.strata.pricer.rate.RatesProvider;
import com.opengamma.strata.product.capfloor.ResolvedIborCapFloorLeg;
/**
* Caplet volatilities calibration to cap volatilities based on interpolated surface.
*
* The caplet volatilities are computed by bootstrapping along the expiry time dimension.
* The result is an interpolated surface spanned by expiry and strike.
* The position of the node points on the resultant surface corresponds to last expiry date of market caps.
* The nodes should be interpolated by a local interpolation scheme along the time direction.
* See {@link SurfaceIborCapletFloorletVolatilityBootstrapDefinition} for detail.
*
* If the shift curve is not present in {@code SurfaceIborCapletFloorletBootstrapVolatilityDefinition},
* the resultant volatility type is the same as the input volatility type, i.e.,
* Black caplet volatilities are returned if Black cap volatilities are plugged in, and normal caplet volatilities are
* returned otherwise.
* On the other hand, if the shift curve is present in {@code SurfaceIborCapletFloorletBootstrapVolatilityDefinition},
* Black caplet volatilities are returned for any input volatility type.
*/
public class SurfaceIborCapletFloorletVolatilityBootstrapper extends IborCapletFloorletVolatilityCalibrator {
/**
* Default implementation.
*/
public static final SurfaceIborCapletFloorletVolatilityBootstrapper DEFAULT = of(
VolatilityIborCapFloorLegPricer.DEFAULT, ReferenceData.standard());
//-------------------------------------------------------------------------
/**
* Creates an instance.
*
* @param pricer the cap pricer
* @param referenceData the reference data
* @return the instance
*/
public static SurfaceIborCapletFloorletVolatilityBootstrapper of(
VolatilityIborCapFloorLegPricer pricer,
ReferenceData referenceData) {
return new SurfaceIborCapletFloorletVolatilityBootstrapper(pricer, referenceData);
}
// private constructor
private SurfaceIborCapletFloorletVolatilityBootstrapper(VolatilityIborCapFloorLegPricer pricer, ReferenceData referenceData) {
super(pricer, referenceData);
}
//-------------------------------------------------------------------------
@Override
public IborCapletFloorletVolatilityCalibrationResult calibrate(
IborCapletFloorletVolatilityDefinition definition,
ZonedDateTime calibrationDateTime,
RawOptionData capFloorData,
RatesProvider ratesProvider) {
ArgChecker.isTrue(ratesProvider.getValuationDate().equals(calibrationDateTime.toLocalDate()),
"valuationDate of ratesProvider should be coherent to calibrationDateTime");
ArgChecker.isTrue(definition instanceof SurfaceIborCapletFloorletVolatilityBootstrapDefinition,
"definition should be SurfaceIborCapletFloorletVolatilityBootstrapDefinition");
SurfaceIborCapletFloorletVolatilityBootstrapDefinition bsDefinition =
(SurfaceIborCapletFloorletVolatilityBootstrapDefinition) definition;
IborIndex index = bsDefinition.getIndex();
LocalDate calibrationDate = calibrationDateTime.toLocalDate();
LocalDate baseDate = index.getEffectiveDateOffset().adjust(calibrationDate, getReferenceData());
LocalDate startDate = baseDate.plus(index.getTenor());
Function volatilitiesFunction = volatilitiesFunction(
bsDefinition, calibrationDateTime, capFloorData);
SurfaceMetadata metadata = bsDefinition.createMetadata(capFloorData);
List expiries = capFloorData.getExpiries();
int nExpiries = expiries.size();
DoubleArray strikes = capFloorData.getStrikes();
DoubleMatrix errorsMatrix = capFloorData.getError().orElse(DoubleMatrix.filled(nExpiries, strikes.size(), 1d));
List timeList = new ArrayList<>();
List strikeList = new ArrayList<>();
List volList = new ArrayList<>();
List capList = new ArrayList<>();
List priceList = new ArrayList<>();
List errorList = new ArrayList<>();
int[] startIndex = new int[nExpiries + 1];
for (int i = 0; i < nExpiries; ++i) {
LocalDate endDate = baseDate.plus(expiries.get(i));
DoubleArray volatilityData = capFloorData.getData().row(i);
DoubleArray errors = errorsMatrix.row(i);
reduceRawData(bsDefinition, ratesProvider, strikes, volatilityData, errors, startDate, endDate, metadata,
volatilitiesFunction, timeList, strikeList, volList, capList, priceList, errorList);
startIndex[i + 1] = volList.size();
ArgChecker.isTrue(startIndex[i + 1] > startIndex[i], "no valid option data for {}", expiries.get(i));
}
int nTotal = startIndex[nExpiries];
IborCapletFloorletVolatilities vols;
int start;
ZonedDateTime prevExpiry;
DoubleArray initialVol = DoubleArray.copyOf(volList);
if (bsDefinition.getShiftCurve().isPresent()) {
Curve shiftCurve = bsDefinition.getShiftCurve().get();
DoubleArray strikeShifted = DoubleArray.of(nTotal, n -> strikeList.get(n) + shiftCurve.yValue(timeList.get(n)));
if (capFloorData.getDataType().equals(NORMAL_VOLATILITY)) { // correct initial surface
metadata = Surfaces.blackVolatilityByExpiryStrike(bsDefinition.getName().getName(), bsDefinition.getDayCount())
.withParameterMetadata(metadata.getParameterMetadata().get());
initialVol = DoubleArray.of(nTotal, n -> volList.get(n) /
(ratesProvider.iborIndexRates(index).rate(capList.get(n).getFinalPeriod().getIborRate().getObservation()) +
shiftCurve.yValue(timeList.get(n))));
}
InterpolatedNodalSurface surface = InterpolatedNodalSurface.of(
metadata, DoubleArray.copyOf(timeList), strikeShifted, initialVol, bsDefinition.getInterpolator());
vols = ShiftedBlackIborCapletFloorletExpiryStrikeVolatilities.of(
index, calibrationDateTime, surface, bsDefinition.getShiftCurve().get());
start = 0;
prevExpiry = calibrationDateTime.minusDays(1L); // included if calibrationDateTime == fixingDateTime
} else {
InterpolatedNodalSurface surface = InterpolatedNodalSurface.of(
metadata, DoubleArray.copyOf(timeList), DoubleArray.copyOf(strikeList), initialVol, bsDefinition.getInterpolator());
vols = volatilitiesFunction.apply(surface);
start = 1;
prevExpiry = capList.get(startIndex[1] - 1).getFinalFixingDateTime();
}
for (int i = start; i < nExpiries; ++i) {
for (int j = startIndex[i]; j < startIndex[i + 1]; ++j) {
Function func = getValueVegaFunction(capList.get(j), ratesProvider, vols, prevExpiry, j);
GenericImpliedVolatiltySolver solver = new GenericImpliedVolatiltySolver(func);
double priceFixed = i == 0 ? 0d : priceFixed(capList.get(j), ratesProvider, vols, prevExpiry);
double capletVol = solver.impliedVolatility(priceList.get(j) - priceFixed, initialVol.get(j));
vols = vols.withParameter(j, capletVol);
}
prevExpiry = capList.get(startIndex[i + 1] - 1).getFinalFixingDateTime();
}
return IborCapletFloorletVolatilityCalibrationResult.ofRootFind(vols);
}
//-------------------------------------------------------------------------
// price and vega function
private Function getValueVegaFunction(
ResolvedIborCapFloorLeg cap,
RatesProvider ratesProvider,
IborCapletFloorletVolatilities vols,
ZonedDateTime prevExpiry,
int nodeIndex) {
VolatilityIborCapletFloorletPeriodPricer periodPricer = getLegPricer().getPeriodPricer();
Function priceAndVegaFunction = new Function() {
@Override
public double[] apply(Double x) {
IborCapletFloorletVolatilities newVols = vols.withParameter(nodeIndex, x);
double price = cap.getCapletFloorletPeriods().stream()
.filter(p -> p.getFixingDateTime().isAfter(prevExpiry))
.mapToDouble(p -> periodPricer.presentValue(p, ratesProvider, newVols).getAmount())
.sum();
PointSensitivities point = cap.getCapletFloorletPeriods().stream()
.filter(p -> p.getFixingDateTime().isAfter(prevExpiry))
.map(p -> periodPricer.presentValueSensitivityModelParamsVolatility(p, ratesProvider, newVols))
.reduce((c1, c2) -> c1.combinedWith(c2))
.get()
.build();
CurrencyParameterSensitivities sensi = newVols.parameterSensitivity(point);
double vega = sensi.getSensitivities().get(0).getSensitivity().get(nodeIndex);
return new double[] {price, vega};
}
};
return priceAndVegaFunction;
}
// sum of caplet prices which are already fixed
private double priceFixed(
ResolvedIborCapFloorLeg cap,
RatesProvider ratesProvider,
IborCapletFloorletVolatilities vols,
ZonedDateTime prevExpiry) {
VolatilityIborCapletFloorletPeriodPricer periodPricer = getLegPricer().getPeriodPricer();
return cap.getCapletFloorletPeriods().stream()
.filter(p -> !p.getFixingDateTime().isAfter(prevExpiry))
.mapToDouble(p -> periodPricer.presentValue(p, ratesProvider, vols).getAmount())
.sum();
}
}