/**
* Copyright (C) 2016 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.impl.volatility.local;
import static com.opengamma.strata.market.curve.interpolator.CurveExtrapolators.INTERPOLATOR;
import static com.opengamma.strata.market.curve.interpolator.CurveInterpolators.NATURAL_SPLINE;
import static org.testng.Assert.assertEquals;
import java.util.function.Function;
import org.testng.annotations.Test;
import com.opengamma.strata.collect.array.DoubleArray;
import com.opengamma.strata.market.param.UnitParameterSensitivity;
import com.opengamma.strata.market.surface.ConstantSurface;
import com.opengamma.strata.market.surface.DefaultSurfaceMetadata;
import com.opengamma.strata.market.surface.DeformedSurface;
import com.opengamma.strata.market.surface.InterpolatedNodalSurface;
import com.opengamma.strata.market.surface.NodalSurface;
import com.opengamma.strata.market.surface.interpolator.GridSurfaceInterpolator;
import com.opengamma.strata.market.surface.interpolator.SurfaceInterpolator;
/**
* Test {@link DupireLocalVolatilityCalculator}.
*/
@Test
public class DupireLocalVolatilityCalculatorTest {
private static final SurfaceInterpolator INTERPOLATOR_2D = GridSurfaceInterpolator.of(
NATURAL_SPLINE, INTERPOLATOR, NATURAL_SPLINE, INTERPOLATOR);
private static final DoubleArray TIMES =
DoubleArray.of(0.25, 0.25, 0.25, 0.5, 0.5, 0.5, 0.75, 0.75, 0.75, 1, 1, 1);
private static final DoubleArray STRIKES =
DoubleArray.of(0.8, 1.4, 2, 0.8, 1.4, 2, 0.8, 1.4, 2, 0.8, 1.4, 2);
private static final DoubleArray VOLS =
DoubleArray.of(0.21, 0.17, 0.185, 0.17, 0.15, 0.16, 0.15, 0.14, 0.14, 0.14, 0.13, 0.13);
private static final InterpolatedNodalSurface VOL_SURFACE =
InterpolatedNodalSurface.of(DefaultSurfaceMetadata.of("Test"), TIMES, STRIKES, VOLS, INTERPOLATOR_2D);
private static final DoubleArray PRICES = DoubleArray.of(
0.59600, 0.04868, 2.3012E-6,
0.59201, 0.06138, 4.7919E-5,
0.58812, 0.07063, 1.1365E-4,
0.58413, 0.07626, 2.4524E-4);
private static final InterpolatedNodalSurface PRICE_SURFACE =
InterpolatedNodalSurface.of(DefaultSurfaceMetadata.of("Test"), TIMES, STRIKES, PRICES, INTERPOLATOR_2D);
private static final double SPOT = 1.40;
private static final double[] TEST_STRIKES = new double[] {1.1, 1.4, 2.2 };
private static final double[] TEST_TIMES = new double[] {0.1, 0.6, 1.1 };
private static final double FD_EPS = 1.0e-5;
private static final DupireLocalVolatilityCalculator CALC = new DupireLocalVolatilityCalculator();
public void flatVolTest() {
double constantVol = 0.15;
ConstantSurface impliedVolSurface = ConstantSurface.of("impliedVol", constantVol);
Function<Double, Double> zeroRate = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return 0.05d;
}
};
Function<Double, Double> zeroRate1 = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return 0.02d;
}
};
double[] strikes = new double[] {90d, 100d, 115d };
for (double strike : strikes) {
for (double time : TEST_TIMES) {
DeformedSurface localVolSurface =
CALC.localVolatilityFromImpliedVolatility(impliedVolSurface, 100d, zeroRate, zeroRate1);
assertEquals(localVolSurface.zValue(time, strike), constantVol);
}
}
}
public void test_localVolatilityFromImpliedVolatility() {
double r = 0.05;
double q = 0.01;
Function<Double, Double> interestRate = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return r;
}
};
Function<Double, Double> dividendRate = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return q;
}
};
for (double strike : TEST_STRIKES) {
for (double time : TEST_TIMES) {
double computedVol = CALC
.localVolatilityFromImpliedVolatility(VOL_SURFACE, SPOT, interestRate, dividendRate)
.zValue(time, strike);
double expectedVol = volFromFormula(r, q, time, strike, VOL_SURFACE);
assertEquals(computedVol, expectedVol, FD_EPS);
UnitParameterSensitivity computedSensi =
CALC.localVolatilityFromImpliedVolatility(VOL_SURFACE, SPOT, interestRate, dividendRate)
.zValueParameterSensitivity(time, strike);
for (int i = 0; i < VOLS.size(); ++i) {
InterpolatedNodalSurface surfaceUp = VOL_SURFACE.withZValues(VOLS.with(i, VOLS.get(i) + FD_EPS));
InterpolatedNodalSurface surfaceDw = VOL_SURFACE.withZValues(VOLS.with(i, VOLS.get(i) - FD_EPS));
double volUp = CALC.localVolatilityFromImpliedVolatility(
surfaceUp, SPOT, interestRate, dividendRate).zValue(time, strike);
double volDw = CALC.localVolatilityFromImpliedVolatility(
surfaceDw, SPOT, interestRate, dividendRate).zValue(time, strike);
double expectedSensi = 0.5 * (volUp - volDw) / FD_EPS;
assertEquals(computedSensi.getSensitivity().get(i), expectedSensi, FD_EPS * 10d);
}
}
}
}
public void test_localVolatilityFromImpliedVolatility_smallStrike() {
double r = 0.05;
double q = 0.01;
Function<Double, Double> interestRate = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return r;
}
};
Function<Double, Double> dividendRate = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return q;
}
};
double strike = 1.0e-11;
for (double time : TEST_TIMES) {
double computedVol = CALC
.localVolatilityFromImpliedVolatility(VOL_SURFACE, SPOT, interestRate, dividendRate)
.zValue(time, strike);
double expectedVol = volFromFormula(r, q, time, strike, VOL_SURFACE);
assertEquals(computedVol, expectedVol, FD_EPS);
UnitParameterSensitivity computedSensi =
CALC.localVolatilityFromImpliedVolatility(VOL_SURFACE, SPOT, interestRate, dividendRate)
.zValueParameterSensitivity(time, strike);
for (int i = 0; i < VOLS.size(); ++i) {
InterpolatedNodalSurface surfaceUp = VOL_SURFACE.withZValues(VOLS.with(i, VOLS.get(i) + FD_EPS));
InterpolatedNodalSurface surfaceDw = VOL_SURFACE.withZValues(VOLS.with(i, VOLS.get(i) - FD_EPS));
double volUp = CALC.localVolatilityFromImpliedVolatility(
surfaceUp, SPOT, interestRate, dividendRate).zValue(time, strike);
double volDw = CALC.localVolatilityFromImpliedVolatility(
surfaceDw, SPOT, interestRate, dividendRate).zValue(time, strike);
double expectedSensi = 0.5 * (volUp - volDw) / FD_EPS;
assertEquals(computedSensi.getSensitivity().get(i), expectedSensi, FD_EPS * 10d);
}
}
}
public void test_localVolatilityFromPrice() {
double r = 0.03;
double q = 0.02;
Function<Double, Double> interestRate = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return r;
}
};
Function<Double, Double> dividendRate = new Function<Double, Double>() {
@Override
public Double apply(Double x) {
return q;
}
};
for (double strike : TEST_STRIKES) {
for (double time : TEST_TIMES) {
double computedVol = CALC
.localVolatilityFromPrice(PRICE_SURFACE, SPOT, interestRate, dividendRate)
.zValue(time, strike);
double expectedVol = volFromFormulaPrice(r, q, time, strike, PRICE_SURFACE);
assertEquals(computedVol, expectedVol, FD_EPS);
UnitParameterSensitivity computedSensi =
CALC.localVolatilityFromPrice(PRICE_SURFACE, SPOT, interestRate, dividendRate)
.zValueParameterSensitivity(time, strike);
for (int i = 0; i < PRICES.size(); ++i) {
InterpolatedNodalSurface surfaceUp = PRICE_SURFACE.withZValues(PRICES.with(i, PRICES.get(i) + FD_EPS));
InterpolatedNodalSurface surfaceDw = PRICE_SURFACE.withZValues(PRICES.with(i, PRICES.get(i) - FD_EPS));
double priceUp = CALC.localVolatilityFromPrice(
surfaceUp, SPOT, interestRate, dividendRate).zValue(time, strike);
double priceDw = CALC.localVolatilityFromPrice(
surfaceDw, SPOT, interestRate, dividendRate).zValue(time, strike);
double expectedSensi = 0.5 * (priceUp - priceDw) / FD_EPS;
assertEquals(computedSensi.getSensitivity().get(i), expectedSensi, FD_EPS * 100d); // tiny call price
}
}
}
}
private double volFromFormula(double r, double q, double time, double strike, NodalSurface surface) {
double vol = surface.zValue(time, strike);
double volT = 0.5 / FD_EPS * (surface.zValue(time + FD_EPS, strike) - surface.zValue(time - FD_EPS, strike));
double volK = 0.5 / FD_EPS * (surface.zValue(time, strike + FD_EPS) - surface.zValue(time, strike - FD_EPS));
double volKK =
(surface.zValue(time, strike + FD_EPS) + surface.zValue(time, strike - FD_EPS) - 2d * vol) / FD_EPS / FD_EPS;
double rootT = Math.sqrt(time);
double d1 = (Math.log(SPOT / strike) + (r - q + 0.5 * vol * vol) * time) / vol / rootT;
double d2 = (Math.log(SPOT / strike) + (r - q - 0.5 * vol * vol) * time) / vol / rootT;
double den = 1d + 2d * d1 * strike * rootT * volK + strike * strike * time * (d1 * d2 * volK * volK + vol * volKK);
double var = (vol * vol + 2d * vol * time * (volT + (r - q) * strike * volK)) / den;
return Math.sqrt(var);
}
private double volFromFormulaPrice(double r, double q, double time, double strike, NodalSurface surface) {
double p = surface.zValue(time, strike);
double pT = 0.5 / FD_EPS * (surface.zValue(time + FD_EPS, strike) - surface.zValue(time - FD_EPS, strike));
double pK = 0.5 / FD_EPS * (surface.zValue(time, strike + FD_EPS) - surface.zValue(time, strike - FD_EPS));
double pKK =
(surface.zValue(time, strike + FD_EPS) + surface.zValue(time, strike - FD_EPS) - 2d * p) / FD_EPS / FD_EPS;
double var = 2d * (pT + (r - q) * strike * pK + q * p) / (strike * strike * pKK);
return Math.sqrt(var);
}
}