/**
* Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.analytics.financial.equity.variance;
import static com.opengamma.analytics.math.interpolation.CombinedInterpolatorExtrapolatorFactory.getInterpolator;
import static org.testng.AssertJUnit.assertEquals;
import org.apache.commons.lang.Validate;
import org.testng.annotations.Test;
import com.opengamma.analytics.financial.equity.StaticReplicationDataBundle;
import com.opengamma.analytics.financial.equity.variance.pricing.VarianceSwapStaticReplication;
import com.opengamma.analytics.financial.model.interestrate.curve.ForwardCurve;
import com.opengamma.analytics.financial.model.interestrate.curve.YieldAndDiscountCurve;
import com.opengamma.analytics.financial.model.interestrate.curve.YieldCurve;
import com.opengamma.analytics.financial.model.volatility.BlackFormulaRepository;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurface;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceConverter;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceDelta;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceLogMoneyness;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceMoneyness;
import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceStrike;
import com.opengamma.analytics.financial.model.volatility.surface.Strike;
import com.opengamma.analytics.financial.varianceswap.VarianceSwap;
import com.opengamma.analytics.math.curve.ConstantDoublesCurve;
import com.opengamma.analytics.math.function.Function;
import com.opengamma.analytics.math.interpolation.CombinedInterpolatorExtrapolator;
import com.opengamma.analytics.math.interpolation.GridInterpolator2D;
import com.opengamma.analytics.math.interpolation.Interpolator1DFactory;
import com.opengamma.analytics.math.interpolation.Interpolator2D;
import com.opengamma.analytics.math.surface.ConstantDoublesSurface;
import com.opengamma.analytics.math.surface.FunctionalDoublesSurface;
import com.opengamma.analytics.math.surface.InterpolatedDoublesSurface;
import com.opengamma.util.money.Currency;
import com.opengamma.util.test.TestGroup;
/**
* Test.
*/
@Test(groups = TestGroup.UNIT)
public class VarianceSwapStaticReplicationTest {
// Setup ------------------------------------------
// The derivative
private static final double varStrike = 0.05;
private static final double varNotional = 3150;
private static final double now = 0;
private static final double aYearAgo = -1;
private static final double expiry6M = 0.5;
private static final double expiry1 = 1;
private static final double expiry2 = 2;
private static final double expiry5 = 5;
private static final double expiry10 = 10;
private static final int nObsExpected = 750;
private static final int nObsDisrupted = 0;
private static final double annualization = 252;
private static final double[] observations = {};
private static final double[] obsWeights = {};
private static final VarianceSwap swap0 = new VarianceSwap(aYearAgo, now, now, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, nObsDisrupted, observations, obsWeights);
private static final VarianceSwap swap6M = new VarianceSwap(now, expiry6M, expiry6M, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, nObsDisrupted, observations, obsWeights);
private static final VarianceSwap swap1 = new VarianceSwap(now, expiry1, expiry1, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, nObsDisrupted, observations, obsWeights);
private static final VarianceSwap swap2 = new VarianceSwap(now, expiry2, expiry2, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, nObsDisrupted, observations, obsWeights);
private static final VarianceSwap swap5 = new VarianceSwap(now, expiry5, expiry5, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, nObsDisrupted, observations, obsWeights);
private static final VarianceSwap swap10 = new VarianceSwap(now, expiry10, expiry10, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, nObsDisrupted, observations, obsWeights);
private static final VarianceSwap swap5x10 = new VarianceSwap(expiry5, expiry10, expiry10, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, nObsDisrupted, observations, obsWeights);
// Market data
private static final double SPOT = 80;
private static final double DRIFT = 0.05;
private static final ForwardCurve FORWARD_CURVE = new ForwardCurve(SPOT, DRIFT);
private static final double TEST_VOL = 0.25;
private static final YieldAndDiscountCurve DISCOUNT = new YieldCurve("Discount", ConstantDoublesCurve.from(0.05));
private static final double[] EXPIRIES = new double[] {0.5, 0.5, 0.5, 0.5, 0.5, 1.0, 1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0, 5.0, 10.0, 10.0, 10.0, 10.0, 10.0 };
private static final double[] CALLDELTAS = new double[] {0.9, 0.75, 0.5, 0.25, 0.1, 0.9, 0.75, 0.5, 0.25, 0.1, 0.9, 0.75, 0.5, 0.25, 0.1, 0.9, 0.75, 0.5, 0.25, 0.1 };
private static final double[] STRIKES = new double[] {20, 40, 80, 100, 120, 20, 40, 80, 100, 120, 20, 40, 80, 100, 120, 20, 40, 80, 100, 120 };
private static final double[] VOLS = new double[] {0.28, 0.28, 0.28, 0.28, 0.28, 0.25, 0.25, 0.25, 0.25, 0.25, 0.27, 0.26, 0.24, 0.23, 0.25, 0.27, 0.26, 0.25, 0.26, 0.27 };
private static final CombinedInterpolatorExtrapolator INTERPOLATOR_1D_STRIKE = getInterpolator(Interpolator1DFactory.DOUBLE_QUADRATIC,
Interpolator1DFactory.LINEAR_EXTRAPOLATOR, Interpolator1DFactory.LINEAR_EXTRAPOLATOR);
private static final CombinedInterpolatorExtrapolator INTERPOLATOR_1D_EXPIRY = getInterpolator(Interpolator1DFactory.LINEAR, Interpolator1DFactory.LINEAR_EXTRAPOLATOR,
Interpolator1DFactory.FLAT_EXTRAPOLATOR);
private static final Interpolator2D INTERPOLATOR_2D = new GridInterpolator2D(INTERPOLATOR_1D_EXPIRY, INTERPOLATOR_1D_STRIKE);
private static final BlackVolatilitySurfaceStrike VOL_STRIKE_SURFACE = new BlackVolatilitySurfaceStrike(new InterpolatedDoublesSurface(EXPIRIES, STRIKES, VOLS, INTERPOLATOR_2D));
private static final BlackVolatilitySurfaceDelta VOL_CALLDELTA_SURFACE = new BlackVolatilitySurfaceDelta(new InterpolatedDoublesSurface(EXPIRIES, CALLDELTAS, VOLS, INTERPOLATOR_2D), FORWARD_CURVE);
private static final StaticReplicationDataBundle MARKET_W_STRIKESURF = new StaticReplicationDataBundle(VOL_STRIKE_SURFACE, DISCOUNT, FORWARD_CURVE);
private static final StaticReplicationDataBundle MARKET_W_CALLDELTASURF = new StaticReplicationDataBundle(VOL_CALLDELTA_SURFACE, DISCOUNT, FORWARD_CURVE);
//Since we use very conservative estimates of the tolerance, the actual error is 100x less than the tolerance set. In really, you'll never need a 1 part in 1,000,000,000
//accuracy that we test for here.
private static final double INTEGRAL_TOL = 1e-9;
private static final double TEST_TOL = 1e-9;
private static final VarianceSwapStaticReplication PRICER = new VarianceSwapStaticReplication(INTEGRAL_TOL);
// impliedVariance Tests ------------------------------------------
/**
* Test ConstantDoublesSurface delta surface at 1 and 10 years
*/
@Test
public void testConstantDoublesDeltaSurface() {
final BlackVolatilitySurfaceDelta constVolSurf = new BlackVolatilitySurfaceDelta(ConstantDoublesSurface.from(TEST_VOL), FORWARD_CURVE);
final double testVar = PRICER.expectedVariance(swap1, new StaticReplicationDataBundle(constVolSurf, DISCOUNT, FORWARD_CURVE));
final double testVar2 = PRICER.expectedVariance(swap10, new StaticReplicationDataBundle(constVolSurf, DISCOUNT, FORWARD_CURVE));
final double targetVar = TEST_VOL * TEST_VOL;
assertEquals(targetVar, testVar, TEST_TOL);
assertEquals(targetVar, testVar2, TEST_TOL);
}
/**
* Test ConstantDoublesSurface strike surface at 1 and 10 years
*/
@Test
public void testConstantDoublesStrikeSurface() {
final BlackVolatilitySurfaceStrike constVolSurf = new BlackVolatilitySurfaceStrike(ConstantDoublesSurface.from(TEST_VOL));
final double testVar = PRICER.expectedVariance(swap1, new StaticReplicationDataBundle(constVolSurf, DISCOUNT, FORWARD_CURVE));
final double testVar2 = PRICER.expectedVariance(swap10, new StaticReplicationDataBundle(constVolSurf, DISCOUNT, FORWARD_CURVE));
final double targetVar = TEST_VOL * TEST_VOL;
assertEquals(targetVar, testVar, TEST_TOL);
assertEquals(targetVar, testVar2, TEST_TOL);
}
/**
* Test of VolatilitySurface that doesn't permit extrapolation in strike dimension
*/
@Test(expectedExceptions = IllegalArgumentException.class)
public void testSurfaceWithoutStrikeExtrapolation() {
final CombinedInterpolatorExtrapolator interpOnlyStrike = getInterpolator(Interpolator1DFactory.LINEAR);
final Interpolator2D interp2D = new GridInterpolator2D(INTERPOLATOR_1D_EXPIRY, interpOnlyStrike);
final InterpolatedDoublesSurface surface = new InterpolatedDoublesSurface(EXPIRIES, STRIKES, VOLS, interp2D);
final BlackVolatilitySurfaceStrike volSurface = new BlackVolatilitySurfaceStrike(surface);
PRICER.expectedVariance(swap1, new StaticReplicationDataBundle(volSurface, DISCOUNT, FORWARD_CURVE));
}
/**
* Test of flat VolatilitySurface Strike vs Delta with tail extrapolation
*/
@Test
public void testFlatSurfaceOnStrikeAndDelta() {
final double testDeltaVar = PRICER.expectedVariance(swap6M, MARKET_W_CALLDELTASURF);
final double testStrikeVar = PRICER.expectedVariance(swap6M, MARKET_W_STRIKESURF);
final double targetVar = 0.28 * 0.28;
assertEquals(targetVar, testDeltaVar, 1e-9);
assertEquals(targetVar, testStrikeVar, 1e-9);
}
/**
* Test that an expired swap returns 0 variance
*/
@Test
public void testExpiredSwap() {
final double noMoreVariance = PRICER.expectedVariance(swap0, MARKET_W_STRIKESURF);
assertEquals(0.0, noMoreVariance, 1e-9);
}
/**
* Test that for the same volatility surface, the result is the same for integrals over strike, delta, moneyness and log-moneyness. The volatility surface is defined as a delta surface,
* and converted to a the other surfaces - so the surfaces are numerically different, but conceptually the same thing
*/
@Test
public void testVolSurface() {
final Function<Double, Double> surf = new Function<Double, Double>() {
@Override
public Double evaluate(final Double... x) {
final double delta = x[1];
return 0.2 + 0.3 * (delta - 0.4) * (delta - 0.4);
}
};
final BlackVolatilitySurfaceDelta surfaceDelta = new BlackVolatilitySurfaceDelta(FunctionalDoublesSurface.from(surf), FORWARD_CURVE);
final BlackVolatilitySurfaceLogMoneyness surfaceLogMoneyness = BlackVolatilitySurfaceConverter.toLogMoneynessSurface(surfaceDelta);
final BlackVolatilitySurfaceMoneyness surfaceMoneyness = BlackVolatilitySurfaceConverter.toMoneynessSurface(surfaceLogMoneyness);
final BlackVolatilitySurfaceStrike surfaceStrike = BlackVolatilitySurfaceConverter.toStrikeSurface(surfaceLogMoneyness);
final StaticReplicationDataBundle marketStrike = new StaticReplicationDataBundle(surfaceStrike, DISCOUNT, FORWARD_CURVE);
final StaticReplicationDataBundle marketLogMoneyness = new StaticReplicationDataBundle(surfaceLogMoneyness, DISCOUNT, FORWARD_CURVE);
final StaticReplicationDataBundle marketMoneyness = new StaticReplicationDataBundle(surfaceMoneyness, DISCOUNT, FORWARD_CURVE);
final StaticReplicationDataBundle marketDelta = new StaticReplicationDataBundle(surfaceDelta, DISCOUNT, FORWARD_CURVE);
final double totalVarStrike = PRICER.expectedVariance(swap1, marketStrike);
final double totalVarLogMoneyness = PRICER.expectedVariance(swap1, marketLogMoneyness);
final double totalVarMoneyness = PRICER.expectedVariance(swap1, marketMoneyness);
final double totalVarDelta = PRICER.expectedVariance(swap1, marketDelta);
assertEquals(totalVarStrike, totalVarDelta, 1e-7); //TODO why is integral over delta not as good?
assertEquals(totalVarStrike, totalVarLogMoneyness, TEST_TOL);
assertEquals(totalVarStrike, totalVarMoneyness, TEST_TOL);
}
/**
* For a symmetric mixed logNormal model (i.e. the forward is the same for all states of the world), then the expected variance is trivial to calculate
*/
@Test
public void testMixedLogNormalVolSurface() {
final double sigma1 = 0.2;
final double sigma2 = 1.0;
final double w = 0.9;
final Function<Double, Double> surf = new Function<Double, Double>() {
@Override
public Double evaluate(final Double... x) {
final double t = x[0];
final double k = x[1];
@SuppressWarnings("synthetic-access")
final double fwd = FORWARD_CURVE.getForward(t);
final boolean isCall = k > fwd;
final double price = w * BlackFormulaRepository.price(fwd, k, t, sigma1, isCall) + (1 - w) * BlackFormulaRepository.price(fwd, k, t, sigma2, isCall);
return BlackFormulaRepository.impliedVolatility(price, fwd, k, t, isCall);
}
};
final BlackVolatilitySurface<Strike> surfaceStrike = new BlackVolatilitySurfaceStrike(FunctionalDoublesSurface.from(surf));
final StaticReplicationDataBundle marketStrike = new StaticReplicationDataBundle(surfaceStrike, DISCOUNT, FORWARD_CURVE);
final double compVar = PRICER.expectedVariance(swap1, marketStrike);
final double compVarLimits = PRICER.expectedVariance(swap1, marketStrike);
final double expected = w * sigma1 * sigma1 + (1 - w) * sigma2 * sigma2;
assertEquals(expected, compVar, TEST_TOL);
assertEquals(expected, compVarLimits, 2e-4); //The substitution of shifted log-normal below the cutoff introduced some error
//test a forward start
final double compVar2 = PRICER.expectedVariance(swap5x10, marketStrike);
assertEquals(expected, compVar2, 10 * TEST_TOL);
}
/**
* A general mixed logNormal model. In this case the different forwards for the different branches, themselves generate volatility which must be accounted for
*/
@Test
public void testMixedLogNormalVolSurface2() {
final double t = swap2.getTimeToObsEnd();
final double fwd = FORWARD_CURVE.getForward(t);
final int n = 3;
final double[] sigma = new double[] {0.2, 0.5, 2.0 };
final double[] w = new double[] {0.8, 0.15, 0.05 };
final double[] f = new double[n];
f[0] = 1.05 * fwd;
f[1] = 0.9 * fwd;
double sum = 0;
for (int i = 0; i < n - 1; i++) {
sum += w[i] * f[i];
}
f[n - 1] = (fwd - sum) / w[n - 1];
Validate.isTrue(f[n - 1] > 0);
final Function<Double, Double> surf = new Function<Double, Double>() {
@Override
public Double evaluate(final Double... x) {
final double expiry = x[0];
final double k = x[1];
final boolean isCall = k > fwd;
double price = 0;
for (int i = 0; i < n; i++) {
price += w[i] * BlackFormulaRepository.price(f[i], k, expiry, sigma[i], isCall);
}
return BlackFormulaRepository.impliedVolatility(price, fwd, k, expiry, isCall);
}
};
final BlackVolatilitySurface<Strike> surfaceStrike = new BlackVolatilitySurfaceStrike(FunctionalDoublesSurface.from(surf));
//PDEUtilityTools.printSurface(null, surfaceStrike.getSurface(), 0.1, 4, 5, 300);
final StaticReplicationDataBundle marketStrike = new StaticReplicationDataBundle(surfaceStrike, DISCOUNT, FORWARD_CURVE);
final double compVar = PRICER.expectedVariance(swap2, marketStrike);
double expected = 0;
for (int i = 0; i < n; i++) {
expected += w[i] * (sigma[i] * sigma[i] + 2 / t * (f[i] / fwd + Math.log(fwd / f[i]) - 1));
}
assertEquals(expected, compVar, 5e-7);
}
// impliedVolatility Tests ------------------------------------------
@Test
public void testExpectedVolatility() {
final double sigmaSquared = PRICER.expectedVariance(swap5, MARKET_W_STRIKESURF);
final double sigma = PRICER.expectedVolatility(swap5, MARKET_W_STRIKESURF);
assertEquals(sigmaSquared, sigma * sigma, 1e-9);
}
}