/** * Copyright (C) 2011 - present by OpenGamma Inc. and the OpenGamma group of companies * * Please see distribution for license. */ package com.opengamma.analytics.financial.equity.variance.pricing; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.opengamma.OpenGammaRuntimeException; import com.opengamma.analytics.financial.model.volatility.BlackFormulaRepository; import com.opengamma.analytics.financial.model.volatility.smile.fitting.interpolation.ShiftedLogNormalTailExtrapolation; import com.opengamma.analytics.math.function.Function1D; import com.opengamma.analytics.math.matrix.DoubleMatrix1D; import com.opengamma.analytics.math.minimization.ParameterLimitsTransform; import com.opengamma.analytics.math.minimization.ParameterLimitsTransform.LimitType; import com.opengamma.analytics.math.minimization.SingleRangeLimitTransform; import com.opengamma.analytics.math.rootfinding.VectorRootFinder; import com.opengamma.analytics.math.rootfinding.newton.BroydenVectorRootFinder; import com.opengamma.util.ArgumentChecker; import com.opengamma.util.CompareUtils; /** * This is a model where the SDE for the forward are $\frac{df}{f+\alpha}=\sigma_{\alpha} dW$, that is, the forward, $f$, plus some displacement, $\alpha$, follow a geometric * Brownian motion (GBM). European options can be priced using the Black formula with forward $f \rightarrow f +\alpha$ and strike $k \rightarrow k + \alpha$ <p> * <b> This should not be confused with Shifted Log-Normal</b> (see {@link ShiftedLogNormalTailExtrapolation}) */ public class DisplacedDiffusionModel { /** A logger */ private static final Logger s_logger = LoggerFactory.getLogger(DisplacedDiffusionModel.class); //TODO none of these next fields should be stored in this class /** The forward */ private double _forward; /** The expiry */ private double _expiry; /** The volatility */ private double _vol; /** The shift */ private double _shift; /** The default tolerance */ private static final double DEF_TOL = 1.0E-6; /** The default number of steps */ private static final int DEF_STEPS = 10000; /** The default initial volatility */ private static final double DEF_GUESS_VOL = 0.20; /** The default fraction of a forward */ private static final double DEF_GUESS_SHIFT = 0.1; /** The default root-finder */ private static final VectorRootFinder DEF_SOLVER = new BroydenVectorRootFinder(DEF_TOL, DEF_TOL, DEF_STEPS); /** A transform to remove sigma > 0 as a constraint */ private static final ParameterLimitsTransform TRANSFORM = new SingleRangeLimitTransform(0, LimitType.GREATER_THAN); /** * Build a shifted lognormal volatility model directly from model inputs * @param forward absolute level of the forward * @param expiry expiry in years * @param lognormalVol annual lognormal (black) vol * @param shift absolute level of the shift applied to the forward and strike. A positive value shifts distribution left. */ public DisplacedDiffusionModel(final double forward, final double expiry, final double lognormalVol, final double shift) { _forward = forward; _expiry = expiry; _vol = lognormalVol; _shift = shift; } /** * Fit a shifted lognormal volatility model to two target points at one expiry. * @param forward absolute level of the forward * @param expiry expiry in years * @param targetStrike1 absolute level of the first target strike * @param targetVol1 lognormal volatility at the first target strike * @param targetStrike2 absolute level of the second target strike * @param targetVol2 lognormal volatility at the second target strike * @param volGuess initial guess for the model's annual lognormal volatility * @param shiftGuess initial guess for the model's shift, as absolute level * @param solver VectorRootFinder */ public DisplacedDiffusionModel(final double forward, final double expiry, final double targetStrike1, final double targetVol1, final double targetStrike2, final double targetVol2, final double volGuess, final double shiftGuess, final VectorRootFinder solver) { _forward = forward; _expiry = expiry; // Find target volatilities final DoubleMatrix1D volShift = fitShiftedLnParams(targetStrike1, targetVol1, targetStrike2, targetVol2, volGuess, shiftGuess * _forward, solver); _vol = volShift.getEntry(0); _shift = volShift.getEntry(1); } /** * Fit a Shifted Lognormal Volatility to two target points at one expiry * @param forward absolute level of the forward * @param expiry expiry in years * @param targetStrike1 absolute level of the first target strike * @param targetVol1 lognormal vol at the first target strike * @param targetStrike2 absolute level of the second target strike * @param targetVol2 lognormal vol at the second target strike */ public DisplacedDiffusionModel(final double forward, final double expiry, final double targetStrike1, final double targetVol1, final double targetStrike2, final double targetVol2) { this(forward, expiry, targetStrike1, targetVol1, targetStrike2, targetVol2, DEF_GUESS_VOL, DEF_GUESS_SHIFT, DEF_SOLVER); } /** * Fit a Shifted Lognormal Volatility to two target points at one expiry * @param forward absolute level of the forward * @param expiry expiry in years * @param targetStrike1 absolute level of the first target strike * @param targetVol1 lognormal vol at the first target strike * @param targetStrike2 absolute level of the second target strike * @param targetVol2 lognormal vol at the second target strike * @return a displaced diffusion model */ public DisplacedDiffusionModel from(final double forward, final double expiry, final double targetStrike1, final double targetVol1, final double targetStrike2, final double targetVol2) { return new DisplacedDiffusionModel(forward, expiry, targetStrike1, targetVol1, targetStrike2, targetVol2, DEF_GUESS_VOL, DEF_GUESS_SHIFT, DEF_SOLVER); } private DoubleMatrix1D fitShiftedLnParams(final double strikeTarget1, final double volTarget1, final double strikeTarget2, final double volTarget2, final double volGuess, final double shiftGuess, final VectorRootFinder solver) { ArgumentChecker.notNull(solver, "solver"); DoubleMatrix1D volShiftParams; // [transform(vol),shift] final DoubleMatrix1D guess = new DoubleMatrix1D(new double[] {TRANSFORM.transform(volGuess), shiftGuess }); // Targets final double target1Price = BlackFormulaRepository.price(_forward, strikeTarget1, _expiry, volTarget1, strikeTarget1 > _forward); final double target2Price = BlackFormulaRepository.price(_forward, strikeTarget2, _expiry, volTarget2, strikeTarget2 > _forward); // Handle trivial case 1: Same Vol ==> 0.0 shift if (CompareUtils.closeEquals(volTarget1, volTarget2, DEF_TOL)) { return new DoubleMatrix1D(new double[] {volTarget1, 0.0 }); } // Objective function - hit the two vol targets final Function1D<DoubleMatrix1D, DoubleMatrix1D> priceDiffs = new Function1D<DoubleMatrix1D, DoubleMatrix1D>() { @SuppressWarnings("synthetic-access") @Override public DoubleMatrix1D evaluate(final DoubleMatrix1D volShiftPair) { final double[] diffs = new double[] {100, 100 }; final double vol = TRANSFORM.inverseTransform(volShiftPair.getEntry(0)); // Math.max(1e-9, volShiftPair.getEntry(0)); final double shift = volShiftPair.getEntry(1); diffs[0] = (target1Price - BlackFormulaRepository.price(_forward + shift, strikeTarget1 + shift, _expiry, vol, strikeTarget1 > _forward)) * 1e6; diffs[1] = (target2Price - BlackFormulaRepository.price(_forward + shift, strikeTarget2 + shift, _expiry, vol, strikeTarget2 > _forward)) * 1e6; return new DoubleMatrix1D(diffs); } }; try { volShiftParams = solver.getRoot(priceDiffs, guess); } catch (final Exception e) { // Failed on first solver attempt. Doing a second try { volShiftParams = solver.getRoot(priceDiffs, new DoubleMatrix1D(new double[] {TRANSFORM.transform(volTarget2), 0.0 })); } catch (final Exception e2) { s_logger.error("Failed to find roots to fit a Shifted Lognormal Distribution to your targets. Increase maxSteps, change guess, or change secondTarget."); s_logger.error("K1 = " + strikeTarget1 + ",vol1 = " + volTarget1 + ",price1 = " + target1Price); s_logger.error("K2 = " + strikeTarget2 + ",vol2 = " + volTarget2 + ",price2 = " + target2Price); throw new OpenGammaRuntimeException(e.getMessage()); } } return new DoubleMatrix1D(new double[] {TRANSFORM.inverseTransform(volShiftParams.getEntry(0)), volShiftParams.getEntry(1) }); } /** * @param absoluteStrike The absolute strike * @return Price of the calibrated model given a fixed (absolute) strike. So if the forward, was 80, an OTM Put might have a strike of 65. */ public double priceFromFixedStrike(final double absoluteStrike) { return BlackFormulaRepository.price(_forward + _shift, absoluteStrike + _shift, _expiry, _vol, absoluteStrike > _forward); } /** * Gets the forward. * @return the forward */ public final double getForward() { return _forward; } /** * Sets the forward. * @param forward the forward */ public final void setForward(final double forward) { _forward = forward; } /** * Gets the expiry. * @return the expiry */ public final double getExpiry() { return _expiry; } /** * Sets the expiry. * @param expiry the expiry */ public final void setExpiry(final double expiry) { _expiry = expiry; } /** * Gets the vol. * @return the vol */ public final double getVol() { return _vol; } /** * Sets the vol. * @param vol the vol */ public final void setVol(final double vol) { _vol = vol; } /** * Gets the shift. * @return the shift */ public final double getShift() { return _shift; } /** * Sets the shift. * @param shift the shift */ public final void setShift(final double shift) { _shift = shift; } @Override public int hashCode() { final int prime = 31; int result = 1; long temp; temp = Double.doubleToLongBits(_expiry); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(_forward); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(_shift); result = prime * result + (int) (temp ^ (temp >>> 32)); temp = Double.doubleToLongBits(_vol); result = prime * result + (int) (temp ^ (temp >>> 32)); return result; } @Override public boolean equals(final Object obj) { if (this == obj) { return true; } if (!(obj instanceof DisplacedDiffusionModel)) { return false; } final DisplacedDiffusionModel other = (DisplacedDiffusionModel) obj; if (Double.doubleToLongBits(_expiry) != Double.doubleToLongBits(other._expiry)) { return false; } if (Double.doubleToLongBits(_forward) != Double.doubleToLongBits(other._forward)) { return false; } if (Double.doubleToLongBits(_shift) != Double.doubleToLongBits(other._shift)) { return false; } if (Double.doubleToLongBits(_vol) != Double.doubleToLongBits(other._vol)) { return false; } return true; } }