/** * /** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.analytics.financial.varianceswap.demo; import static com.opengamma.financial.convention.businessday.BusinessDayDateUtils.getWorkingDaysInclusive; 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.EquityVarianceSwap; import com.opengamma.analytics.financial.equity.variance.EquityVarianceSwapDefinition; 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.BlackFormulaRepository; import com.opengamma.analytics.financial.model.volatility.smile.fitting.interpolation.GeneralSmileInterpolator; import com.opengamma.analytics.financial.model.volatility.smile.fitting.interpolation.SmileInterpolatorSABR; import com.opengamma.analytics.financial.model.volatility.smile.fitting.interpolation.SmileInterpolatorSpline; import com.opengamma.analytics.financial.model.volatility.smile.function.SABRFormulaData; import com.opengamma.analytics.financial.model.volatility.smile.function.SABRHaganVolatilityFunction; import com.opengamma.analytics.financial.model.volatility.smile.function.VolatilityFunctionProvider; 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.BlackVolatilitySurfaceStrike; 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.function.Function1D; import com.opengamma.analytics.math.function.Function2D; import com.opengamma.analytics.math.statistics.distribution.NormalDistribution; import com.opengamma.analytics.math.surface.ConstantDoublesSurface; import com.opengamma.analytics.math.surface.FunctionalDoublesSurface; 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; /** * This class is designed to demonstrate some of the basic coverage of (equity) variance swaps in the analytics.<p> * The first <i>test</i> builds an {@link EquityVarianceSwapDefinition} - essentially the representation of the swap as it * would appear on a term sheet. This is turned into an <i>analytics</i> representation (an object containing just primitive * data types), which can be passed to <i>calculators</i>. We simulate some observations (the closing price) over the life * of the swap, and check that the payoff calculated by the {@link RealizedVariance} calculator is correct.<p> * * The second test prices a variance swap both before the first observation and after some (but not all) observations have * been made, using a flat volatility surface. The calculation involves the integral of $C(k)/k^2$ from zero to infinity * (in practice some cut-off), where $C(K)$ is price of a vanilla option (derived from the volatility surface) - this is * <i>static replication</i> of a derivation with a pay-off proportional to the log of the terminal value of the underlying. * Since the volatility is constant, the expected variance is just the square of this, so we can easily check our calculations.<p> * * The third test is similar to the second, except the volatility surface is no longer flat and instead comes from a * mixed log-normal model; again this gives us a simple analytic result to compare our calculation against. <p> * * The fourth test is more realistic; we start with some parameters of the SABR model and from this get the (Black) * volatility at nine strikes around (and including) ATMF. This 'market data' is then our starting point. We use two * different smile interpolation/extrapolation methods to reconstruct the smile from nine 'market prices' of vanilla * options. These smiles are used to calculate the expected variance (via static replication), and we compare the * these results with that derived from our knowledge of the SABR parameters. We do not recover exactly the 'true' answer, * and the different interpolation methods give different results - this is to be expected. <p> * * Despite the name, these tests are not specific to <b>equity</b> variance swaps; they are equally valid of variance * swaps in other asset classes. Where equity will differ from say FX, is in discrete dividends. The implicit assumption * in these tests is that dividends are paid continuously (i.e. a dividend yield) and this information is contained in the * forward curve. We have a model to handle discrete dividends @see <a>href ="http://developers.opengamma.com/quantitative-research/Equity-Variance-Swaps-with-Dividends-OpenGamma.pdf">here</a> * but do not give examples in this demo. */ public class EquityVarianceSwapDemo { private static final RealizedVariance REALIZED_VOL_CAL = new RealizedVariance(); private static final VarianceSwapStaticReplication PRICER = new VarianceSwapStaticReplication(); 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_Spot = 80; private static final double s_Drift = 0.05; private static final double s_Vol = 0.3; private static final ForwardCurve s_FwdCurve = new ForwardCurve(s_Spot, s_Drift); private static final YieldAndDiscountCurve s_DiscountCurve = new YieldCurve("Discount", ConstantDoublesCurve.from(s_Drift)); 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; /** * Demonstrate building an equity variance swap and adding time series of observations. Check that the realized variance * and present value of the swap are as expected when all the observations are known */ @Test(description = "Demo") public void buildSwap() { EquityVarianceSwapDefinition def = new EquityVarianceSwapDefinition(s_ObsStartTime, s_ObsEndTime, s_SettlementTime, s_Ccy, s_Calendar, s_AnnualizationFactor, s_VolStrike, s_VolNotional, false); ZonedDateTime obsDate = ZonedDateTime.of(2014, 8, 11, 12, 0, 0, 0, UTC); EquityVarianceSwap varSwap = def.toDerivative(obsDate); System.out.println("time to observation start: " + varSwap.getTimeToObsStart()); System.out.println("time to observation end: " + varSwap.getTimeToObsEnd()); System.out.println("time to settlement: " + varSwap.getTimeToSettlement()); System.out.println("Var Notional: " + varSwap.getVarNotional()); System.out.println("Vol Notional: " + varSwap.getVolNotional()); System.out.println("Annualization Factor: " + varSwap.getAnnualizationFactor()); // we haven't added any observations, so all historical observations are treated as disrupted System.out.println("Observations disrupted: " + varSwap.getObsDisrupted()); System.out.println("Observations expected: " + varSwap.getObsExpected()); double[] obs = varSwap.getObservations(); System.out.println("Observations: " + obs.length); // now add some randomly generated observations, and compute some values on the settlement date (i.e. all observations // are in the past) int observationDays = getWorkingDaysInclusive(s_ObsStartTime, s_ObsEndTime, 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 = (s_Drift - 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(s_SettlementTime, ts); System.out.println("Observations disrupted: " + varSwap.getObsDisrupted()); System.out.println("Observations expected: " + varSwap.getObsExpected()); obs = varSwap.getObservations(); System.out.println("Observations: " + obs.length); double relVar = REALIZED_VOL_CAL.evaluate(varSwap); // even with B-S dynamics, the realized variance will differ from the expected variance System.out.println("Expected variance: " + s_Vol * s_Vol + ", realized variance: " + relVar); double calRelVar = s_AnnualizationFactor / (observationDays - 1) * sum2; assertEquals(calRelVar, relVar, 1e-15); // check the calculation inside RealizedVariance is correct // check the price computed by VarianceSwapStaticReplication is as expected when all the observations are known double calPV = s_VolNotional / 2 / s_VolStrike * (calRelVar - s_VolStrike * s_VolStrike); StaticReplicationDataBundle market = new StaticReplicationDataBundle(s_FlatVolSurf, s_DiscountCurve, s_FwdCurve); double pv = PRICER.presentValue(varSwap, market); System.out.println("Variance swap value at settlement: " + pv); assertEquals(calPV, pv, 1e-9); } /** * 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(description = "Demo") 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, s_DiscountCurve, s_FwdCurve); 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 = (s_Drift - 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); } /** * A mixed log-normal model can give realistic looking smiles. It also allows a very simple analytic calculation of the * expected variance. This can be compared with the calculator that just 'sees' a volatility surface */ @Test(description = "Demo") public void testMixedLogNormalVolSurface() { final double sigma1 = 0.2; final double sigma2 = 1.0; final double w = 0.9; Function<Double, Double> surf = new Function<Double, Double>() { @Override public Double evaluate(Double... x) { double t = x[0]; double k = x[1]; @SuppressWarnings("synthetic-access") double fwd = s_FwdCurve.getForward(t); boolean isCall = k > fwd; double price = w * BlackFormulaRepository.price(fwd, k, t, sigma1, isCall) + (1 - w) * BlackFormulaRepository.price(fwd, k, t, sigma2, isCall); if (price < 1e-100) { return sigma2; } return BlackFormulaRepository.impliedVolatility(price, fwd, k, t, isCall); } }; // with a mixed log-normal, the expected variance is trivial double expected = w * sigma1 * sigma1 + (1 - w) * sigma2 * sigma2; VarianceSwapDefinition def = new VarianceSwapDefinition(s_ObsStartTime, s_ObsEndTime, s_SettlementTime, s_Ccy, s_Calendar, s_AnnualizationFactor, s_VolStrike, s_VolNotional); VarianceSwap varSwap = def.toDerivative(s_ObsStartTime); BlackVolatilitySurfaceStrike surfaceStrike = new BlackVolatilitySurfaceStrike(FunctionalDoublesSurface.from(surf)); double strikeVal = PRICER.expectedVariance(varSwap, new StaticReplicationDataBundle(surfaceStrike, s_DiscountCurve, s_FwdCurve)); assertEquals("strike", expected, strikeVal, 5e-12); // convert the vol surface to one parameterised by delta BlackVolatilitySurfaceDelta surfaceDelta = BlackVolatilitySurfaceConverter.toDeltaSurface(surfaceStrike, s_FwdCurve); double deltaVal = PRICER.expectedVariance(varSwap, new StaticReplicationDataBundle(surfaceDelta, s_DiscountCurve, s_FwdCurve)); assertEquals("delta", expected, deltaVal, 5e-8); // comment - convection to the delta surface involves root-finding, so the delta surface is less accurate, not the method using // the delta surface } /** * So far we have assumed that a volatility surface (valid for strikes from zero to infinity) is known. In practice we * will have a finite set of vanilla option prices. Assume initially that the expiry of these options coincides with the * expiry of the variance swap. */ @Test(description = "Demo") public void discreteOptionPricesTest() { VarianceSwapDefinition def = new VarianceSwapDefinition(s_ObsStartTime, s_ObsEndTime, s_SettlementTime, s_Ccy, s_Calendar, s_AnnualizationFactor, s_VolStrike, s_VolNotional); VarianceSwap varSwap = def.toDerivative(s_ObsStartTime); double expiry = varSwap.getTimeToObsEnd(); double fwd = s_FwdCurve.getForward(expiry); double df = s_DiscountCurve.getDiscountFactor(varSwap.getTimeToSettlement()); double[] strikes = new double[] {50.0, 60.0, 70.0, 80.0, fwd, 90.0, 100.0, 120.0, 150.0 }; double alpha = 0.2; double beta = 1.0; double rho = -0.0; double nu = 0.5; SABRFormulaData sabr = new SABRFormulaData(alpha, beta, rho, nu); VolatilityFunctionProvider<SABRFormulaData> volFunPro = new SABRHaganVolatilityFunction(); Function1D<SABRFormulaData, double[]> func = volFunPro.getVolatilityFunction(fwd, strikes, expiry); double[] blackVols = func.evaluate(sabr); GeneralSmileInterpolator smileInterpolator = new SmileInterpolatorSpline(); Function1D<Double, Double> smileFunc = smileInterpolator.getVolatilityFunction(fwd, strikes, expiry, blackVols); BlackVolatilitySurface<?> volSurface = makeSurfaceFromSmile(smileFunc); StaticReplicationDataBundle market = new StaticReplicationDataBundle(volSurface, s_DiscountCurve, s_FwdCurve); //For the case of beta = 1.0 & rho = 0.0, we know exactly the expected variance of a variance swap under SABR dynamics double alpha2 = alpha*alpha; double nu2 = nu*nu; double analExpVar = alpha2*(Math.exp(nu2*expiry)-1)/nu2/expiry; double expVar = PRICER.expectedVariance(varSwap, market); System.out.println("Expected variance - exact value: "+ analExpVar+", calculated value: "+expVar); //now use a different smile interpolator - get a slightly different answer smileInterpolator = new SmileInterpolatorSABR(); smileFunc = smileInterpolator.getVolatilityFunction(fwd, strikes, expiry, blackVols); volSurface = makeSurfaceFromSmile(smileFunc); market = new StaticReplicationDataBundle(volSurface, s_DiscountCurve, s_FwdCurve); expVar = PRICER.expectedVariance(varSwap, market); System.out.println("Expected variance - exact value: "+ analExpVar+", calculated value: "+expVar); } private BlackVolatilitySurface<?> makeSurfaceFromSmile(final Function1D<Double, Double> smileFunc) { Function<Double, Double> volFunc = new Function2D<Double, Double>() { @Override public Double evaluate(Double t, Double k) { return smileFunc.evaluate(k); } }; return new BlackVolatilitySurfaceStrike(FunctionalDoublesSurface.from(volFunc)); } }