/**
* Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.analytics.financial.model.volatility.surface;
import static com.opengamma.analytics.math.FunctionUtils.square;
import java.util.Arrays;
import org.apache.commons.lang.ObjectUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.SurfaceArrayUtils;
import com.opengamma.analytics.financial.model.volatility.smile.fitting.sabr.SmileSurfaceDataBundle;
import com.opengamma.analytics.math.function.Function;
import com.opengamma.analytics.math.function.Function1D;
import com.opengamma.analytics.math.interpolation.CombinedInterpolatorExtrapolatorFactory;
import com.opengamma.analytics.math.interpolation.Interpolator1D;
import com.opengamma.analytics.math.interpolation.Interpolator1DFactory;
import com.opengamma.analytics.math.interpolation.data.Interpolator1DDataBundle;
import com.opengamma.analytics.math.surface.FunctionalDoublesSurface;
import com.opengamma.analytics.util.serialization.InvokedSerializedForm;
import com.opengamma.util.ArgumentChecker;
/**
*
*/
public class VolatilitySurfaceInterpolator {
private static final Logger LOGGER = LoggerFactory.getLogger(VolatilitySurfaceInterpolator.class);
private static final GeneralSmileInterpolator DEFAULT_SMILE_INTERPOLATOR = new SmileInterpolatorSABR();
private static final Interpolator1D DEFAULT_TIME_INTERPOLATOR = CombinedInterpolatorExtrapolatorFactory.getInterpolator(Interpolator1DFactory.NATURAL_CUBIC_SPLINE,
Interpolator1DFactory.LINEAR_EXTRAPOLATOR);
private static final boolean USE_LOG_TIME = true;
private static final boolean USE_INTEGRATED_VARIANCE = true;
private static final boolean USE_LOG_VALUE = true;
private final GeneralSmileInterpolator _smileInterpolator;
private final Interpolator1D _timeInterpolator;
private final boolean _useLogTime;
private final boolean _useLogVar;
private final boolean _useIntegratedVariance;
public VolatilitySurfaceInterpolator() {
this(DEFAULT_SMILE_INTERPOLATOR, DEFAULT_TIME_INTERPOLATOR, USE_LOG_TIME, USE_INTEGRATED_VARIANCE, USE_LOG_VALUE);
}
public VolatilitySurfaceInterpolator(final GeneralSmileInterpolator smileInterpolator) {
this(smileInterpolator, DEFAULT_TIME_INTERPOLATOR, USE_LOG_TIME, USE_INTEGRATED_VARIANCE, USE_LOG_VALUE);
}
public VolatilitySurfaceInterpolator(final Interpolator1D timeInterpolator) {
this(DEFAULT_SMILE_INTERPOLATOR, timeInterpolator, USE_LOG_TIME, USE_INTEGRATED_VARIANCE, USE_LOG_VALUE);
}
public VolatilitySurfaceInterpolator(final GeneralSmileInterpolator smileInterpolator, final Interpolator1D timeInterpolator) {
this(smileInterpolator, timeInterpolator, USE_LOG_TIME, USE_INTEGRATED_VARIANCE, USE_LOG_VALUE);
}
/**
* <b>Note</b> The combination of useIntegratedVariance = true, useLogTime != useLogValue can produce very bad results, including considerable dips/humps between points at the same level (all other
* combinations give a flat line), and thus should be avoided.
*
* @param useIntegratedVariance if true integrated variance ($\sigma^2t$) is used in the interpolation, otherwise variance is used
* @param useLogTime if true the natural-log of the time values are used in interpolation, if false the time values are used directly. This can be useful if the expiries vary greatly in magnitude
* @param useLogVariance If true the log of variance (actually either variance or integrated variance) is used in the interpolation
*/
public VolatilitySurfaceInterpolator(final boolean useIntegratedVariance, final boolean useLogTime, final boolean useLogVariance) {
_smileInterpolator = DEFAULT_SMILE_INTERPOLATOR;
_timeInterpolator = DEFAULT_TIME_INTERPOLATOR;
_useLogTime = useLogTime;
_useIntegratedVariance = useIntegratedVariance;
_useLogVar = useLogVariance;
if (_useIntegratedVariance && _useLogVar != _useLogTime) {
LOGGER.warn("The combination of useIntegratedVariance = true, useLogTime != useLogValue can produce very bad results, including considerable dips between "
+ "points at the same level (all other combinations give a flat line), and thus should be avoided.");
}
}
public VolatilitySurfaceInterpolator(final GeneralSmileInterpolator smileInterpolator, final boolean useLogTime, final boolean useIntegratedVariance,
final boolean useLogValue) {
this(smileInterpolator, DEFAULT_TIME_INTERPOLATOR, useLogTime, useIntegratedVariance, useLogValue);
}
public VolatilitySurfaceInterpolator(final Interpolator1D timeInterpolator, final boolean useLogTime, final boolean useIntegratedVariance, final boolean useLogValue) {
this(DEFAULT_SMILE_INTERPOLATOR, timeInterpolator, useLogTime, useIntegratedVariance, useLogValue);
}
public VolatilitySurfaceInterpolator(final GeneralSmileInterpolator smileInterpolator, final Interpolator1D timeInterpolator, final boolean useLogTime,
final boolean useIntegratedVariance, final boolean useLogValue) {
ArgumentChecker.notNull(smileInterpolator, "null smile interpolator");
ArgumentChecker.notNull(timeInterpolator, "null time interpolator");
_smileInterpolator = smileInterpolator;
_timeInterpolator = timeInterpolator;
_useLogTime = useLogTime;
_useIntegratedVariance = useIntegratedVariance;
_useLogVar = useLogValue;
if (_useIntegratedVariance && _useLogVar != _useLogTime) {
LOGGER.warn("The combination of useIntegratedVariance = true, useLogTime != useLogValue can produce very bad results, including considerable dips between "
+ "points at the same level (all other combinations give a flat line), and thus should be avoided.");
}
}
//TODO add new constructor pattern using builder to set options, as in EquityVarianceSwapPricer
public Function1D<Double, Double>[] getIndependentSmileFits(final SmileSurfaceDataBundle marketData) {
ArgumentChecker.notNull(marketData, "market data");
final int n = marketData.getNumExpiries();
final double[] forwards = marketData.getForwards();
final double[][] strikes = marketData.getStrikes();
final double[] expiries = marketData.getExpiries();
final double[][] vols = marketData.getVolatilities();
//fit each smile independently
@SuppressWarnings("unchecked")
final Function1D<Double, Double>[] smileFunctions = new Function1D[n];
for (int i = 0; i < n; i++) {
smileFunctions[i] = _smileInterpolator.getVolatilityFunction(forwards[i], strikes[i], expiries[i], vols[i]);
}
return smileFunctions;
}
/**
* For a given expiry and strike, perform an interpolation between either the variance (square of volatility) or integrated variances of points with the same proxy delta (defined as d =
* Math.log(forward / k) / Math.sqrt(t)) on the fitted smiles.
* <p>
* Each smile is fitted independently using the supplied GeneralSmileInterpolator (the default is SmileInterpolatorSABR), which produces a curve (the smile) that fits all the market implied
* volatilities and has sensible extrapolation behaviour.
* <p>
* The interpolation in the time direction uses the supplied interpolator (default is natural cubic spline) using the four nearest points. There is no guarantees of a monotonically increasing
* integrated variance (hence calendar arbitrage or negative local volatility are possible), but using log time to better space out the x-points helps.
*
* @param marketData The mark data - contains the forwards, expiries, and strikes and (market) implied volatilities at each expiry, not null
* @return Implied volatility surface parameterised by time and moneyness
*/
public BlackVolatilitySurfaceMoneynessFcnBackedByGrid getVolatilitySurface(final SmileSurfaceDataBundle marketData) {
ArgumentChecker.notNull(marketData, "market data");
final Function1D<Double, Double>[] smileFunctions = getIndependentSmileFits(marketData);
return combineIndependentSmileFits(smileFunctions, marketData);
}
/**
* Given a set of smiles in the moneyness dimension, produce surface function that additionally interpolates in expiry.
* <p>
* Access to the individual parts of getVolatilitySurface() permits user to bump vols without having to recalibrate each independent smile
*
* @param smileFunctions Array of Function1D's, one per expiry, that return volatility given strike
* @param marketData The mark data - contains the forwards, expiries, and strikes and (market) implied volatilities at each expiry, not null
* @return Implied volatility surface parameterised by time and moneyness
*/
public BlackVolatilitySurfaceMoneynessFcnBackedByGrid combineIndependentSmileFits(final Function1D<Double, Double>[] smileFunctions,
final SmileSurfaceDataBundle marketData) {
ArgumentChecker.notNull(marketData, "market data");
ArgumentChecker.isTrue(marketData.getNumExpiries() > 0, "Do not have market data for any expiry");
final int n = marketData.getNumExpiries();
final double[] forwards = marketData.getForwards();
final double[] expiries = marketData.getExpiries();
double[] temp = null;
if (_useLogTime) {
temp = new double[n];
for (int i = 0; i < n; i++) {
temp[i] = Math.log(expiries[i]);
}
} else {
temp = expiries;
}
final double[] xValues = temp;
final Function<Double, Double> surFunc = new Function<Double, Double>() {
@SuppressWarnings("synthetic-access")
@Override
public Double evaluate(final Double... tm) {
final double t = tm[0];
final double m = tm[1];
// Case 1: Only a single expiry is available
if (n == 1) {
return smileFunctions[0].evaluate(forwards[0] * m);
}
// Case 2 & 3: Extrapolation OR Less than 4 Expiries => Linear Extrapolation / Interpolation
// FIXME Casey 15-01-2015 Extrapolation is hardcoded, to Linear.Should take input from _timeInterpolator
// FIXME If n < 4, time interpolation is hardcoded, also to be linear.
final int index = SurfaceArrayUtils.getLowerBoundIndex(expiries, t);
if (index == 0 || index == (n - 1) || n < 4) {
int lowIdx;
if (index == 0) {
lowIdx = 0;
} else if (index == n - 1) {
lowIdx = n - 2;
} else {
lowIdx = index;
}
final double x = _useLogTime ? Math.log(t) : t;
final double k0 = forwards[lowIdx] * Math.pow(m, Math.sqrt(expiries[lowIdx] / t));
final double k1 = forwards[lowIdx + 1] * Math.pow(m, Math.sqrt(expiries[lowIdx + 1] / t));
double var0 = square(smileFunctions[lowIdx].evaluate(k0));
double var1 = square(smileFunctions[lowIdx + 1].evaluate(k1));
if (_useIntegratedVariance) {
var0 *= expiries[lowIdx];
var1 *= expiries[lowIdx + 1];
}
if (_useLogVar) {
var0 = Math.log(var0);
var1 = Math.log(var1);
}
final double dt = xValues[lowIdx + 1] - xValues[lowIdx];
double var = ((xValues[lowIdx + 1] - x) * var0 + (x - xValues[lowIdx]) * var1) / dt;
if (_useLogVar) {
var = Math.exp(var);
if (var < 0.0) {
var0 = Math.exp(var0);
var1 = Math.exp(var1);
}
}
if (var >= 0.0) {
return Math.sqrt(var / (_useIntegratedVariance ? t : 1.0));
}
return Math.sqrt(Math.min(var0, var1) / (_useIntegratedVariance ? t : 1.0));
}
// Case 4: Interpolation when n >= 4
//FIXME Time interpolator hard-coded to be a natural cubic spline when n > 3
int lower;
if (index == 0) {
lower = 0;
} else if (index == n - 2) {
lower = index - 2;
} else if (index == n - 1) {
lower = index - 3;
} else {
lower = index - 1;
}
final double[] xData = Arrays.copyOfRange(xValues, lower, lower + 4);
final double x = _useLogTime ? Math.log(t) : t;
final double[] yData = new double[4];
for (int i = 0; i < 4; i++) {
final double time = expiries[lower + i];
final double k = forwards[lower + i] * Math.pow(m, Math.sqrt(time / t));
double y = square(smileFunctions[lower + i].evaluate(k));
if (_useIntegratedVariance) {
y *= time;
}
yData[i] = _useLogVar ? Math.log(y) : y;
}
final Interpolator1DDataBundle db = _timeInterpolator.getDataBundle(xData, yData);
final double tRes = _timeInterpolator.interpolate(db, x);
final double yValue = _useLogVar ? Math.exp(tRes) : tRes;
final double res = Math.sqrt(yValue / (_useIntegratedVariance ? t : 1.0));
return res;
}
public Object writeReplace() {
return new InvokedSerializedForm(new InvokedSerializedForm(new InvokedSerializedForm(VolatilitySurfaceInterpolator.this, "getVolatilitySurface", marketData), "getSurface"), "getFunction");
}
};
return new BlackVolatilitySurfaceMoneynessFcnBackedByGrid(FunctionalDoublesSurface.from(surFunc), marketData.getForwardCurve(), marketData,
VolatilitySurfaceInterpolator.this) {
public Object writeReplace() {
return new InvokedSerializedForm(VolatilitySurfaceInterpolator.this, "getVolatilitySurface", marketData);
}
};
}
//TODO find a way of bumping a single point without recalibrating all unaffected smiles
public BlackVolatilitySurfaceMoneynessFcnBackedByGrid getBumpedVolatilitySurface(final SmileSurfaceDataBundle marketData, final int expiryIndex, final int strikeIndex,
final double amount) {
ArgumentChecker.notNull(marketData, "marketData");
final SmileSurfaceDataBundle bumpedData = marketData.withBumpedPoint(expiryIndex, strikeIndex, amount);
return getVolatilitySurface(bumpedData);
}
public boolean useLogTime() {
return _useLogTime;
}
public boolean useIntegratedVariance() {
return _useIntegratedVariance;
}
public boolean useLogValue() {
return _useLogVar;
}
public Interpolator1D getTimeInterpolator() {
return _timeInterpolator;
}
public GeneralSmileInterpolator getSmileInterpolator() {
return _smileInterpolator;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + _smileInterpolator.hashCode();
result = prime * result + _timeInterpolator.hashCode();
result = prime * result + (_useIntegratedVariance ? 1231 : 1237);
result = prime * result + (_useLogTime ? 1231 : 1237);
result = prime * result + (_useLogVar ? 1231 : 1237);
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 VolatilitySurfaceInterpolator other = (VolatilitySurfaceInterpolator) obj;
if (!ObjectUtils.equals(_smileInterpolator, other._smileInterpolator)) {
return false;
}
if (!ObjectUtils.equals(_timeInterpolator, other._timeInterpolator)) {
return false;
}
if (_useIntegratedVariance != other._useIntegratedVariance) {
return false;
}
if (_useLogTime != other._useLogTime) {
return false;
}
if (_useLogVar != other._useLogVar) {
return false;
}
return true;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("VolatilitySurfaceInterpolator[time interpolator=");
sb.append(_timeInterpolator.toString());
sb.append(", smile interpolator=");
sb.append(_smileInterpolator.toString());
sb.append(" using ");
sb.append(_useIntegratedVariance ? "integrated variance, " : " variance, ");
sb.append(_useLogTime ? " log time and " : " linear time and ");
sb.append(_useLogVar ? " log y" : " linear y");
sb.append("]");
return sb.toString();
}
}