/** * Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.financial.analytics.volatility.surface; import it.unimi.dsi.fastutil.doubles.DoubleArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.threeten.bp.Clock; import org.threeten.bp.LocalDate; import org.threeten.bp.ZonedDateTime; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.analytics.financial.model.volatility.BlackFormulaRepository; import com.opengamma.analytics.math.MathException; import com.opengamma.analytics.math.curve.NodalDoublesCurve; import com.opengamma.analytics.util.time.TimeCalculator; import com.opengamma.core.id.ExternalSchemes; import com.opengamma.core.marketdatasnapshot.VolatilitySurfaceData; import com.opengamma.core.security.Security; import com.opengamma.core.security.SecuritySource; import com.opengamma.engine.ComputationTarget; import com.opengamma.engine.function.AbstractFunction; import com.opengamma.engine.function.FunctionCompilationContext; import com.opengamma.engine.function.FunctionExecutionContext; import com.opengamma.engine.function.FunctionInputs; import com.opengamma.engine.target.ComputationTargetType; import com.opengamma.engine.target.PrimitiveComputationTargetType; import com.opengamma.engine.value.ComputedValue; import com.opengamma.engine.value.SurfaceAndCubePropertyNames; import com.opengamma.engine.value.ValueProperties; import com.opengamma.engine.value.ValuePropertyNames; import com.opengamma.engine.value.ValueRequirement; import com.opengamma.engine.value.ValueRequirementNames; import com.opengamma.engine.value.ValueSpecification; import com.opengamma.financial.OpenGammaExecutionContext; import com.opengamma.financial.analytics.model.InstrumentTypeProperties; import com.opengamma.financial.analytics.model.irfutureoption.FutureOptionUtils; import com.opengamma.financial.convention.HolidaySourceCalendarAdapter; import com.opengamma.financial.convention.calendar.Calendar; import com.opengamma.financial.convention.expirycalc.ExchangeTradedInstrumentExpiryCalculator; import com.opengamma.financial.convention.expirycalc.IRFutureAndFutureOptionExpiryCalculator; import com.opengamma.financial.security.future.InterestRateFutureSecurity; import com.opengamma.financial.security.option.IRFutureOptionSecurity; import com.opengamma.id.ExternalId; import com.opengamma.util.CompareUtils; import com.opengamma.util.money.Currency; import com.opengamma.util.tuple.Pair; import com.opengamma.util.tuple.Pairs; /** * */ public class IRFutureOptionVolatilitySurfaceDataFunction extends AbstractFunction.NonCompiledInvoker { /** The logger */ private static final Logger s_logger = LoggerFactory.getLogger(IRFutureOptionVolatilitySurfaceDataFunction.class); private ConfigDBVolatilitySurfaceSpecificationSource _volatilitySurfaceSpecificationSource; @Override public void init(final FunctionCompilationContext context) { _volatilitySurfaceSpecificationSource = ConfigDBVolatilitySurfaceSpecificationSource.init(context, this); } @Override public Set<ComputedValue> execute(final FunctionExecutionContext executionContext, final FunctionInputs inputs, final ComputationTarget target, final Set<ValueRequirement> desiredValues) { final ValueRequirement desiredValue = desiredValues.iterator().next(); final Currency currency = target.getValue(PrimitiveComputationTargetType.CURRENCY); final Calendar calendar = new HolidaySourceCalendarAdapter(OpenGammaExecutionContext.getHolidaySource(executionContext), currency); final String surfaceName = desiredValue.getConstraint(ValuePropertyNames.SURFACE); final String fullSpecificationName = surfaceName + "_" + target.getUniqueId().getValue(); final VolatilitySurfaceSpecification specification = _volatilitySurfaceSpecificationSource.getSpecification(fullSpecificationName, InstrumentTypeProperties.IR_FUTURE_OPTION); if (specification == null) { throw new OpenGammaRuntimeException("Could not get volatility surface specification named " + fullSpecificationName); } String surfaceQuoteType = null; String surfaceQuoteUnits = null; String curveName = null; for (final ComputedValue input : inputs.getAllValues()) { final ValueSpecification spec = input.getSpecification(); final String valueName = spec.getValueName(); if (valueName.equals(ValueRequirementNames.VOLATILITY_SURFACE_DATA)) { surfaceQuoteType = spec.getProperty(SurfaceAndCubePropertyNames.PROPERTY_SURFACE_QUOTE_TYPE); surfaceQuoteUnits = spec.getProperty(SurfaceAndCubePropertyNames.PROPERTY_SURFACE_UNITS); } else if (valueName.equals(ValueRequirementNames.FUTURE_PRICE_CURVE_DATA)) { curveName = spec.getProperty(ValuePropertyNames.CURVE); } } if (surfaceQuoteType == null) { throw new OpenGammaRuntimeException("Could not get surface quote type"); } if (surfaceQuoteUnits == null) { throw new OpenGammaRuntimeException("Could not get surface quote units"); } final Clock snapshotClock = executionContext.getValuationClock(); final ZonedDateTime now = ZonedDateTime.now(snapshotClock); final ValueProperties surfaceProperties = ValueProperties.builder().with(ValuePropertyNames.SURFACE, surfaceName) .with(InstrumentTypeProperties.PROPERTY_SURFACE_INSTRUMENT_TYPE, InstrumentTypeProperties.IR_FUTURE_OPTION) .with(SurfaceAndCubePropertyNames.PROPERTY_SURFACE_QUOTE_TYPE, surfaceQuoteType).with(SurfaceAndCubePropertyNames.PROPERTY_SURFACE_UNITS, surfaceQuoteUnits).get(); final Object volatilityDataObject = inputs.getValue(new ValueRequirement(ValueRequirementNames.VOLATILITY_SURFACE_DATA, target.toSpecification(), surfaceProperties)); if (volatilityDataObject == null) { throw new OpenGammaRuntimeException("Could not get volatility surface data"); } @SuppressWarnings("unchecked") final VolatilitySurfaceData<Object, Object> surfaceData = (VolatilitySurfaceData<Object, Object>) volatilityDataObject; final ValueProperties properties = createValueProperties().with(ValuePropertyNames.SURFACE, surfaceName) .with(InstrumentTypeProperties.PROPERTY_SURFACE_INSTRUMENT_TYPE, InstrumentTypeProperties.IR_FUTURE_OPTION).get(); final ValueSpecification spec = new ValueSpecification(ValueRequirementNames.STANDARD_VOLATILITY_SURFACE_DATA, target.toSpecification(), properties); if (surfaceQuoteUnits.equals(SurfaceAndCubePropertyNames.VOLATILITY_QUOTE)) { return Collections.singleton(new ComputedValue(spec, getSurfaceFromVolatilityQuote(surfaceData, now, calendar))); } else if (surfaceQuoteUnits.equals(SurfaceAndCubePropertyNames.PRICE_QUOTE)) { final NodalDoublesCurve futuresPrices = getFuturePricesCurve(target, curveName, inputs); final SecuritySource securitySource = OpenGammaExecutionContext.getSecuritySource(executionContext); final VolatilitySurfaceData<Double, Double> volSurface = getSurfaceFromPriceQuote(specification, surfaceData, futuresPrices, now, surfaceQuoteType, calendar, securitySource); if (volSurface != null) { return Collections.singleton(new ComputedValue(spec, volSurface)); } return Collections.emptySet(); } else { throw new OpenGammaRuntimeException("Encountered an unexpected surfaceQuoteUnits. Valid values are found in SurfaceAndCubePropertyNames as VolatilityQuote or PriceQuote."); } } @Override public ComputationTargetType getTargetType() { return ComputationTargetType.CURRENCY; } @Override public Set<ValueSpecification> getResults(final FunctionCompilationContext context, final ComputationTarget target) { return Collections.singleton(new ValueSpecification(ValueRequirementNames.STANDARD_VOLATILITY_SURFACE_DATA, target.toSpecification(), createValueProperties() .withAny(ValuePropertyNames.SURFACE).with(InstrumentTypeProperties.PROPERTY_SURFACE_INSTRUMENT_TYPE, InstrumentTypeProperties.IR_FUTURE_OPTION).get())); } @Override public Set<ValueRequirement> getRequirements(final FunctionCompilationContext context, final ComputationTarget target, final ValueRequirement desiredValue) { final String surfaceName = desiredValue.getConstraints().getStrictValue(ValuePropertyNames.SURFACE); if (surfaceName == null) { return null; } final String fullSpecificationName = surfaceName + "_" + target.getUniqueId().getValue(); final VolatilitySurfaceSpecification specification = _volatilitySurfaceSpecificationSource.getSpecification(fullSpecificationName, InstrumentTypeProperties.IR_FUTURE_OPTION, context .getComputationTargetResolver().getVersionCorrection()); if (specification == null) { s_logger.error("Could not get volatility surface specification named {}", fullSpecificationName); return null; } final Set<ValueRequirement> requirements = new HashSet<>(); final ValueProperties surfaceProperties = ValueProperties.builder().with(ValuePropertyNames.SURFACE, surfaceName) .with(InstrumentTypeProperties.PROPERTY_SURFACE_INSTRUMENT_TYPE, InstrumentTypeProperties.IR_FUTURE_OPTION) .with(SurfaceAndCubePropertyNames.PROPERTY_SURFACE_QUOTE_TYPE, specification.getSurfaceQuoteType()) .with(SurfaceAndCubePropertyNames.PROPERTY_SURFACE_UNITS, specification.getQuoteUnits()).get(); requirements.add(new ValueRequirement(ValueRequirementNames.VOLATILITY_SURFACE_DATA, target.toSpecification(), surfaceProperties)); if (specification.getQuoteUnits().equals(SurfaceAndCubePropertyNames.PRICE_QUOTE)) { // Term structure of futures prices is also required final Set<String> curveNames = desiredValue.getConstraints().getValues(ValuePropertyNames.CURVE); final String curveName; if (curveNames == null || curveNames.size() != 1) { curveName = surfaceName; } else { curveName = curveNames.iterator().next(); } final ValueProperties curveProperties = ValueProperties.builder().with(ValuePropertyNames.CURVE, curveName) .with(InstrumentTypeProperties.PROPERTY_SURFACE_INSTRUMENT_TYPE, InstrumentTypeProperties.IR_FUTURE_PRICE).get(); final ValueRequirement curveRequirement = new ValueRequirement(ValueRequirementNames.FUTURE_PRICE_CURVE_DATA, target.toSpecification(), curveProperties); requirements.add(curveRequirement); } return requirements; } /** Build a volatility surface based on Expiry, T, and Strike, K. T is in measured in our standard OG-Analytic years */ private static VolatilitySurfaceData<Double, Double> getSurfaceFromVolatilityQuote(final VolatilitySurfaceData<Object, Object> optionVolatilities, final ZonedDateTime now, final Calendar calendar) { final Map<Pair<Double, Double>, Double> volatilityValues = new HashMap<>(); final DoubleArrayList tList = new DoubleArrayList(); final DoubleArrayList kList = new DoubleArrayList(); final LocalDate today = now.toLocalDate(); final Object[] xs = optionVolatilities.getXs(); for (final Object xObj : optionVolatilities.getXs()) { final Number x = (Number) xObj; final Double t = FutureOptionUtils.getIRFutureOptionTtm(x.intValue(), today, calendar); final Object[] ys = optionVolatilities.getYs(); for (final Object yObj : ys) { final Double y = (Double) yObj; final Double volatility = optionVolatilities.getVolatility(x, y); if (volatility != null) { tList.add(t); kList.add(y / 100.); volatilityValues.put(Pairs.of(t, y / 100.), volatility / 100); // TODO Normalisation, could this be done elsewhere? } } } return new VolatilitySurfaceData<>(optionVolatilities.getDefinitionName(), optionVolatilities.getSpecificationName(), optionVolatilities.getTarget(), tList.toArray(new Double[0]), kList.toArray(new Double[0]), volatilityValues); } /** Build a volatility surface based on Expiry, T, and Strike, K. T is in measured in our standard OG-Analytic years */ private static VolatilitySurfaceData<Double, Double> getSurfaceFromPriceQuote(final VolatilitySurfaceSpecification specification, final VolatilitySurfaceData<Object, Object> optionPrices, final NodalDoublesCurve futurePrices, final ZonedDateTime now, final String surfaceQuoteType, final Calendar calendar, final SecuritySource securitySource) { double callAboveStrike = 0; final SurfaceInstrumentProvider<Number, Double> instrumentProvider = (SurfaceInstrumentProvider<Number, Double>) specification.getSurfaceInstrumentProvider(); ExchangeTradedInstrumentExpiryCalculator expiryRule; if (instrumentProvider instanceof CallPutSurfaceInstrumentProvider) { expiryRule = ((CallPutSurfaceInstrumentProvider<?, ?>) instrumentProvider).getExpiryRuleCalculator(); } else { expiryRule = IRFutureAndFutureOptionExpiryCalculator.getInstance(); } if (specification.getSurfaceInstrumentProvider() instanceof CallPutSurfaceInstrumentProvider) { callAboveStrike = ((CallPutSurfaceInstrumentProvider<?, ?>) specification.getSurfaceInstrumentProvider()).useCallAboveStrike(); } final Map<Pair<Double, Double>, Double> volatilityValues = new HashMap<>(); final DoubleArrayList txList = new DoubleArrayList(); final DoubleArrayList kList = new DoubleArrayList(); final LocalDate today = now.toLocalDate(); for (final Object xObj : optionPrices.getXs()) { // Loop over option expiries final Number x = (Number) xObj; final LocalDate expiry = expiryRule.getExpiryDate(x.intValue(), today, calendar); final Double optionTtm = TimeCalculator.getTimeBetween(today, expiry); // Get the corresponding future, which may not share the same expiries as the option itself final Double[] futureExpiries = futurePrices.getXData(); final int nFutures = futureExpiries.length; if (nFutures == 0) { s_logger.info("No future prices found for surface : " + specification.getName()); return null; } // Loop over strikes for (final Object yObj : optionPrices.getYs()) { final Double y = (Double) yObj; try { final Double forward = specification.isUseUnderlyingSecurityForExpiry() ? getForwardFromSecurity(securitySource, today, futurePrices, instrumentProvider, x, y) : getForwardFromFuturesCurve(futurePrices, futureExpiries, optionTtm); final Double price = optionPrices.getVolatility(x, y); if (price != null) { try { // Compute the Black volatility implied from the option price final double volatility = getVolatility(surfaceQuoteType, y / 100.0, price, forward, optionTtm, callAboveStrike / 100.); if (!CompareUtils.closeEquals(volatility, 0.0)) { txList.add(optionTtm); kList.add(y / 100.0); volatilityValues.put(Pairs.of(optionTtm, y / 100.), volatility); } } catch (final MathException e) { s_logger.info("Could not imply volatility for ({}, {}); error was {}", new Object[] {x, y, e.getMessage() }); } catch (final IllegalArgumentException e) { s_logger.info("Could not imply volatility for ({}, {}); error was {}", new Object[] {x, y, e.getMessage() }); } } } catch (final IllegalArgumentException e) { s_logger.info("Could not imply volatility for ({}, {}); error was {}", new Object[] {x, y, e.getMessage() }); } catch (final OpenGammaRuntimeException e) { s_logger.info("Could not imply volatility for ({}, {}); error was {}", new Object[] {x, y, e.getMessage() }); } } } return new VolatilitySurfaceData<>(optionPrices.getDefinitionName(), optionPrices.getSpecificationName(), optionPrices.getTarget(), txList.toArray(new Double[0]), kList.toArray(new Double[0]), volatilityValues); } private static double getForwardFromSecurity(final SecuritySource securitySource, final LocalDate today, final NodalDoublesCurve futurePrices, final SurfaceInstrumentProvider<Number, Double> instrumentProvider, final Number x, final Double y) { ExternalId id = instrumentProvider.getInstrument(x, y, today); if (id.getScheme().equals(ExternalSchemes.BLOOMBERG_TICKER_WEAK)) { id = ExternalSchemes.bloombergTickerSecurityId(id.getValue()); } final Security security = securitySource.getSingle(id.toBundle()); if (security == null) { throw new OpenGammaRuntimeException("Could not get security with id " + id + " from the security source"); } if (!(security instanceof IRFutureOptionSecurity)) { throw new OpenGammaRuntimeException("Security in surface that was not an interest rate future option: " + security); } final IRFutureOptionSecurity irFutureOption = (IRFutureOptionSecurity) security; final InterestRateFutureSecurity underlyingFuture = (InterestRateFutureSecurity) securitySource.getSingle(irFutureOption.getUnderlyingId().toBundle()); if (underlyingFuture == null) { throw new OpenGammaRuntimeException("Could not get security with id " + irFutureOption.getUnderlyingId() + " from the security source"); } final LocalDate futureMaturity = underlyingFuture.getExpiry().getExpiry().toLocalDate(); final double underlyingExpiry1 = futureMaturity.isBefore(today) ? TimeCalculator.getTimeBetween(today, futureMaturity.plusYears(10)) : TimeCalculator.getTimeBetween(today, futureMaturity); return futurePrices.getYValue(underlyingExpiry1); } private static double getForwardFromFuturesCurve(final NodalDoublesCurve futurePrices, final Double[] futureExpiries, final double optionTtm) { Double underlyingExpiry; int i = 0; final int nFutures = futureExpiries.length; do { underlyingExpiry = futureExpiries[i++]; } while (underlyingExpiry < optionTtm && i < nFutures); if (underlyingExpiry < optionTtm) { s_logger.info("Requesting an option price where the underlying future price isn't available. " + "Either there are too many expiries in VolatilitySurfaceDefinition or too few in the corresponding FuturePriceCurveDefinition"); } return futurePrices.getYValue(underlyingExpiry); } /** Futures prices are required to form implied volatilities when the units of the input surface is quoted in prices. */ private static NodalDoublesCurve getFuturePricesCurve(final ComputationTarget target, final String curveName, final FunctionInputs inputs) { if (curveName == null) { throw new OpenGammaRuntimeException("Could not get curve name"); } final ValueRequirement futuresRequirement = new ValueRequirement(ValueRequirementNames.FUTURE_PRICE_CURVE_DATA, target.toSpecification(), ValueProperties.builder() .with(ValuePropertyNames.CURVE, curveName).with(InstrumentTypeProperties.PROPERTY_SURFACE_INSTRUMENT_TYPE, InstrumentTypeProperties.IR_FUTURE_PRICE).get()); final Object futurePricesObject = inputs.getValue(futuresRequirement); if (futurePricesObject == null) { throw new OpenGammaRuntimeException("Could not get futures price data"); } final NodalDoublesCurve futurePrices = (NodalDoublesCurve) futurePricesObject; return futurePrices; } /** FIXME This function relies on callAboveStrike. Needs a rework */ private static double getVolatility(final String surfaceQuoteType, final double strike, final double price, final double forward, final double t, final Double callAboveStrike) { if (surfaceQuoteType.equals(SurfaceAndCubeQuoteType.CALL_STRIKE)) { return BlackFormulaRepository.impliedVolatility(price, forward, strike, t, true); } if (surfaceQuoteType.equals(SurfaceAndCubeQuoteType.PUT_STRIKE)) { return BlackFormulaRepository.impliedVolatility(price, forward, strike, t, false); } if (surfaceQuoteType.equals(SurfaceAndCubeQuoteType.CALL_AND_PUT_STRIKE)) { if (callAboveStrike == null) { throw new OpenGammaRuntimeException("No value specified for useCallAboveStrikeValue in VolatilitySurfaceSpecification. See Configuration."); } // Futures Options are priced as options on rates, not prices. // A Call ON Futures PRICE is a PUT ON Futures RATE final boolean callOnRates = strike < callAboveStrike; return BlackFormulaRepository.impliedVolatility(price, 1 - forward, 1 - strike, t, callOnRates); } throw new OpenGammaRuntimeException("Cannot handle surface quote type " + surfaceQuoteType); } }