/**
* Copyright (C) 2015 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.sensitivity;
import static com.opengamma.strata.collect.Guavate.toImmutableList;
import java.util.ArrayList;
import java.util.List;
import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.IntStream;
import com.google.common.primitives.Doubles;
import com.opengamma.strata.basics.currency.Currency;
import com.opengamma.strata.basics.index.Index;
import com.opengamma.strata.basics.index.PriceIndex;
import com.opengamma.strata.basics.index.RateIndex;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.collect.array.DoubleArray;
import com.opengamma.strata.collect.array.DoubleMatrix;
import com.opengamma.strata.collect.tuple.Pair;
import com.opengamma.strata.data.MarketDataName;
import com.opengamma.strata.market.curve.Curve;
import com.opengamma.strata.market.curve.NodalCurve;
import com.opengamma.strata.market.curve.ParallelShiftedCurve;
import com.opengamma.strata.market.param.CrossGammaParameterSensitivities;
import com.opengamma.strata.market.param.CrossGammaParameterSensitivity;
import com.opengamma.strata.market.param.CurrencyParameterSensitivities;
import com.opengamma.strata.market.param.CurrencyParameterSensitivity;
import com.opengamma.strata.market.param.ParameterMetadata;
import com.opengamma.strata.math.impl.differentiation.FiniteDifferenceType;
import com.opengamma.strata.math.impl.differentiation.VectorFieldFirstOrderDifferentiator;
import com.opengamma.strata.pricer.rate.ImmutableRatesProvider;
import com.opengamma.strata.pricer.rate.RatesProvider;
/**
* Computes the gamma-related values for the rates curve parameters.
* <p>
* By default the gamma is computed using a one basis-point shift and a forward finite difference.
* The results themselves are not scaled (they represent the second order derivative).
* <p>
* Reference: Interest Rate Cross-gamma for Single and Multiple Curves. OpenGamma quantitative research 15, July 14
*/
public final class CurveGammaCalculator {
/**
* Default implementation. Finite difference is forward and the shift is one basis point (0.0001).
*/
public static final CurveGammaCalculator DEFAULT = new CurveGammaCalculator(FiniteDifferenceType.FORWARD, 1e-4);
/**
* The first order finite difference calculator.
*/
private final VectorFieldFirstOrderDifferentiator fd;
//-------------------------------------------------------------------------
/**
* Obtains an instance of the finite difference calculator using forward differencing.
*
* @param shift the shift to be applied to the curves
* @return the calculator
*/
public static CurveGammaCalculator ofForwardDifference(double shift) {
return new CurveGammaCalculator(FiniteDifferenceType.FORWARD, shift);
}
/**
* Obtains an instance of the finite difference calculator using central differencing.
*
* @param shift the shift to be applied to the curves
* @return the calculator
*/
public static CurveGammaCalculator ofCentralDifference(double shift) {
return new CurveGammaCalculator(FiniteDifferenceType.CENTRAL, shift);
}
/**
* Obtains an instance of the finite difference calculator using backward differencing.
*
* @param shift the shift to be applied to the curves
* @return the calculator
*/
public static CurveGammaCalculator ofBackwardDifference(double shift) {
return new CurveGammaCalculator(FiniteDifferenceType.BACKWARD, shift);
}
//-------------------------------------------------------------------------
/**
* Create an instance of the finite difference calculator.
*
* @param fdType the finite difference type
* @param shift the shift to be applied to the curves
*/
private CurveGammaCalculator(FiniteDifferenceType fdType, double shift) {
this.fd = new VectorFieldFirstOrderDifferentiator(fdType, shift);
}
//-------------------------------------------------------------------------
/**
* Computes intra-curve cross gamma by applying finite difference method to curve delta.
* <p>
* This computes the intra-curve cross gamma, i.e., the second order sensitivities to individual curves.
* Thus the sensitivity of a curve delta to another curve is not produced.
* <p>
* The sensitivities are computed for discount curves, and forward curves for {@code RateIndex} and {@code PriceIndex}.
* This implementation works only for single currency trades.
*
* @param ratesProvider the rates provider
* @param sensitivitiesFn the sensitivity function
* @return the cross gamma
*/
public CrossGammaParameterSensitivities calculateCrossGammaIntraCurve(
RatesProvider ratesProvider,
Function<ImmutableRatesProvider, CurrencyParameterSensitivities> sensitivitiesFn) {
ImmutableRatesProvider immProv = ratesProvider.toImmutableRatesProvider();
CurrencyParameterSensitivities baseDelta = sensitivitiesFn.apply(immProv); // used to check target sensitivity exits
CrossGammaParameterSensitivities result = CrossGammaParameterSensitivities.empty();
// discount curve
for (Entry<Currency, Curve> entry : immProv.getDiscountCurves().entrySet()) {
Currency currency = entry.getKey();
Curve curve = entry.getValue();
if (baseDelta.findSensitivity(curve.getName(), currency).isPresent()) {
NodalCurve nodalCurve = getNodalCurve(curve);
CrossGammaParameterSensitivity gammaSingle = computeGammaForCurve(
nodalCurve, currency, c -> immProv.toBuilder().discountCurve(currency, c).build(), sensitivitiesFn);
result = result.combinedWith(gammaSingle);
}
}
// forward curve
for (Entry<Index, Curve> entry : immProv.getIndexCurves().entrySet()) {
Index index = entry.getKey();
if (index instanceof RateIndex || index instanceof PriceIndex) {
Currency currency = getCurrency(index);
Curve curve = entry.getValue();
if (baseDelta.findSensitivity(curve.getName(), currency).isPresent()) {
NodalCurve nodalCurve = getNodalCurve(curve);
CrossGammaParameterSensitivity gammaSingle = computeGammaForCurve(
nodalCurve, currency, c -> immProv.toBuilder().indexCurve(index, c).build(), sensitivitiesFn);
result = result.combinedWith(gammaSingle);
}
}
}
return result;
}
//-------------------------------------------------------------------------
/**
* Computes cross-curve gamma by applying finite difference method to curve delta.
* <p>
* This computes the cross-curve gamma, i.e., the second order sensitivities to full curves.
* Thus the sensitivities of curve delta to other curves are produced.
* <p>
* The sensitivities are computed for discount curves, and forward curves for {@code RateIndex} and {@code PriceIndex}.
* This implementation works only for single currency trades.
*
* @param ratesProvider the rates provider
* @param sensitivitiesFn the sensitivity function
* @return the cross gamma
*/
public CrossGammaParameterSensitivities calculateCrossGammaCrossCurve(
RatesProvider ratesProvider,
Function<ImmutableRatesProvider, CurrencyParameterSensitivities> sensitivitiesFn) {
ImmutableRatesProvider immProv = ratesProvider.toImmutableRatesProvider();
CurrencyParameterSensitivities baseDelta = sensitivitiesFn.apply(immProv); // used to check target sensitivity exits.
CrossGammaParameterSensitivities result = CrossGammaParameterSensitivities.empty();
for (CurrencyParameterSensitivity baseDeltaSingle : baseDelta.getSensitivities()) {
CrossGammaParameterSensitivities resultInner = CrossGammaParameterSensitivities.empty();
// discount curve
for (Entry<Currency, Curve> entry : immProv.getDiscountCurves().entrySet()) {
Currency currency = entry.getKey();
Curve curve = entry.getValue();
if (baseDelta.findSensitivity(curve.getName(), currency).isPresent()) {
NodalCurve nodalCurve = getNodalCurve(curve);
CrossGammaParameterSensitivity gammaSingle = computeGammaForCurve(
baseDeltaSingle, nodalCurve, c -> immProv.toBuilder().discountCurve(currency, c).build(), sensitivitiesFn);
resultInner = resultInner.combinedWith(gammaSingle);
}
}
// forward curve
for (Entry<Index, Curve> entry : immProv.getIndexCurves().entrySet()) {
Index index = entry.getKey();
if (index instanceof RateIndex || index instanceof PriceIndex) {
Currency currency = getCurrency(index);
Curve curve = entry.getValue();
if (baseDelta.findSensitivity(curve.getName(), currency).isPresent()) {
NodalCurve nodalCurve = getNodalCurve(curve);
CrossGammaParameterSensitivity gammaSingle = computeGammaForCurve(
baseDeltaSingle, nodalCurve, c -> immProv.toBuilder().indexCurve(index, c).build(), sensitivitiesFn);
resultInner = resultInner.combinedWith(gammaSingle);
}
}
}
result = result.combinedWith(combineSensitivities(baseDeltaSingle, resultInner));
}
return result;
}
//-------------------------------------------------------------------------
private NodalCurve getNodalCurve(Curve curve) {
ArgChecker.isTrue(curve instanceof NodalCurve, "underlying curve must be NodalCurve");
return (NodalCurve) curve;
}
private Currency getCurrency(Index index) {
if (index instanceof RateIndex) {
return ((RateIndex) index).getCurrency();
} else if (index instanceof PriceIndex) {
return ((PriceIndex) index).getCurrency();
}
throw new IllegalArgumentException("unsupported index");
}
// compute the second order sensitivity to nodalCurve
CrossGammaParameterSensitivity computeGammaForCurve(
NodalCurve nodalCurve,
Currency sensitivityCurrency,
Function<Curve, ImmutableRatesProvider> ratesProviderFn,
Function<ImmutableRatesProvider, CurrencyParameterSensitivities> sensitivitiesFn) {
Function<DoubleArray, DoubleArray> function = new Function<DoubleArray, DoubleArray>() {
@Override
public DoubleArray apply(DoubleArray t) {
NodalCurve newCurve = nodalCurve.withYValues(t);
ImmutableRatesProvider newRates = ratesProviderFn.apply(newCurve);
CurrencyParameterSensitivities sensiMulti = sensitivitiesFn.apply(newRates);
return sensiMulti.getSensitivity(newCurve.getName(), sensitivityCurrency).getSensitivity();
}
};
DoubleMatrix sensi = fd.differentiate(function).apply(nodalCurve.getYValues());
List<ParameterMetadata> metadata = IntStream.range(0, nodalCurve.getParameterCount())
.mapToObj(i -> nodalCurve.getParameterMetadata(i))
.collect(toImmutableList());
return CrossGammaParameterSensitivity.of(nodalCurve.getName(), metadata, sensitivityCurrency, sensi);
}
// computes the sensitivity of baseDeltaSingle to nodalCurve
CrossGammaParameterSensitivity computeGammaForCurve(
CurrencyParameterSensitivity baseDeltaSingle,
NodalCurve nodalCurve,
Function<Curve, ImmutableRatesProvider> ratesProviderFn,
Function<ImmutableRatesProvider, CurrencyParameterSensitivities> sensitivitiesFn) {
Function<DoubleArray, DoubleArray> function = new Function<DoubleArray, DoubleArray>() {
@Override
public DoubleArray apply(DoubleArray t) {
NodalCurve newCurve = nodalCurve.withYValues(t);
ImmutableRatesProvider newRates = ratesProviderFn.apply(newCurve);
CurrencyParameterSensitivities sensiMulti = sensitivitiesFn.apply(newRates);
return sensiMulti.getSensitivity(baseDeltaSingle.getMarketDataName(), baseDeltaSingle.getCurrency()).getSensitivity();
}
};
DoubleMatrix sensi = fd.differentiate(function).apply(nodalCurve.getYValues());
List<ParameterMetadata> metadata = IntStream.range(0, nodalCurve.getParameterCount())
.mapToObj(i -> nodalCurve.getParameterMetadata(i))
.collect(toImmutableList());
return CrossGammaParameterSensitivity.of(
baseDeltaSingle.getMarketDataName(),
baseDeltaSingle.getParameterMetadata(),
nodalCurve.getName(),
metadata,
baseDeltaSingle.getCurrency(),
sensi);
}
private CrossGammaParameterSensitivity combineSensitivities(
CurrencyParameterSensitivity baseDeltaSingle,
CrossGammaParameterSensitivities blockCrossGamma) {
double[][] valuesTotal = new double[baseDeltaSingle.getParameterCount()][];
List<Pair<MarketDataName<?>, List<? extends ParameterMetadata>>> order = new ArrayList<>();
for (int i = 0; i < baseDeltaSingle.getParameterCount(); ++i) {
ArrayList<Double> innerList = new ArrayList<>();
for (CrossGammaParameterSensitivity gammaSingle : blockCrossGamma.getSensitivities()) {
innerList.addAll(gammaSingle.getSensitivity().row(i).toList());
if (i == 0) {
order.add(gammaSingle.getOrder().get(0));
}
}
valuesTotal[i] = Doubles.toArray(innerList);
}
return CrossGammaParameterSensitivity.of(
baseDeltaSingle.getMarketDataName(),
baseDeltaSingle.getParameterMetadata(),
order,
baseDeltaSingle.getCurrency(),
DoubleMatrix.ofUnsafe(valuesTotal));
}
//-------------------------------------------------------------------------
/**
* Computes the "sum-of-column gamma" or "semi-parallel gamma" for a sensitivity function.
* <p>
* This implementation supports a single {@link Curve} on the zero-coupon rates.
* By default the gamma is computed using a one basis-point shift and a forward finite difference.
* The results themselves are not scaled (they represent the second order derivative).
*
* @param curve the single curve to be bumped
* @param curveCurrency the currency of the curve and resulting sensitivity
* @param sensitivitiesFn the function to convert the bumped curve to parameter sensitivities
* @return the "sum-of-columns" or "semi-parallel" gamma vector
*/
public CurrencyParameterSensitivity calculateSemiParallelGamma(
Curve curve,
Currency curveCurrency,
Function<Curve, CurrencyParameterSensitivity> sensitivitiesFn) {
Delta deltaShift = new Delta(curve, sensitivitiesFn);
Function<DoubleArray, DoubleMatrix> gammaFn = fd.differentiate(deltaShift);
DoubleArray gamma = gammaFn.apply(DoubleArray.filled(1)).column(0);
return curve.createParameterSensitivity(curveCurrency, gamma);
}
//-------------------------------------------------------------------------
/**
* Inner class to compute the delta for a given parallel shift of the curve.
*/
static class Delta implements Function<DoubleArray, DoubleArray> {
private final Curve curve;
private final Function<Curve, CurrencyParameterSensitivity> sensitivitiesFn;
Delta(Curve curve, Function<Curve, CurrencyParameterSensitivity> sensitivitiesFn) {
this.curve = curve;
this.sensitivitiesFn = sensitivitiesFn;
}
@Override
public DoubleArray apply(DoubleArray s) {
double shift = s.get(0);
Curve curveBumped = ParallelShiftedCurve.absolute(curve, shift);
CurrencyParameterSensitivity pts = sensitivitiesFn.apply(curveBumped);
return pts.getSensitivity();
}
}
}