/**
* Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.analytics.financial.model.volatility.smile.fitting.interpolation;
import org.apache.commons.lang.ObjectUtils;
import com.opengamma.OpenGammaRuntimeException;
import com.opengamma.analytics.math.differentiation.ScalarFirstOrderDifferentiator;
import com.opengamma.analytics.math.function.Function1D;
import com.opengamma.analytics.math.interpolation.DoubleQuadraticInterpolator1D;
import com.opengamma.analytics.math.interpolation.Interpolator1D;
import com.opengamma.analytics.math.interpolation.data.Interpolator1DDataBundle;
import com.opengamma.util.ArgumentChecker;
/**
* Fits a set of implied volatilities at given strikes by interpolating log-moneyness (ln(strike/forward)) against implied volatility using the supplied interpolator (the default
* is double quadratic). While this will fit any input data, there is no guarantee that the smile is arbitrage free, or indeed always positive, and should therefore be used with
* care, and only when other smile interpolators fail. The smile is extrapolated in both directions using shifted log-normals set to match the level and slope of the smile at
* the end point.
*/
public class SmileInterpolatorSpline implements GeneralSmileInterpolator {
// private static final Logger LOG = LoggerFactory.getLogger(ShiftedLogNormalTailExtrapolationFitter.class);
private static final Interpolator1D DEFAULT_INTERPOLATOR = new DoubleQuadraticInterpolator1D();
private static final ScalarFirstOrderDifferentiator DIFFERENTIATOR = new ScalarFirstOrderDifferentiator();
private static final ShiftedLogNormalTailExtrapolationFitter TAIL_FITTER = new ShiftedLogNormalTailExtrapolationFitter();
private static final String s_exception = "Exception"; // OG-Financial's BlackVolatilitySurfacePropertyNamesAndValues.EXCEPTION_SPLINE_EXTRAPOLATOR_FAILURE;
private static final String s_flat = "Flat"; // OG-Financial's BlackVolatilitySurfacePropertyNamesAndValues.FLAT_SPLINE_EXTRAPOLATOR_FAILURE;
private static final String s_quiet = "Quiet"; // OG-Financial's BlackVolatilitySurfacePropertyNamesAndValues.QUIET_SPLINE_EXTRAPOLATOR_FAILURE;
private final Interpolator1D _interpolator;
private final String _extrapolatorFailureBehaviour;
public SmileInterpolatorSpline() {
this(DEFAULT_INTERPOLATOR);
}
public SmileInterpolatorSpline(final Interpolator1D interpolator) {
ArgumentChecker.notNull(interpolator, "null interpolator");
_interpolator = interpolator;
_extrapolatorFailureBehaviour = s_exception; // This follows pattern of OG-Financial's BlackVolatilitySurfacePropertyNamesAndValues.EXCEPTION_SPLINE_EXTRAPOLATOR_FAILURE
}
public SmileInterpolatorSpline(final Interpolator1D interpolator, String extrapolatorFailureBehaviour) {
ArgumentChecker.notNull(interpolator, "null interpolator");
_interpolator = interpolator;
_extrapolatorFailureBehaviour = extrapolatorFailureBehaviour;
}
/**
* Gets the extrapolatorFailureBehaviour. If a shiftedLognormal model (Black with additional free parameter, F' = F*exp(mu)) fails to fit the boundary vol and the vol smile at that point...<p>
* "Exception": an exception will be thrown <p>
* "Quiet": the target gradient is reduced until a solution is found.<p>
* "Flat": the target gradient is zero. A trivial solution exists in which extrapolated volatilities equal target volatility.<p>
* @return the extrapolatorFailureBehaviour
*/
public final String getExtrapolatorFailureBehaviour() {
return _extrapolatorFailureBehaviour;
}
@Override
public Function1D<Double, Double> getVolatilityFunction(final double forward, final double[] strikes, final double expiry, final double[] impliedVols) {
ArgumentChecker.notNull(strikes, "strikes");
ArgumentChecker.notNull(impliedVols, "implied vols");
final int n = strikes.length;
ArgumentChecker.isTrue(impliedVols.length == n, "#strikes {} does not match #vols {}", n, impliedVols.length);
final double kL = strikes[0];
final double kH = strikes[n - 1];
final double[] x = new double[n];
for (int i = 0; i < n; i++) {
x[i] = Math.log(strikes[i] / forward);
}
// Interpolator
final Interpolator1DDataBundle data = _interpolator.getDataBundle(x, impliedVols);
final Function1D<Double, Double> interpFunc = new Function1D<Double, Double>() {
@SuppressWarnings("synthetic-access")
@Override
public Double evaluate(final Double k) {
final double m = Math.log(k / forward);
return _interpolator.interpolate(data, m);
}
};
final Function1D<Double, Boolean> domain = new Function1D<Double, Boolean>() {
@Override
public Boolean evaluate(final Double k) {
return k >= kL && k <= kH;
}
};
// Extrapolation of High and Low Strikes by ShiftedLogNormalTailExtrapolationFitter
// Solutions contain two parameters: [0] = mu = ln(shiftedForward / originalForward), [1] = theta = new ln volatility to use
final double[] shiftLnVolHighTail;
final double[] shiftLnVolLowTail;
// Volatility gradient (dVol/dStrike) of interpolator
final Function1D<Double, Double> dSigmaDx = DIFFERENTIATOR.differentiate(interpFunc, domain);
// The 'quiet' method reduces smile if the volatility gradient is either out of bounds of ShiftedLognormal model, or if root-finder fails to find solution
if (_extrapolatorFailureBehaviour.equalsIgnoreCase(s_quiet)) {
ArgumentChecker.isTrue(kL <= forward, "Cannot do left tail extrapolation when the lowest strike ({}) is greater than the forward ({})", kL, forward);
ArgumentChecker.isTrue(kH >= forward, "Cannot do right tail extrapolation when the highest strike ({}) is less than the forward ({})", kH, forward);
shiftLnVolHighTail = TAIL_FITTER.fitVolatilityAndGradRecursivelyByReducingSmile(forward, strikes[n - 1], impliedVols[n - 1], dSigmaDx.evaluate(kH), expiry);
shiftLnVolLowTail = TAIL_FITTER.fitVolatilityAndGradRecursivelyByReducingSmile(forward, kL, impliedVols[0], dSigmaDx.evaluate(kL), expiry);
// 'Exception' will throw an exception if it fails to fit to target vol and gradient provided by interpolating function at the boundary
} else if (_extrapolatorFailureBehaviour.equalsIgnoreCase(s_exception)) {
ArgumentChecker.isTrue(kL <= forward, "Cannot do left tail extrapolation when the lowest strike ({}) is greater than the forward ({})", kL, forward);
ArgumentChecker.isTrue(kH >= forward, "Cannot do right tail extrapolation when the highest strike ({}) is less than the forward ({})", kH, forward);
shiftLnVolHighTail = TAIL_FITTER.fitVolatilityAndGrad(forward, kH, impliedVols[n - 1], dSigmaDx.evaluate(kH), expiry);
shiftLnVolLowTail = TAIL_FITTER.fitVolatilityAndGrad(forward, kL, impliedVols[0], dSigmaDx.evaluate(kL), expiry);
// 'Flat' will simply return the target volatility at the boundary. Thus the target gradient is zero.
} else if (_extrapolatorFailureBehaviour.equalsIgnoreCase(s_flat)) {
shiftLnVolHighTail = TAIL_FITTER.fitVolatilityAndGrad(forward, kH, impliedVols[n - 1], 0.0, expiry);
shiftLnVolLowTail = TAIL_FITTER.fitVolatilityAndGrad(forward, kL, impliedVols[0], 0.0, expiry);
} else {
throw new OpenGammaRuntimeException("Unrecognized _extrapolatorFailureBehaviour. Looking for one of Exception, Quiet, or Flat");
}
// Resulting Functional Vol Surface
Function1D<Double, Double> volSmileFunction = new Function1D<Double, Double>() {
@Override
public Double evaluate(final Double k) {
if (k < kL) {
return ShiftedLogNormalTailExtrapolation.impliedVolatility(forward, k, expiry, shiftLnVolLowTail[0], shiftLnVolLowTail[1]);
} else if (k > kH) {
return ShiftedLogNormalTailExtrapolation.impliedVolatility(forward, k, expiry, shiftLnVolHighTail[0], shiftLnVolHighTail[1]);
} else {
return interpFunc.evaluate(k);
}
}
};
return volSmileFunction;
}
public Interpolator1D getInterpolator() {
return _interpolator;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + _interpolator.hashCode();
return result;
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final SmileInterpolatorSpline other = (SmileInterpolatorSpline) obj;
return ObjectUtils.equals(_interpolator, other._interpolator);
}
}