/**
* Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.analytics.financial.equity;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.analytics.financial.equity.option.EquityIndexOption;
import com.opengamma.analytics.financial.interestrate.InstrumentDerivative;
import com.opengamma.analytics.financial.interestrate.InstrumentDerivativeVisitor;
import com.opengamma.analytics.financial.interestrate.NodeYieldSensitivityCalculator;
import com.opengamma.analytics.financial.interestrate.PresentValueNodeSensitivityCalculator;
import com.opengamma.analytics.financial.model.interestrate.curve.YieldCurve;
import com.opengamma.analytics.financial.model.volatility.smile.fitting.interpolation.GeneralSmileInterpolator;
import com.opengamma.analytics.financial.model.volatility.smile.fitting.interpolation.SurfaceArrayUtils;
import com.opengamma.analytics.financial.model.volatility.smile.fitting.sabr.SmileSurfaceDataBundle;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurface;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceMoneynessFcnBackedByGrid;
import com.opengamma.analytics.financial.model.volatility.surface.VolatilitySurfaceInterpolator;
import com.opengamma.analytics.math.function.Function1D;
import com.opengamma.analytics.math.matrix.DoubleMatrix1D;
import com.opengamma.analytics.math.surface.InterpolatedDoublesSurface;
import com.opengamma.analytics.math.surface.InterpolatedSurfaceAdditiveShiftFunction;
import com.opengamma.analytics.math.surface.NodalDoublesSurface;
import com.opengamma.analytics.math.surface.Surface;
import com.opengamma.util.ArgumentChecker;
import com.opengamma.util.tuple.DoublesPair;
import com.opengamma.util.tuple.Triple;
/**
* This Calculator provides simple bump and reprice sensitivities for Derivatives
*/
@SuppressWarnings("deprecation")
public class EquityDerivativeSensitivityCalculator {
/** The default value of the absolute shift */
private static final double DEFAULT_ABS_SHIFT = 0.0001; // Shift used for vol, +/- 1bp == 0.01%
/** The default value of the relative shift */
private static final double DEFAULT_REL_SHIFT = 0.01; // Shift used for rates +/- 1% * Rate
/** Gets the settlement time for the instrument */
private static final SettlementTimeCalculator SETTLEMENT_CALCULATOR = SettlementTimeCalculator.getInstance();
/** The pricer */
private final InstrumentDerivativeVisitor<StaticReplicationDataBundle, Double> _pricer;
/**
* @param pricer The pricer, not null
*/
public EquityDerivativeSensitivityCalculator(final InstrumentDerivativeVisitor<StaticReplicationDataBundle, Double> pricer) {
ArgumentChecker.notNull(pricer, "pricer");
_pricer = pricer;
}
/**
* This calculates the sensitivity of the present value (PV) to a unit move in the forward.
* The volatility surface remains unchanged.
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @param relShift Relative size of shift made in centered-finite difference approximation.
* @return A Double. Currency amount per unit amount change in the black volatility
*/
public Double calcForwardSensitivity(final InstrumentDerivative derivative, final StaticReplicationDataBundle market, final double relShift) {
ArgumentChecker.notNull(derivative, "null EquityDerivative");
ArgumentChecker.notNull(market, "null EquityOptionDataBundle");
// Shift UP
StaticReplicationDataBundle bumpedMarket = new StaticReplicationDataBundle(market.getVolatilitySurface(), market.getDiscountCurve(), market.getForwardCurve()
.withFractionalShift(relShift));
final double pvUp = derivative.accept(_pricer, bumpedMarket);
// Shift Down
bumpedMarket = new StaticReplicationDataBundle(market.getVolatilitySurface(), market.getDiscountCurve(), market.getForwardCurve().withFractionalShift(-relShift));
final double pvDown = derivative.accept(_pricer, bumpedMarket);
final double t = derivative.accept(SETTLEMENT_CALCULATOR);
final double fwd = market.getForwardCurve().getForward(t);
// Centered-difference result
return (pvUp - pvDown) / 2.0 / relShift / fwd;
}
/**
* This calculates the sensitivity of the present value (PV) to a unit move in the forward,
* under a default shift of 1% of the forward <p>
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @return A Double. Currency amount per unit amount change in the black volatility
*/
public Double calcForwardSensitivity(final InstrumentDerivative derivative, final StaticReplicationDataBundle market) {
return calcForwardSensitivity(derivative, market, DEFAULT_REL_SHIFT);
}
/**
* Calculates the sensitivity of the present value (PV) to a change in the funding rate from valuation to settlement.
* Also know as PVBP and DV01, though note this return per UNIT change in rate. calcPV01 returns per basis point change in rates. <p>
* <p>
* Rates enter the pricing of a EquityDerivative in two places: in the discounting and forward projection.<p>
* The presentValue has been structured such that the form of the PV = Z(t,T) * FwdPrice(t,T) with Z a zero coupon bond, and t and T the valuation and settlement times respectively.
* The form of our discounting rates is such that Z(t,T) = exp[- R(t,T) * (T-t)], hence dZ/dR = -(T-t)*Z(t,T) and d(PV)/dR = PV * dZ/dR
* The forward's dependence on the discounting rate is similar to the zero coupon bonds, but of opposite sign, dF/dR = (T-t)*F(t,T)
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @param shift Relative size of shift made in centered-finite difference approximation.
* @return A Double in the currency, deriv.getCurrency(). Currency amount per unit amount change in discount rate
*/
public Double calcDiscountRateSensitivity(final InstrumentDerivative derivative, final StaticReplicationDataBundle market, final double shift) {
ArgumentChecker.notNull(market, "market");
ArgumentChecker.notNull(derivative, "derivative");
// Sensitivity from the discounting
final double pv = derivative.accept(_pricer, market);
final double timeToSettlement = derivative.accept(SETTLEMENT_CALCULATOR);
// Sensitivity from forward projection
final double fwdSens = calcForwardSensitivity(derivative, market, shift);
final double fwd = market.getForwardCurve().getForward(timeToSettlement);
return timeToSettlement * (fwd * fwdSens - pv);
}
/**
* Calculates the sensitivity of the present value (PV) to a change in the funding rate from valuation to settlement.
* Also know as PVBP and DV01, though note this return per UNIT change in rate. calcPV01 returns per basis point change in rates. <p>
* <p>
* Rates enter the pricing of a EquityDerivative in two places: in the discounting and forward projection.<p>
* The presentValue has been structured such that the form of the PV = Z(t,T) * FwdPrice(t,T) with Z a zero coupon bond, and t and T the valuation and settlement times respectively.
* The form of our discounting rates is such that Z(t,T) = exp[- R(t,T) * (T-t)], hence dZ/dR = (t-T)*Z(t,T) and d(PV)/dR = PV * dZ/dR
* The forward's dependence on the discounting rate is similar to the zero coupon bonds, but of opposite sign, dF/dR = (T-t)*F(t,T)
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @return A Double in the currency, deriv.getCurrency(). Currency amount per unit amount change in discount rate
*/
public Double calcDiscountRateSensitivity(final InstrumentDerivative derivative, final StaticReplicationDataBundle market) {
return calcDiscountRateSensitivity(derivative, market, DEFAULT_REL_SHIFT);
}
/**
* Calculates the sensitivity of the present value (PV) to a basis point (bp) move in the funding rates across all maturities. <p>
* Also know as PVBP and DV01.
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @return A Double in the currency, derivative.getCurrency()
*/
public Double calcPV01(final InstrumentDerivative derivative, final StaticReplicationDataBundle market) {
return calcDiscountRateSensitivity(derivative, market) / 10000;
}
/**
* This calculates the sensitivity of the present value (PV) to the continuously-compounded discount rates at the knot points of the funding curve. <p>
* The return format is a DoubleMatrix1D (i.e. a vector) with length equal to the total number of knots in the curve <p>
* The change of a curve due to the movement of a single knot is interpolator-dependent, so an instrument can have sensitivity to knots at times beyond its maturity
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @return A DoubleMatrix1D containing bucketed delta in order and length of market.getDiscountCurve(). Currency amount per unit amount change in discount rate
*/
public DoubleMatrix1D calcDeltaBucketed(final InstrumentDerivative derivative, final StaticReplicationDataBundle market) {
ArgumentChecker.notNull(derivative, "null EquityDerivative");
ArgumentChecker.notNull(market, "null EquityOptionDataBundle");
// We know that the EquityDerivative only has true sensitivity to one maturity on one curve.
// A function written for interestRate sensitivities spreads this sensitivity across yield nodes
// NodeSensitivityCalculator.curveToNodeSensitivities(curveSensitivities, interpolatedCurves)
if (!(market.getDiscountCurve() instanceof YieldCurve)) {
throw new IllegalArgumentException("Can only handle YieldCurve");
}
final YieldCurve discCrv = (YieldCurve) market.getDiscountCurve();
final double settlement = derivative.accept(SETTLEMENT_CALCULATOR);
final double sens = calcDiscountRateSensitivity(derivative, market);
final NodeYieldSensitivityCalculator distributor = PresentValueNodeSensitivityCalculator.getDefaultInstance();
final List<Double> result = distributor.curveToNodeSensitivity(Arrays.asList(DoublesPair.of(settlement, sens)), discCrv);
return new DoubleMatrix1D(result.toArray(new Double[result.size()]));
}
/**
* This calculates the sensitivity of the present value (PV) to the lognormal Black implied volatilities at the knot points of the surface. <p>
* Note - the change of the surface due to the movement of a single node is interpolator-dependent, so an instrument may have non-local sensitivity
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @return A Double. Currency amount per unit amount change in the black volatility
*/
public Double calcBlackVegaParallel(final InstrumentDerivative derivative, final StaticReplicationDataBundle market) {
return calcBlackVegaParallel(derivative, market, DEFAULT_ABS_SHIFT);
}
/**
* This calculates the sensitivity of the present value (PV) to the lognormal Black implied volatilities at the knot points of the surface. <p>
* Note - the change of the surface due to the movement of a single node is interpolator-dependent, so an instrument may have non-local sensitivity
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @param shift Size of shift made in centered-finite difference approximation. e.g. 1% would be 0.01, and 1bp 0.0001
* @return A Double. Currency amount per unit amount change in the black volatility
*/
public Double calcBlackVegaParallel(final InstrumentDerivative derivative, final StaticReplicationDataBundle market, final double shift) {
ArgumentChecker.notNull(derivative, "null EquityDerivative");
ArgumentChecker.notNull(market, "null EquityOptionDataBundle");
// Parallel shift UP
final BlackVolatilitySurface<?> upSurface = market.getVolatilitySurface().withShift(shift, true);
final double pvUp = derivative.accept(_pricer, new StaticReplicationDataBundle(upSurface, market.getDiscountCurve(), market.getForwardCurve()));
// Parallel shift DOWN
final BlackVolatilitySurface<?> downSurface = market.getVolatilitySurface().withShift(-shift, true);
final double pvDown = derivative.accept(_pricer, new StaticReplicationDataBundle(downSurface, market.getDiscountCurve(), market.getForwardCurve()));
// Centered-difference result
return (pvUp - pvDown) / (2.0 * shift);
}
/**
* This calculates the sensitivity of the present value (PV) to the lognormal Black implied volatilities at the knot points of the surface. <p>
* The return format is a DoubleMatrix2D with rows equal to the total number of maturities and columns equal to the number of strikes. <p>
* Note - the change of the surface due to the movement of a single node is interpolator-dependent, so an instrument may have non-local sensitivity
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @return A NodalDoublesSurface with same axes as market.getVolatilitySurface(). Contains currency amount per unit amount change in the black volatility of each node
*/
public NodalDoublesSurface calcBlackVegaForEntireSurface(final InstrumentDerivative derivative, final StaticReplicationDataBundle market) {
return calcBlackVegaForEntireSurface(derivative, market, DEFAULT_ABS_SHIFT);
}
/**
* This calculates the sensitivity of the present value (PV) to the lognormal Black implied volatilities at the knot points of the surface. <p>
* The return format is a DoubleMatrix2D with rows equal to the total number of maturities and columns equal to the number of strikes. <p>
* Note - the change of the surface due to the movement of a single node is interpolator-dependent, so an instrument may have non-local sensitivity
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @param shift Size of shift made in centred-finite difference approximation. e.g. 1% would be 0.01, and 1bp 0.0001
* @return A NodalDoublesSurface with same axes as market.getVolatilitySurface(). Contains currency amount per unit amount change in the black volatility of each node
*/
public NodalDoublesSurface calcBlackVegaForEntireSurface(final InstrumentDerivative derivative, final StaticReplicationDataBundle market, final double shift) {
ArgumentChecker.notNull(derivative, "null EquityDerivative");
ArgumentChecker.notNull(market, "null EquityOptionDataBundle");
if (market.getVolatilitySurface().getSurface() instanceof InterpolatedDoublesSurface) {
final InterpolatedDoublesSurface blackSurf = (InterpolatedDoublesSurface) market.getVolatilitySurface().getSurface();
final Double[] maturities = blackSurf.getXData();
final Double[] strikes = blackSurf.getYData();
final int nNodes = maturities.length;
ArgumentChecker.isTrue(nNodes == strikes.length, "Number of strikes must match number of nodes");
// Bump and reprice
final Double[] vegas = new Double[nNodes];
for (int j = 0; j < nNodes; j++) {
vegas[j] = calcBlackVegaForSinglePoint(derivative, market, maturities[j], strikes[j], shift);
}
return NodalDoublesSurface.from(maturities, strikes, vegas);
// Special case for EquityIndexOptions
} else if (market.getVolatilitySurface() instanceof BlackVolatilitySurfaceMoneynessFcnBackedByGrid) {
final BlackVolatilitySurfaceMoneynessFcnBackedByGrid surfaceBundle = (BlackVolatilitySurfaceMoneynessFcnBackedByGrid) market.getVolatilitySurface();
final EquityIndexOption option;
if (derivative instanceof EquityIndexOption) {
option = (EquityIndexOption) derivative;
} else {
throw new OpenGammaRuntimeException("Calculator with BlackVolatilitySurfaceMoneynessFcnBackedByGrid was expecting an EquityIndexOption.");
}
// Unpack
final SmileSurfaceDataBundle volGrid = surfaceBundle.getGridData();
final double[] forwards = volGrid.getForwards();
final double[] volExpiries = volGrid.getExpiries();
final int nExpiries = volGrid.getNumExpiries();
final double[][] strikes = volGrid.getStrikes();
final double[][] vols = volGrid.getVolatilities();
final VolatilitySurfaceInterpolator surfaceInterpolator = surfaceBundle.getInterpolator();
final GeneralSmileInterpolator strikeInterpolator = surfaceInterpolator.getSmileInterpolator();
// Base price and set of independent smile fits (one function vol(k) for each expiry)
final Double pvBase = option.accept(_pricer, market);
final Function1D<Double, Double>[] smileFitsBase = surfaceInterpolator.getIndependentSmileFits(volGrid);
// Bump and reprice - loop over expiry and strike
final List<Triple<Double, Double, Double>> triplesExpiryStrikeVega = new ArrayList<>();
// TODO: REVIEW: We can drastically reduce the time it takes to compute this if we are sensible about avoiding points which almost certainly won't have any sensitivity
// Of course, this is all based upon the interpolor's scheme...
final int expiryIndex = SurfaceArrayUtils.getLowerBoundIndex(volExpiries, option.getTimeToExpiry());
for (int t = Math.max(0, expiryIndex - 3); t < Math.min(nExpiries, expiryIndex + 4); t++) {
final int nStrikes = strikes[t].length;
final int strikeIndex = SurfaceArrayUtils.getLowerBoundIndex(strikes[t], option.getStrike());
for (int k = Math.max(0, strikeIndex - 6); k < Math.min(nStrikes, strikeIndex + 7); k++) {
// TODO: REVIEW We only recompute the smile function for the specific expiry we are bumping..
final double[] bumpedVols = Arrays.copyOf(vols[t], nStrikes);
bumpedVols[k] = vols[t][k] - shift;
final Function1D<Double, Double> thisExpirysSmile = strikeInterpolator.getVolatilityFunction(forwards[t], strikes[t], volExpiries[t], bumpedVols);
final Function1D<Double, Double>[] scenarioSmileFits = Arrays.copyOf(smileFitsBase, smileFitsBase.length);
scenarioSmileFits[t] = thisExpirysSmile;
final BlackVolatilitySurfaceMoneynessFcnBackedByGrid shiftedSurface = surfaceInterpolator.combineIndependentSmileFits(scenarioSmileFits, volGrid);
//BlackVolatilitySurfaceMoneynessFcnBackedByGrid shiftedSurface = surfaceInterpolator.getBumpedVolatilitySurface(volGrid, t, k, -shift);
final StaticReplicationDataBundle shiftedMarket = market.withShiftedSurface(shiftedSurface);
final Double pvScenario = option.accept(_pricer, shiftedMarket);
ArgumentChecker.notNull(pvScenario, "Null PV in shifted scenario, T = " + volExpiries[t] + ", k = " + strikes[t][k]);
final Double vega = (pvScenario - pvBase) / -shift;
final Triple<Double, Double, Double> xyz = new Triple<>(volExpiries[t], strikes[t][k], vega);
triplesExpiryStrikeVega.add(xyz);
}
}
return NodalDoublesSurface.from(triplesExpiryStrikeVega);
} else {
throw new OpenGammaRuntimeException("Currently will only accept an Equity Volatility Surface based on an InterpolatedDoublesSurface, "
+ "or BlackVolatilitySurfaceMoneynessFcnBackedByGrid");
}
}
/**
* Compute the price sensitivity to a shift of the Black volatility at a given maturity and strike. <p>
* Note - the change of the surface due to the movement of a single node is interpolator-dependent, so an instrument may have non-local sensitivity.<p>
* Important!!! If the <i>(x, y)</i> value(s) of the shift(s) are not in the nodal points of the original surface, they are added (with shift) to the nodal points of the new surface.
* @param derivative the EquityDerivative
* @param market the EquityOptionDataBundle
* @param maturity a double in same unit as VolatilitySurface
* @param strike a double in same unit as VolatilitySurface
* @param shift Size of shift made in centered-finite difference approximation. e.g. 1% would be 0.01, and 1bp 0.0001
* @return Currency amount per unit amount change in the black volatility at the point provided
*/
public double calcBlackVegaForSinglePoint(final InstrumentDerivative derivative, final StaticReplicationDataBundle market, final double maturity, final double strike,
final double shift) {
final Surface<Double, Double, Double> surface = market.getVolatilitySurface().getSurface();
ArgumentChecker.isTrue(surface instanceof InterpolatedDoublesSurface, "Currently will only accept a Equity VolatilitySurfaces based on an InterpolatedDoublesSurface");
final InterpolatedDoublesSurface blackSurf = (InterpolatedDoublesSurface) surface;
final InterpolatedSurfaceAdditiveShiftFunction volShifter = new InterpolatedSurfaceAdditiveShiftFunction();
// shift UP
final InterpolatedDoublesSurface bumpedVolUp = volShifter.evaluate(blackSurf, maturity, strike, shift);
StaticReplicationDataBundle bumpedMarket = new StaticReplicationDataBundle(market.getVolatilitySurface().withSurface(bumpedVolUp), market.getDiscountCurve(),
market.getForwardCurve());
final double pvUp = derivative.accept(_pricer, bumpedMarket);
// shift DOWN
final InterpolatedDoublesSurface bumpedVolDown = volShifter.evaluate(blackSurf, maturity, strike, -shift);
bumpedMarket = new StaticReplicationDataBundle(market.getVolatilitySurface().withSurface(bumpedVolDown), market.getDiscountCurve(), market.getForwardCurve());
final double pvDown = derivative.accept(_pricer, bumpedMarket);
// Centered-difference result
return (pvUp - pvDown) / (2.0 * shift);
}
}