/** * 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.local; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.opengamma.analytics.financial.model.interestrate.curve.ForwardCurve; import com.opengamma.analytics.financial.model.volatility.BlackFormulaRepository; import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurface; import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceMoneyness; import com.opengamma.analytics.financial.model.volatility.surface.BlackVolatilitySurfaceStrike; import com.opengamma.analytics.financial.model.volatility.surface.PriceSurface; import com.opengamma.analytics.financial.model.volatility.surface.PureImpliedVolatilitySurface; import com.opengamma.analytics.financial.model.volatility.surface.StrikeType; import com.opengamma.analytics.math.function.Function; import com.opengamma.analytics.math.surface.FunctionalDoublesSurface; import com.opengamma.analytics.math.surface.Surface; import com.opengamma.analytics.util.serialization.InvokedSerializedForm; import com.opengamma.util.ArgumentChecker; /** * */ public class DupireLocalVolatilityCalculator { private static final Logger s_logger = LoggerFactory.getLogger(DupireLocalVolatilityCalculator.class); private final double _eps; public DupireLocalVolatilityCalculator() { _eps = 1e-3; } public DupireLocalVolatilityCalculator(final double h) { _eps = h; } /** * Classic Dupire local volatility formula * * @param priceSurface present value (i.e. discounted) value of options on underlying at various expiries and strikes * @param spot The current value of the underlying * @param r The risk free rate (or domestic rate in FX) * @param q The dividend yield (or foreign rate in FX) * @return The local volatility surface */ public LocalVolatilitySurfaceStrike getLocalVolatility(final PriceSurface priceSurface, final double spot, final double r, final double q) { final Function<Double, Double> locVol = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... x) { final double t = x[0]; final double k = x[1]; final double price = priceSurface.getPrice(t, k); final double divT = getFirstTimeDev(priceSurface.getSurface(), t, k, price); final double divK = getFirstStrikeDev(priceSurface.getSurface(), t, k, price, spot); final double divK2 = getSecondStrikeDev(priceSurface.getSurface(), t, k, price, spot); final double var = 2. * (divT + q * price + (r - q) * k * divK) / (k * k * divK2); if (var < 0) { s_logger.info("Negative variance; returning 0"); return 0.; } return Math.sqrt(var); } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", priceSurface, spot, r, q), "getSurface"), "getFunction"); } }; return new LocalVolatilitySurfaceStrike(FunctionalDoublesSurface.from(locVol)) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", priceSurface, spot, r, q); } }; } /** * REVIEW if we need this Get the absolute (i.e. normal instantaneous) local vol surface * * @param impliedVolatilitySurface BlackVolatilitySurface * @param spot value of underlying * @param rate interest rate * @return local vol surface */ public AbsoluteLocalVolatilitySurface getAbsoluteLocalVolatilitySurface(final BlackVolatilitySurfaceStrike impliedVolatilitySurface, final double spot, final double rate) { final Function<Double, Double> locVol = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... x) { final double t = x[0]; final double s = x[1]; final double vol = impliedVolatilitySurface.getVolatility(t, s); final double divT = getFirstTimeDev(impliedVolatilitySurface.getSurface(), t, s, vol); double var; if (s == 0) { var = 0.0; } else { final double divK = getFirstStrikeDev(impliedVolatilitySurface.getSurface(), t, s, vol, spot); final double divK2 = getSecondStrikeDev(impliedVolatilitySurface.getSurface(), t, s, vol, spot); final double h1 = (Math.log(spot / s) + (rate + vol * vol / 2) * t) / vol; final double h2 = h1 - vol * t; var = (vol * vol + 2 * vol * t * (divT + rate * s * divK)) / (1 + 2 * h1 * s * divK + s * s * (h1 * h2 * divK * divK + t * vol * divK2)); if (var < 0.0) { s_logger.info("negative variance; returning 0"); return 0.; } } return s * Math.sqrt(var); } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getAbsoluteLocalVolatilitSurface", impliedVolatilitySurface, spot, rate), "getSurface"), "getFunction"); } }; return new AbsoluteLocalVolatilitySurface(FunctionalDoublesSurface.from(locVol)) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getAbsoluteLocalVolatilitSurface", impliedVolatilitySurface, spot, rate); } }; } /** * Classic Dupire local volatility formula in terms of the Black Volatility surface (parameterised by strike) * * @param impliedVolatilitySurface Black Volatility surface (parameterised by strike) * @param spot Level of underlying * @param drift The risk free rate minus The dividend yield (r-q), or the difference between the domestic and foreign risk free rates in FX * @return A Local Volatility surface parameterised by expiry and strike * @deprecated Don't use */ @Deprecated public LocalVolatilitySurfaceStrike getLocalVolatility(final BlackVolatilitySurfaceStrike impliedVolatilitySurface, final double spot, final double drift) { final Function<Double, Double> locVol = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... x) { final double t = x[0]; final double s = x[1]; final double vol = impliedVolatilitySurface.getVolatility(t, s); final double divT = getFirstTimeDev(impliedVolatilitySurface.getSurface(), t, s, vol); double var; if (s == 0) { var = vol * vol + 2 * vol * t * (divT); } else { final double divK = getFirstStrikeDev(impliedVolatilitySurface.getSurface(), t, s, vol, spot); final double divK2 = getSecondStrikeDev(impliedVolatilitySurface.getSurface(), t, s, vol, spot); final double h1 = (Math.log(spot / s) + (drift + vol * vol / 2) * t) / vol; final double h2 = h1 - vol * t; var = (vol * vol + 2 * vol * t * (divT + drift * s * divK)) / (1 + 2 * h1 * s * divK + s * s * (h1 * h2 * divK * divK + t * vol * divK2)); if (var < 0.0) { s_logger.error("Negative variance; returning 0"); var = 0.0; } } return Math.sqrt(var); } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", impliedVolatilitySurface, spot, drift), "getSurface"), "getFunction"); } }; return new LocalVolatilitySurfaceStrike(FunctionalDoublesSurface.from(locVol)) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", impliedVolatilitySurface, spot, drift); } }; } //TODO replace this public <T extends StrikeType> LocalVolatilitySurface<?> getLocalVolatilitySurface(final BlackVolatilitySurface<T> impliedVolatilitySurface, final ForwardCurve forwardCurve) { if (impliedVolatilitySurface instanceof BlackVolatilitySurfaceStrike) { return getLocalVolatility((BlackVolatilitySurfaceStrike) impliedVolatilitySurface, forwardCurve); } else if (impliedVolatilitySurface instanceof BlackVolatilitySurfaceMoneyness) { return getLocalVolatility((BlackVolatilitySurfaceMoneyness) impliedVolatilitySurface); } throw new IllegalArgumentException(); } /** * Get the local volatility in the case where the option price is a function of the forward price * * @param impliedVolatilitySurface The Black implied volatility surface * @param forwardCurve Curve of forward prices * @return The local volatility */ public LocalVolatilitySurfaceStrike getLocalVolatility(final BlackVolatilitySurfaceStrike impliedVolatilitySurface, final ForwardCurve forwardCurve) { final Function<Double, Double> locVol = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... tk) { final double t = tk[0]; final double k = tk[1]; final double forward = forwardCurve.getForward(t); final double drift = forwardCurve.getDrift(t); final double vol = impliedVolatilitySurface.getVolatility(t, k); final double divT = getFirstTimeDev(impliedVolatilitySurface.getSurface(), t, k, vol); double var; if (k == 0) { var = vol * vol + 2 * vol * t * (divT); } else { final double divK = getFirstStrikeDev(impliedVolatilitySurface.getSurface(), t, k, vol, forward); final double divK2 = getSecondStrikeDev(impliedVolatilitySurface.getSurface(), t, k, vol, forward); final double h1 = (Math.log(forward / k) + (vol * vol / 2) * t) / vol; final double h2 = h1 - vol * t; var = (vol * vol + 2 * vol * t * (divT + k * drift * divK)) / (1 + 2 * h1 * k * divK + k * k * (h1 * h2 * divK * divK + t * vol * divK2)); if (var < 0.0) { s_logger.info("Negative variance; returning 0"); var = 0.0; } } return Math.sqrt(var); } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", impliedVolatilitySurface, forwardCurve), "getSurface"), "getFunction"); } }; return new LocalVolatilitySurfaceStrike(FunctionalDoublesSurface.from(locVol)) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", impliedVolatilitySurface, forwardCurve); } }; } /** * Get the <b>pure</b> local volatility surface (i.e. if the pure stock $x$ follows the SDE $\frac{dx}{x} = \sigma(t,x) dW$ then $\sigma(t,x)$ is the pure local volatility * <p> * See White, R (2012) Equity Variance Swap with Dividends * * @param pureImpliedVolatilitySurface The pure implied volatility surface - i.e. the volatility that put into the Black formula will give the price of an option on the pure stock * @return pure local volatility surface */ public PureLocalVolatilitySurface getLocalVolatility(final PureImpliedVolatilitySurface pureImpliedVolatilitySurface) { final Surface<Double, Double, Double> lv = getLocalVolatility(pureImpliedVolatilitySurface.getSurface()); return new PureLocalVolatilitySurface(lv); } /** * Get the local volatility surface (parameterised by expiry and moneyness = strike/forward) from a Black volatility surface (also parameterised by expiry and moneyness). <b>Note</b> this is the * cleanest method as is does not require any knowledge of instantaneous rates (i.e. r & q). If the Black volatility surface is parameterised by strike and/or the local volatility surface is * required to be parameterised by strike use can use the converters BlackVolatilitySurfaceConverter and/or LocalVolatilitySurfaceConverter * * @param impliedVolatilitySurface Black volatility surface (parameterised by expiry and moneyness) * @return local volatility surface (parameterised by expiry and moneyness) */ public LocalVolatilitySurfaceMoneyness getLocalVolatility(final BlackVolatilitySurfaceMoneyness impliedVolatilitySurface) { final Surface<Double, Double, Double> lv = getLocalVolatility(impliedVolatilitySurface.getSurface()); return new LocalVolatilitySurfaceMoneyness(lv, impliedVolatilitySurface.getForwardCurve()); } public LocalVolatilitySurfaceMoneyness getLocalVolatilityDebug(final BlackVolatilitySurfaceMoneyness impliedVolatilitySurface) { final Function<Double, Double> locVol = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... tm) { final double t = tm[0]; final double m = tm[1]; ArgumentChecker.isTrue(t >= 0, "negative t"); ArgumentChecker.isTrue(m >= 0, "negative m"); final double vol = impliedVolatilitySurface.getVolatilityForMoneyness(t, m); final double divT = getFirstTimeDev(impliedVolatilitySurface.getSurface(), t, m, vol); final double divM = getFirstStrikeDev(impliedVolatilitySurface.getSurface(), t, m, vol, 1.0); final double divM2 = getSecondStrikeDev(impliedVolatilitySurface.getSurface(), t, m, vol, 1.0); final double bTheta = -BlackFormulaRepository.driftlessTheta(1.0, m, t, vol); final double vega = BlackFormulaRepository.vega(1.0, m, t, vol); final double theta = bTheta + vega * divT; final double dg = BlackFormulaRepository.dualGamma(1.0, m, t, vol); final double vanna = BlackFormulaRepository.dualVanna(1.0, m, t, vol); final double vomma = BlackFormulaRepository.vomma(1.0, m, t, vol); final double dens = dg + 2 * vanna * divM + +vomma * divM * divM + vega * divM2; return Math.sqrt(2 * theta / dens) / m; } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatilityDebug", impliedVolatilitySurface), "getSurface"), "getFunction"); } }; return new LocalVolatilitySurfaceMoneyness(FunctionalDoublesSurface.from(locVol), impliedVolatilitySurface.getForwardCurve()) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatilityDebug", impliedVolatilitySurface); } }; } /** * Get the theta surface - the rate of change of an option with respect to the time-to-expiry (<b>Note</b> this is the negative of the normal definition as change of an option with respect to * (calendar) time) * * @param impliedVolatilitySurface Black volatility surface (parameterised by expiry and moneyness) * @return Theta surface (parameterised by moneyness) */ public Surface<Double, Double, Double> getTheta(final BlackVolatilitySurfaceMoneyness impliedVolatilitySurface) { final Function<Double, Double> theta = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... tm) { final double t = tm[0]; final double m = tm[1]; ArgumentChecker.isTrue(t >= 0, "negative t"); ArgumentChecker.isTrue(m >= 0, "negative m"); final double vol = impliedVolatilitySurface.getVolatilityForMoneyness(t, m); final double divT = getFirstTimeDev(impliedVolatilitySurface.getSurface(), t, m, vol); final double bTheta = -BlackFormulaRepository.driftlessTheta(1.0, m, t, vol); final double vega = BlackFormulaRepository.vega(1.0, m, t, vol); return bTheta + vega * divT; } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getTheta", impliedVolatilitySurface), "getFunction"); } }; return new FunctionalDoublesSurface(theta) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getTheta", impliedVolatilitySurface); } }; } /** * Get the transition density surface - each time slice through this surface is the Probably Density Function (PDF), in the risk neutral measure, for the underlying at that time (parameterised by * moneyness) * * @param impliedVolatilitySurface Black volatility surface (parameterised by expiry and moneyness) * @return The transition density surface */ public Surface<Double, Double, Double> getDensity(final BlackVolatilitySurfaceMoneyness impliedVolatilitySurface) { final Function<Double, Double> density = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... tm) { final double t = tm[0]; final double m = tm[1]; ArgumentChecker.isTrue(t >= 0, "negative t"); ArgumentChecker.isTrue(m >= 0, "negative m"); final double vol = impliedVolatilitySurface.getVolatilityForMoneyness(t, m); final double divM = getFirstStrikeDev(impliedVolatilitySurface.getSurface(), t, m, vol, 1.0); final double divM2 = getSecondStrikeDev(impliedVolatilitySurface.getSurface(), t, m, vol, 1.0); final double dg = BlackFormulaRepository.dualGamma(1.0, m, t, vol); final double vanna = BlackFormulaRepository.dualVanna(1.0, m, t, vol); final double vega = BlackFormulaRepository.vega(1.0, m, t, vol); final double vomma = BlackFormulaRepository.vomma(1.0, m, t, vol); final double dens = dg + 2 * vanna * divM + +vomma * divM * divM + vega * divM2; return dens; } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getDensity", impliedVolatilitySurface), "getFunction"); } }; return new FunctionalDoublesSurface(density) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getDensity", impliedVolatilitySurface); } }; } public Surface<Double, Double, Double> getLocalVolatility(final Surface<Double, Double, Double> surf) { final Function<Double, Double> locVol = new Function<Double, Double>() { @SuppressWarnings("synthetic-access") @Override public Double evaluate(final Double... tm) { final double t = tm[0]; final double m = tm[1]; ArgumentChecker.isTrue(t >= 0, "negative t"); ArgumentChecker.isTrue(m >= 0, "negative m"); final double vol = surf.getZValue(t, m); final double divT = getFirstTimeDev(surf, t, m, vol); double var; if (m == 0) { var = vol * vol + 2 * vol * t * (divT); } else { final double divM = getFirstStrikeDev(surf, t, m, vol, 1.0); final double divM2 = getSecondStrikeDev(surf, t, m, vol, 1.0); final double h1 = (-Math.log(m) + (vol * vol / 2) * t) / vol; final double h2 = h1 - vol * t; var = (vol * vol + 2 * vol * t * divT) / (1 + 2 * h1 * m * divM + m * m * (h1 * h2 * divM * divM + t * vol * divM2)); if (var < 0.0) { s_logger.info("Negative variance; returning 0"); var = 0.0; } } return Math.sqrt(var); } public Object writeReplace() { return new InvokedSerializedForm(new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", surf), "getFunction"); } }; return new FunctionalDoublesSurface(locVol) { public Object writeReplace() { return new InvokedSerializedForm(DupireLocalVolatilityCalculator.this, "getLocalVolatility", surf); } }; } private double getFirstTimeDev(final Surface<Double, Double, Double> surface, final double t, final double k, final double mid) { if (t <= _eps) { final double up = surface.getZValue(t + _eps, k); return (up - mid) / _eps; } final double up = surface.getZValue(t + _eps, k); final double down = surface.getZValue(t - _eps, k); return (up - down) / 2. / _eps; } private double getFirstStrikeDev(final Surface<Double, Double, Double> surface, final double t, final double k, final double mid, final double spot) { final double eps = spot * _eps; if (k <= eps) { final double up = surface.getZValue(t, k + eps); return (up - mid) / eps; } final double up = surface.getZValue(t, k + eps); final double down = surface.getZValue(t, k - eps); return (up - down) / 2. / eps; } private double getSecondStrikeDev(final Surface<Double, Double, Double> surface, final double t, final double k, final double mid, final double spot) { final double eps = spot * _eps; double offset = 0; double cent; if (k <= eps) { offset = eps - k; cent = surface.getZValue(t, k + offset); } else { cent = mid; } final double up = surface.getZValue(t, k + eps + offset); final double down = surface.getZValue(t, k - eps + offset); final double res = (up + down - 2 * cent) / eps / eps; return res; } }