/** * 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.testng.annotations.Test; import org.threeten.bp.LocalDate; import org.threeten.bp.ZoneId; import org.threeten.bp.ZonedDateTime; import cern.jet.random.engine.MersenneTwister; import cern.jet.random.engine.MersenneTwister64; import cern.jet.random.engine.RandomEngine; import com.opengamma.analytics.financial.datasets.CalendarTarget; import com.opengamma.analytics.financial.equity.StaticReplicationDataBundle; import com.opengamma.analytics.financial.equity.variance.pricing.RealizedVariance; import com.opengamma.analytics.financial.equity.variance.pricing.VarianceSwapStaticReplication; import com.opengamma.analytics.financial.instrument.varianceswap.VarianceSwapDefinition; 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.surface.BlackVolatilitySurfaceStrike; import com.opengamma.analytics.financial.varianceswap.VarianceSwap; import com.opengamma.analytics.math.curve.ConstantDoublesCurve; 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.statistics.distribution.NormalDistribution; import com.opengamma.analytics.math.surface.ConstantDoublesSurface; import com.opengamma.analytics.math.surface.InterpolatedDoublesSurface; import com.opengamma.analytics.util.time.TimeCalculator; import com.opengamma.financial.convention.businessday.BusinessDayDateUtils; import com.opengamma.financial.convention.calendar.Calendar; import com.opengamma.timeseries.date.localdate.ImmutableLocalDateDoubleTimeSeries; import com.opengamma.timeseries.date.localdate.LocalDateDoubleTimeSeries; import com.opengamma.util.money.Currency; import com.opengamma.util.test.TestGroup; /** * Test. */ @Test(groups = TestGroup.UNIT) public class VarianceSwapPresentValueTest { // Setup ------------------------------------------ private static final RealizedVariance REALIZED_VOL_CAL = new RealizedVariance(); private static final RandomEngine RANDOM = new MersenneTwister64(MersenneTwister.DEFAULT_SEED); private static final NormalDistribution NORMAL = new NormalDistribution(0, 1, RANDOM); private static final ZoneId UTC = ZoneId.of("UTC"); private static final double s_Vol = 0.3; private static final BlackVolatilitySurfaceStrike s_FlatVolSurf = new BlackVolatilitySurfaceStrike(ConstantDoublesSurface.from(s_Vol)); private static final ZonedDateTime s_ObsStartTime = ZonedDateTime.of(2013, 12, 16, 12, 0, 0, 0, UTC);// ZonedDateTime.of(2013, 7, 27, 12, 0, 0, 0, UTC); // Saturday private static final ZonedDateTime s_ObsEndTime = ZonedDateTime.of(2015, 7, 30, 12, 0, 0, 0, UTC); // Thursday private static final ZonedDateTime s_SettlementTime = ZonedDateTime.of(2015, 8, 3, 12, 0, 0, 0, UTC);// Monday private static final Currency s_Ccy = Currency.EUR; private static final Calendar s_Calendar = new CalendarTarget("Eur"); private static final double s_AnnualizationFactor = 252.0; private static final double s_VolStrike = 0.3; private static final double s_VolNotional = 1e6; // 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 FORWARD = 100; // The pricing method private static final VarianceSwapStaticReplication PRICER = new VarianceSwapStaticReplication(); 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, 1.0, 1.0, 1.0, 1.0, 5.0, 5.0, 5.0, 5.0, 10.0, 10.0, 10.0, 10.0 }; private static final double[] STRIKES = new double[] {40, 80, 100, 120, 40, 80, 100, 120, 40, 80, 100, 120, 40, 80, 100, 120 }; private static final double[] VOLS = new double[] {0.28, 0.28, 0.28, 0.28, 0.25, 0.25, 0.25, 0.25, 0.26, 0.24, 0.23, 0.25, 0.20, 0.20, 0.20, 0.20 }; private static final CombinedInterpolatorExtrapolator INTERPOLATOR_1D_STRIKE = getInterpolator(Interpolator1DFactory.DOUBLE_QUADRATIC, Interpolator1DFactory.LINEAR_EXTRAPOLATOR, Interpolator1DFactory.FLAT_EXTRAPOLATOR); final static CombinedInterpolatorExtrapolator INTERPOLATOR_1D_EXPIRY = getInterpolator(Interpolator1DFactory.LINEAR, Interpolator1DFactory.FLAT_EXTRAPOLATOR, Interpolator1DFactory.FLAT_EXTRAPOLATOR); private static final InterpolatedDoublesSurface SURFACE = new InterpolatedDoublesSurface(EXPIRIES, STRIKES, VOLS, new GridInterpolator2D(INTERPOLATOR_1D_EXPIRY, INTERPOLATOR_1D_STRIKE)); private static final BlackVolatilitySurfaceStrike VOL_SURFACE = new BlackVolatilitySurfaceStrike(SURFACE); private static final StaticReplicationDataBundle MARKET = new StaticReplicationDataBundle(VOL_SURFACE, DISCOUNT, FORWARD_CURVE); // The derivative private static final double varStrike = 0.05; private static final double varNotional = 10000; // A notional of 10000 means PV is in bp private static final double now = 0; 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 noObsDisrupted = 0; private static final double annualization = 252; private static final double[] noObservations = {}; private static final double[] noObsWeights = {}; private static final double[] singleObsSoNoReturn = {80 }; private static final VarianceSwap swapStartsNow = new VarianceSwap(now, expiry5, expiry5, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, noObsDisrupted, singleObsSoNoReturn, noObsWeights); private static final ZonedDateTime today = ZonedDateTime.now(); private static final ZonedDateTime tomorrow = today.plusDays(1); private static final double tPlusOne = TimeCalculator.getTimeBetween(today, tomorrow); // Tests ------------------------------------------ private static double TOLERATED = 1.0E-9; @Test /** * Compare presentValue with impliedVariance, ensuring that spot starting varianceSwaps equal that coming only from implied part <p> * Ensure we handle the one underlying observation correctly. i.e. no *returns* yet * */ public void onFirstObsDateWithOneObs() { final double pv = PRICER.presentValue(swapStartsNow, MARKET); final double variance = PRICER.expectedVariance(swapStartsNow, MARKET); final double pvOfHedge = swapStartsNow.getVarNotional() * (variance - swapStartsNow.getVarStrike()) * MARKET.getDiscountCurve().getDiscountFactor(expiry5); assertEquals(pv, pvOfHedge, TOLERATED); } @Test /** * Variance is additive, hence a forward starting VarianceSwap may be decomposed into the difference of two spot starting ones. */ public void swapForwardStarting() { // First, create a swap which starts in 1 year and observes for a further four final VarianceSwap swapForwardStarting1to5 = new VarianceSwap(expiry1, expiry5, expiry5, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, noObsDisrupted, singleObsSoNoReturn, noObsWeights); final double pvFowardStart = PRICER.presentValue(swapForwardStarting1to5, MARKET); // Second, create two spot starting swaps. One that expires at the end of observations, one expiring at the beginnning final VarianceSwap swapSpotStarting1 = new VarianceSwap(now, expiry1, expiry5, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, noObsDisrupted, singleObsSoNoReturn, noObsWeights); final VarianceSwap swapSpotStarting5 = new VarianceSwap(now, expiry5, expiry5, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, noObsDisrupted, singleObsSoNoReturn, noObsWeights); final double pvSpot1 = PRICER.presentValue(swapSpotStarting1, MARKET); final double pvSpot5 = PRICER.presentValue(swapSpotStarting5, MARKET); final double pvDiffOfTwoSpotStarts = (5.0 * pvSpot5 - 1.0 * pvSpot1) / 4.0; assertEquals(pvFowardStart, pvDiffOfTwoSpotStarts, TOLERATED); } @Test(expectedExceptions = IllegalArgumentException.class) public void onFirstObsWithoutObs() { final VarianceSwap swapOnFirstObsWithoutObs = new VarianceSwap(now, expiry5, expiry5, varStrike, varNotional, Currency.EUR, annualization, nObsExpected, noObsDisrupted, noObservations, noObsWeights); @SuppressWarnings("unused") final double pv = PRICER.presentValue(swapOnFirstObsWithoutObs, MARKET); } final static double volAnnual = 0.28; final static double volDaily = volAnnual / Math.sqrt(annualization); final static double stdDevDaily = Math.sqrt(0.5) * volDaily; final static int nObs = 252 * 5; final static double[] obsWeight = {1.0 }; static double avgReturn = 0; static double avgSquareReturn = 0; static double[] obs = new double[nObs]; static { for (int i = 0; i < nObs; i++) { obs[i] = Math.exp(NORMAL.nextRandom()*stdDevDaily); if (i > 0) { avgReturn += Math.log(obs[i] / obs[i - 1]); avgSquareReturn += Math.pow(Math.log(obs[i] / obs[i - 1]), 2); } } avgReturn /= (nObs - 1); avgSquareReturn /= (nObs - 1); } @Test /** * After lastObs but before settlement date, presentValue == RealizedVar */ public void swapObservationsCompleted() { final VarianceSwap swapPaysTomorrow = new VarianceSwap(-1., -tPlusOne, tPlusOne, varStrike, varNotional, Currency.EUR, annualization, nObs, 0, obs, obsWeight); final double pv = PRICER.presentValue(swapPaysTomorrow, MARKET); final double variance = new RealizedVariance().evaluate(swapPaysTomorrow); final double pvOfHedge = swapStartsNow.getVarNotional() * (variance - swapStartsNow.getVarStrike()) * MARKET.getDiscountCurve().getDiscountFactor(tPlusOne); assertEquals(pvOfHedge, pv, TOLERATED); } @Test /** * After settlement, presentValue == 0.0 */ public void swapAfterSettlement() { final VarianceSwap swapEnded = new VarianceSwap(-1.0, -1.0 / 365, -1.0 / 365, varStrike, varNotional, Currency.EUR, annualization, nObs, 0, obs, obsWeight); final double pv = PRICER.presentValue(swapEnded, MARKET); assertEquals(0.0, pv, TOLERATED); } /** * The expected variance is computed by static replication - integration over vanilla option prices. These prices are * derived from a volatility surface which is flat at 30% - hence we should recover (up to some numerical tolerance) * 0.3^2 for the expected variance. */ @Test public void flatVolPrice() { VarianceSwapDefinition def = new VarianceSwapDefinition(s_ObsStartTime, s_ObsEndTime, s_SettlementTime, s_Ccy, s_Calendar, s_AnnualizationFactor, s_VolStrike, s_VolNotional); ZonedDateTime valueDate = ZonedDateTime.of(2013, 7, 25, 12, 0, 0, 0, UTC); // before first observation VarianceSwap varSwap = def.toDerivative(valueDate); assertEquals(0.0, REALIZED_VOL_CAL.evaluate(varSwap)); // No observations yet made, so zero realized volatility StaticReplicationDataBundle market = new StaticReplicationDataBundle(s_FlatVolSurf, DISCOUNT, FORWARD_CURVE); assertEquals(s_Vol * s_Vol, PRICER.expectedVariance(varSwap, market), 1e-10); // now look at a seasoned swap valueDate = ZonedDateTime.of(2014, 1, 28, 12, 0, 0, 0, UTC); // Tue // Don't include the valueDate in the observations int observationDays = BusinessDayDateUtils.getDaysBetween(s_ObsStartTime, valueDate, s_Calendar); System.out.println("\nObsevations added: " + observationDays); LocalDate[] dates = new LocalDate[observationDays]; double[] Prices = new double[observationDays]; double[] logPrices = new double[observationDays]; double dailyDrift = (0.05 - 0.5 * s_Vol * s_Vol) / s_AnnualizationFactor; double dailySD = s_Vol / Math.sqrt(s_AnnualizationFactor); dates[0] = s_ObsStartTime.toLocalDate(); Prices[0] = 100.0; logPrices[0] = Math.log(100.0); double sum2 = 0; for (int i = 1; i < observationDays; i++) { dates[i] = BusinessDayDateUtils.addWorkDays(dates[i - 1], 1, s_Calendar); logPrices[i] = logPrices[i - 1] + dailyDrift + dailySD * NORMAL.nextRandom(); Prices[i] = Math.exp(logPrices[i]); double rtn = logPrices[i] - logPrices[i - 1]; sum2 += rtn * rtn; } LocalDateDoubleTimeSeries ts = ImmutableLocalDateDoubleTimeSeries.of(dates, Prices); varSwap = def.toDerivative(valueDate, ts); double relVar = (observationDays - 1) / s_AnnualizationFactor * REALIZED_VOL_CAL.evaluate(varSwap); assertEquals(relVar, sum2, 1e-14); // Compute the price using the observations we have and the knowledge that volatility surface is flat (at 30%), and // compare this with the result of the calculator (which integrates over the vanilla option prices) double df = market.getDiscountCurve().getDiscountFactor(varSwap.getTimeToSettlement()); double expVar = (s_AnnualizationFactor * sum2 + s_Vol * s_Vol * (varSwap.getObsExpected() - observationDays)) / (varSwap.getObsExpected() - 1); double expPV = df * s_VolNotional / 2 / s_VolStrike * (expVar - s_VolStrike * s_VolStrike); double pv = PRICER.presentValue(varSwap, market); assertEquals(expPV, pv, 1e-5); } }