/** * Copyright (C) 2014 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.sesame.equityindexoptions; import static com.opengamma.sesame.config.ConfigBuilder.argument; import static com.opengamma.sesame.config.ConfigBuilder.arguments; import static com.opengamma.sesame.config.ConfigBuilder.config; import static com.opengamma.sesame.config.ConfigBuilder.function; import static com.opengamma.sesame.config.ConfigBuilder.implementations; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.closeTo; import static org.hamcrest.core.Is.is; import static org.mockito.Mockito.mock; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; import org.threeten.bp.LocalDate; import org.threeten.bp.LocalTime; import org.threeten.bp.OffsetTime; import org.threeten.bp.ZoneOffset; import org.threeten.bp.ZonedDateTime; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Iterables; import com.opengamma.analytics.financial.model.interestrate.curve.ForwardCurve; import com.opengamma.analytics.financial.model.interestrate.curve.YieldCurve; import com.opengamma.analytics.financial.model.volatility.surface.VolatilitySurface; import com.opengamma.analytics.financial.provider.curve.CurveBuildingBlockBundle; import com.opengamma.analytics.financial.provider.description.interestrate.MulticurveProviderDiscount; import com.opengamma.analytics.math.curve.InterpolatedDoublesCurve; import com.opengamma.analytics.math.interpolation.CombinedInterpolatorExtrapolatorFactory; import com.opengamma.analytics.math.interpolation.GridInterpolator2D; import com.opengamma.analytics.math.interpolation.Interpolator1D; import com.opengamma.analytics.math.interpolation.Interpolator1DFactory; import com.opengamma.analytics.math.surface.DoublesSurface; import com.opengamma.analytics.math.surface.InterpolatedDoublesSurface; import com.opengamma.analytics.math.surface.NodalDoublesSurface; import com.opengamma.core.config.ConfigSource; import com.opengamma.core.historicaltimeseries.HistoricalTimeSeriesSource; import com.opengamma.core.holiday.HolidaySource; import com.opengamma.core.id.ExternalSchemes; import com.opengamma.core.legalentity.LegalEntitySource; import com.opengamma.core.link.ConfigLink; import com.opengamma.core.position.Counterparty; import com.opengamma.core.position.impl.SimpleCounterparty; import com.opengamma.core.position.impl.SimpleTrade; import com.opengamma.core.region.RegionSource; import com.opengamma.core.security.SecuritySource; import com.opengamma.financial.analytics.curve.exposure.CurrencyExposureFunction; import com.opengamma.financial.analytics.curve.exposure.ExposureFunctions; import com.opengamma.financial.analytics.model.fixedincome.BucketedCurveSensitivities; import com.opengamma.financial.convention.ConventionBundleSource; import com.opengamma.financial.currency.CurrencyMatrix; import com.opengamma.financial.security.option.EquityIndexOptionSecurity; import com.opengamma.financial.security.option.ExerciseType; import com.opengamma.financial.security.option.OptionType; import com.opengamma.id.ExternalId; import com.opengamma.sesame.CurveSelector; import com.opengamma.sesame.CurveSelectorMulticurveBundleFn; import com.opengamma.sesame.DefaultForwardCurveFn; import com.opengamma.sesame.DefaultGridInterpolator2DFn; import com.opengamma.sesame.DiscountingMulticurveCombinerFn; import com.opengamma.sesame.Environment; import com.opengamma.sesame.ForwardCurveFn; import com.opengamma.sesame.GridInterpolator2DFn; import com.opengamma.sesame.MarketExposureSelector; import com.opengamma.sesame.MulticurveBundle; import com.opengamma.sesame.UnderlyingForwardCurveFn; import com.opengamma.sesame.config.FunctionModelConfig; import com.opengamma.sesame.engine.CalculationArguments; import com.opengamma.sesame.engine.ComponentMap; import com.opengamma.sesame.engine.FunctionRunner; import com.opengamma.sesame.equity.StaticReplicationDataBundleFn; import com.opengamma.sesame.equity.StrikeDataBundleFn; import com.opengamma.sesame.equity.StrikeDataFromPriceBundleFn; import com.opengamma.sesame.graph.FunctionModel; import com.opengamma.sesame.marketdata.EmptyMarketDataFactory; import com.opengamma.sesame.marketdata.EmptyMarketDataSpec; import com.opengamma.sesame.marketdata.ForwardCurveId; import com.opengamma.sesame.marketdata.MarketDataEnvironment; import com.opengamma.sesame.marketdata.MarketDataEnvironmentBuilder; import com.opengamma.sesame.marketdata.MulticurveId; import com.opengamma.sesame.marketdata.RawId; import com.opengamma.sesame.marketdata.SurfaceId; import com.opengamma.sesame.marketdata.VolatilitySurfaceId; import com.opengamma.sesame.marketdata.builders.MarketDataBuilder; import com.opengamma.sesame.marketdata.builders.MarketDataBuilders; import com.opengamma.sesame.marketdata.builders.MarketDataEnvironmentFactory; import com.opengamma.sesame.trade.EquityIndexOptionTrade; import com.opengamma.util.function.Function; import com.opengamma.util.money.Currency; import com.opengamma.util.money.CurrencyAmount; import com.opengamma.util.result.Result; import com.opengamma.util.test.TestGroup; import com.opengamma.util.time.DateUtils; import com.opengamma.util.time.Expiry; /** * Test for equity index options. */ @Test(groups = TestGroup.UNIT) public class EquityIndexOptionFnTest { /** Tolerances */ private static final double TOLERANCE_GREEKS = 1.0E-6; private static final double TOLERANCE_PV = 1.0E-2; /** Expected values validated Bloomberg */ public static final double EXPECTED_PV_PRICE_SURFACE = 9150; public static final double EXPECTED_PV = 9159.56570; public static final double EXPECTED_DELTA = 0.982654738; public static final double EXPECTED_GAMMA = 0.001294049921; public static final double EXPECTED_VEGA = 3.23429443418; public static final double EXPECTED_PV01 = 0.1637725628; /** Other expected values */ public static final double[] EXPECTED_BUCKETED_PV01_VALUES = new double[] { 0.05858427321612262, 0.10518828963037462, 0.0, 0.0, 0.0, 0.0, 0.0 }; private EquityIndexOptionFn _functionPriceSurface; private EquityIndexOptionFn _functionFlatForward; private EquityIndexOptionFn _functionForward; private FunctionRunner _functionRunner; static final String JPY_DISCOUNTING = "JPY Discounting"; public static final ZonedDateTime VALUATION_TIME = DateUtils.getUTCDate(2014, 7, 22); private static ExternalId ID = ExternalId.of(ExternalSchemes.BLOOMBERG_TICKER, "NK225"); private static final CalculationArguments ARGS = CalculationArguments.builder() .valuationTime(VALUATION_TIME) .marketDataSpecification(EmptyMarketDataSpec.INSTANCE) .build(); private static MarketDataEnvironment ENV = createMarketDataEnvironment(); private static final EquityIndexOptionTrade EQUITY_INDEX_OPTION_TRADE_1 = createOptionTrade(BigDecimal.ONE); private static final BigDecimal POSITION_SIZE = BigDecimal.valueOf(5); private static final EquityIndexOptionTrade EQUITY_INDEX_OPTION_TRADE_2 = createOptionTrade(POSITION_SIZE); private static final MulticurveBundle createBundle() { Interpolator1D linearFlat = CombinedInterpolatorExtrapolatorFactory.getInterpolator(Interpolator1DFactory.LINEAR, Interpolator1DFactory.FLAT_EXTRAPOLATOR, Interpolator1DFactory.FLAT_EXTRAPOLATOR); double[] time = {0.003, 0.25, 0.5, 1.0, 2.0, 5.0, 10.0}; double[] zc = {0.0025, 0.0025, 0.0025, 0.0025, 0.0025, 0.0025, 0.0025}; InterpolatedDoublesCurve curve = new InterpolatedDoublesCurve(time, zc, linearFlat, true, JPY_DISCOUNTING); MulticurveProviderDiscount multicurve = new MulticurveProviderDiscount(); multicurve.setCurve(Currency.JPY, new YieldCurve(JPY_DISCOUNTING, curve)); return new MulticurveBundle(multicurve, new CurveBuildingBlockBundle()); } private static ExposureFunctions createExposureFunction() { List<String> exposureFunctions = ImmutableList.of(CurrencyExposureFunction.NAME); Map<ExternalId, String> idsToNames = new HashMap<>(); idsToNames.put(ExternalId.of("CurrencyISO", "JPY"), "MultiCurve"); return new ExposureFunctions("Exposure", exposureFunctions, idsToNames); } private static MarketDataEnvironment createMarketDataEnvironment() { MarketDataEnvironmentBuilder builder = new MarketDataEnvironmentBuilder(); builder.add(MulticurveId.of("MultiCurve"), createBundle()); builder.add(RawId.of(ID.toBundle()), 19624.84); builder.add(ForwardCurveId.of(ID.getValue()), createForwardCurve()); builder.add(VolatilitySurfaceId.of("CALL_NK225"), createVolatilitySurface()); builder.add(SurfaceId.of("CALL_NK225"), createPriceSurface()); builder.valuationTime(VALUATION_TIME); return builder.build(); } private static EquityIndexOptionTrade createOptionTrade(BigDecimal quantity) { Counterparty counterparty = new SimpleCounterparty(ExternalId.of(Counterparty.DEFAULT_SCHEME, "COUNTERPARTY")); LocalDate tradeDate = LocalDate.of(2000, 1, 1); OffsetTime tradeTime = OffsetTime.of(LocalTime.of(0, 0), ZoneOffset.UTC); SimpleTrade trade = new SimpleTrade(createEquityIndexOptionSecurity(), quantity, counterparty, tradeDate, tradeTime); trade.setPremium(0.0); trade.setPremiumCurrency(Currency.JPY); return new EquityIndexOptionTrade(trade); } private static EquityIndexOptionSecurity createEquityIndexOptionSecurity() { OptionType optionType = OptionType.CALL; double strike = 10500; Currency currency = Currency.JPY; ExternalId underlyingId = ID; ExerciseType exerciseType = ExerciseType.of("European"); Expiry expiry = new Expiry(VALUATION_TIME.plusDays(59)); double pointValue = 1; String exchange = "XJPY"; EquityIndexOptionSecurity security = new EquityIndexOptionSecurity(optionType, strike, currency, underlyingId, exerciseType, expiry, pointValue, exchange); security.setName(ID.getValue() + " " + optionType.toString() + " Option " + expiry.getExpiry().toString()); return security; } @BeforeClass public void setUp() { FunctionModelConfig configFlatForward = config( arguments( function( MarketExposureSelector.class, argument("exposureFunctions", ConfigLink.resolved(createExposureFunction())))), implementations( EquityIndexOptionFn.class, DefaultEquityIndexOptionFn.class, StaticReplicationDataBundleFn.class, StrikeDataBundleFn.class, CurveSelector.class, MarketExposureSelector.class, ForwardCurveFn.class, UnderlyingForwardCurveFn.class, DiscountingMulticurveCombinerFn.class, CurveSelectorMulticurveBundleFn.class)); FunctionModelConfig configForward = config( arguments( function( MarketExposureSelector.class, argument("exposureFunctions", ConfigLink.resolved(createExposureFunction())))), implementations( EquityIndexOptionFn.class, DefaultEquityIndexOptionFn.class, StaticReplicationDataBundleFn.class, StrikeDataBundleFn.class, CurveSelector.class, MarketExposureSelector.class, ForwardCurveFn.class, DefaultForwardCurveFn.class, DiscountingMulticurveCombinerFn.class, CurveSelectorMulticurveBundleFn.class)); FunctionModelConfig configPriceSurface = config( arguments( function( MarketExposureSelector.class, argument("exposureFunctions", ConfigLink.resolved(createExposureFunction()))), function( DefaultGridInterpolator2DFn.class, argument("xInterpolatorName", "Linear"), argument("xLeftExtrapolatorName", "FlatExtrapolator"), argument("xRightExtrapolatorName", "FlatExtrapolator"), argument("yInterpolatorName", "Linear"), argument("yLeftExtrapolatorName", "FlatExtrapolator"), argument("yRightExtrapolatorName", "FlatExtrapolator")) ), implementations( GridInterpolator2DFn.class, DefaultGridInterpolator2DFn.class, EquityIndexOptionFn.class, DefaultEquityIndexOptionFn.class, StaticReplicationDataBundleFn.class, StrikeDataFromPriceBundleFn.class, CurveSelector.class, MarketExposureSelector.class, ForwardCurveFn.class, DefaultForwardCurveFn.class, DiscountingMulticurveCombinerFn.class, CurveSelectorMulticurveBundleFn.class)); ImmutableMap<Class<?>, Object> components = generateBaseComponents(); ComponentMap componentMap = ComponentMap.of(components); _functionFlatForward = FunctionModel.build(EquityIndexOptionFn.class, configFlatForward, componentMap); _functionForward = FunctionModel.build(EquityIndexOptionFn.class, configForward, componentMap); _functionPriceSurface = FunctionModel.build(EquityIndexOptionFn.class, configPriceSurface, componentMap); EmptyMarketDataFactory dataFactory = new EmptyMarketDataFactory(); ConfigLink<CurrencyMatrix> currencyMatrixLink = ConfigLink.resolved(componentMap.getComponent(CurrencyMatrix.class)); List<MarketDataBuilder> builders = MarketDataBuilders.standard(componentMap, "dataSource", currencyMatrixLink); MarketDataEnvironmentFactory environmentFactory = new MarketDataEnvironmentFactory(dataFactory, builders); _functionRunner = new FunctionRunner(environmentFactory); } private static ImmutableMap<Class<?>, Object> generateBaseComponents() { return generateComponentMap(mock(RegionSource.class), mock(HolidaySource.class), mock(HistoricalTimeSeriesSource.class), mock(SecuritySource.class), mock(ConfigSource.class), mock(ConventionBundleSource.class), mock(LegalEntitySource.class), mock(CurrencyMatrix.class)); } private static ImmutableMap<Class<?>, Object> generateComponentMap(Object... components) { ImmutableMap.Builder<Class<?>, Object> builder = ImmutableMap.builder(); for (Object component : components) { builder.put(component.getClass().getInterfaces()[0], component); } return builder.build(); } private static VolatilitySurface createVolatilitySurface() { Interpolator1D linearFlat = CombinedInterpolatorExtrapolatorFactory.getInterpolator(Interpolator1DFactory.LINEAR, Interpolator1DFactory.FLAT_EXTRAPOLATOR, Interpolator1DFactory.FLAT_EXTRAPOLATOR); GridInterpolator2D interpolator2D = new GridInterpolator2D(linearFlat, linearFlat); InterpolatedDoublesSurface surface = InterpolatedDoublesSurface.from( new double[] {31d/356, 59d/365, 94d/365, 31d/356, 59d/365, 94d/365}, new double[] {20500, 10500, 12750, 19625, 10750, 13000}, new double[] {17.3885/100, 78.7594/100, 39.8789/100, 19.0244/100, 76.0916/100, 38.2484/100}, interpolator2D ); return new VolatilitySurface(surface); } private static DoublesSurface createPriceSurface() { return new NodalDoublesSurface( new double[] {31d/356, 59d/365, 94d/365, 31d/356, 59d/365, 94d/365}, new double[] {20500, 10500, 12750, 19625, 10750, 13000}, new double[] {110, 9150, 6880, 435, 8900, 6630} ); } private static ForwardCurve createForwardCurve() { double[] maturities = {31d/365, 122d/365, 213d/365, 304d/365, 395d/365, 486d/365, 577d/365, 759d/365, 941d/365, 1123d/365, 1312d/365, 1494d/365, 1676d/365}; double[] prices = {19627, 19612, 19518, 19498, 19389, 19374, 19279, 19151, 19043, 18917, 18812, 18687, 18582}; Interpolator1D interpolator = CombinedInterpolatorExtrapolatorFactory.getInterpolator( Interpolator1DFactory.LINEAR, Interpolator1DFactory.LINEAR_EXTRAPOLATOR, Interpolator1DFactory.FLAT_EXTRAPOLATOR); return new ForwardCurve(InterpolatedDoublesCurve.from(maturities, prices, interpolator)); } //------------------------------------------------------------------------- @Test public void testPresentValueWithPriceSurface() { Result<CurrencyAmount> result = runPresentValue(_functionPriceSurface, EQUITY_INDEX_OPTION_TRADE_1); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue().getAmount(), is(closeTo(EXPECTED_PV_PRICE_SURFACE, TOLERANCE_PV))); } @Test public void testPresentValueWithFlatForwardCurve() { Result<CurrencyAmount> result = runPresentValue(_functionFlatForward, EQUITY_INDEX_OPTION_TRADE_1); assertThat(result.isSuccess(), is(true)); } @Test public void testPresentValueWithForwardCurve() { Result<CurrencyAmount> result = runPresentValue(_functionForward, EQUITY_INDEX_OPTION_TRADE_1); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue().getAmount(), is(closeTo(EXPECTED_PV, TOLERANCE_PV))); } @Test public void testPresentValuePositionScalingWithForwardCurve() { Result<CurrencyAmount> result = runPresentValue(_functionForward, EQUITY_INDEX_OPTION_TRADE_2); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue().getAmount(), is(closeTo(POSITION_SIZE.doubleValue() * EXPECTED_PV, TOLERANCE_PV))); } private Result<CurrencyAmount> runPresentValue(final EquityIndexOptionFn fn, final EquityIndexOptionTrade trade) { return _functionRunner.runFunction( ARGS, ENV, new Function<Environment, Result<CurrencyAmount>>() { @Override public Result<CurrencyAmount> apply(Environment env) { return fn.calculatePv(env, trade); } }); } //------------------------------------------------------------------------- @Test public void testDeltaWithForwardCurve() { Result<Double> result = _functionRunner.runFunction( ARGS, ENV, new Function<Environment, Result<Double>>() { @Override public Result<Double> apply(Environment env) { return _functionForward.calculateDelta(env, EQUITY_INDEX_OPTION_TRADE_1); } }); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue(), is(closeTo(EXPECTED_DELTA, TOLERANCE_GREEKS))); } @Test public void testGammaWithForwardCurve() { Result<Double> result = _functionRunner.runFunction( ARGS, ENV, new Function<Environment, Result<Double>>() { @Override public Result<Double> apply(Environment env) { return _functionForward.calculateGamma(env, EQUITY_INDEX_OPTION_TRADE_1); } }); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue(), is(closeTo(EXPECTED_GAMMA, TOLERANCE_GREEKS))); } @Test public void testVegaWithForwardCurve() { Result<Double> result = _functionRunner.runFunction( ARGS, ENV, new Function<Environment, Result<Double>>() { @Override public Result<Double> apply(Environment env) { return _functionForward.calculateVega(env, EQUITY_INDEX_OPTION_TRADE_1); } }); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue(), is(closeTo(EXPECTED_VEGA, TOLERANCE_GREEKS))); } //------------------------------------------------------------------------- @Test public void testPV01WithForwardCurve() { Result<Double> result = runPV01WithForwardCurve(EQUITY_INDEX_OPTION_TRADE_1); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue(), is(closeTo(EXPECTED_PV01, TOLERANCE_GREEKS))); } @Test public void testPV01PositionScalingWithForwardCurve() { Result<Double> result = runPV01WithForwardCurve(EQUITY_INDEX_OPTION_TRADE_2); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue(), is(closeTo(POSITION_SIZE.doubleValue() * EXPECTED_PV01, TOLERANCE_GREEKS))); } private Result<Double> runPV01WithForwardCurve(final EquityIndexOptionTrade trade) { return _functionRunner.runFunction( ARGS, ENV, new Function<Environment, Result<Double>>() { @Override public Result<Double> apply(Environment env) { return _functionForward.calculatePv01(env, trade); } }); } //------------------------------------------------------------------------- @Test public void testBucketedPV01WithForwardCurve() { testBucketedPV01WithForwardCurve(EQUITY_INDEX_OPTION_TRADE_1, BigDecimal.ONE); } @Test public void testBucketedPV01PositionScalingWithForwardCurve() { testBucketedPV01WithForwardCurve(EQUITY_INDEX_OPTION_TRADE_2, POSITION_SIZE); } private void testBucketedPV01WithForwardCurve(final EquityIndexOptionTrade trade, BigDecimal scalingFactor) { Result<BucketedCurveSensitivities> result = _functionRunner.runFunction( ARGS, ENV, new Function<Environment,Result<BucketedCurveSensitivities>>() { @Override public Result<BucketedCurveSensitivities> apply(Environment env) { return _functionForward.calculateBucketedPv01(env, trade); } }); assertThat(result.isSuccess(), is(true)); assertThat(result.getValue().getSensitivities().size(), is(1)); double[] sensitivities = Iterables.getOnlyElement(result.getValue().getSensitivities().values()).getValues(); assertThat(sensitivities.length, is(EXPECTED_BUCKETED_PV01_VALUES.length)); for (int i = 0; i < sensitivities.length; i++) { assertThat(sensitivities[i], is(scalingFactor.doubleValue() * EXPECTED_BUCKETED_PV01_VALUES[i])); } } }