/** * 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; import com.google.common.primitives.Doubles; import com.opengamma.analytics.math.MathException; import com.opengamma.analytics.math.function.Function1D; import com.opengamma.analytics.math.rootfinding.BisectionSingleRootFinder; import com.opengamma.analytics.math.rootfinding.BracketRoot; import com.opengamma.util.ArgumentChecker; /** * Finds an implied volatility (a parameter that put into a model gives the market pirce of an option) for any option pricing model that has a 'volatility' parameter. * This included the Black-Scholes-Merton model (and derivatives) for European options and Barone-Adesi & Whaley and Bjeksund and Stensland for American options */ public class GenericImpliedVolatiltySolver { private static final int MAX_ITERATIONS = 20; // something's wrong if Newton-Raphson taking longer than this private static final double VOL_TOL = 1e-9; // 1 part in 100,000 basis points will do for implied vol private static final double VOL_GUESS = 0.3; private static final double BRACKET_STEP = 0.1; private static final double MAX_CHANGE = 0.5; private final Function1D<Double, Double> _priceFunc; private final Function1D<Double, double[]> _priceAndVegaFunc; public GenericImpliedVolatiltySolver(Function1D<Double, double[]> priceAndVegaFunc) { ArgumentChecker.notNull(priceAndVegaFunc, "priceAndVegaFunc"); _priceAndVegaFunc = priceAndVegaFunc; _priceFunc = new Function1D<Double, Double>() { @Override public Double evaluate(Double sigma) { return _priceAndVegaFunc.evaluate(sigma)[0]; } }; } public GenericImpliedVolatiltySolver(final Function1D<Double, Double> priceFunc, final Function1D<Double, Double> vegaFunc) { ArgumentChecker.notNull(priceFunc, "priceFunc"); ArgumentChecker.notNull(vegaFunc, "vegaFunc"); _priceFunc = priceFunc; _priceAndVegaFunc = new Function1D<Double, double[]>() { @Override public double[] evaluate(Double sigma) { return new double[] {priceFunc.evaluate(sigma), vegaFunc.evaluate(sigma) }; } }; } public double impliedVolatility(double optionPrice) { return impliedVolatility(optionPrice, VOL_GUESS); } public double impliedVolatility(double optionPrice, double volGuess) { ArgumentChecker.isTrue(volGuess >= 0.0, "volGuess must be positive; have {}", volGuess); ArgumentChecker.isTrue(Doubles.isFinite(volGuess), "volGuess must be finite; have {} ", volGuess); double lowerSigma; double upperSigma; try { final double[] temp = bracketRoot(optionPrice, volGuess); lowerSigma = temp[0]; upperSigma = temp[1]; } catch (final MathException e) { throw new IllegalArgumentException(e.toString() + " No implied Volatility for this price. [price: " + optionPrice + "]"); } double sigma = (lowerSigma + upperSigma) / 2.0; double[] pnv = _priceAndVegaFunc.evaluate(sigma); // This can happen for American options, where low volatilities puts you in the early excise region which obviously has zero vega if (pnv[1] == 0 || Double.isNaN(pnv[1])) { return solveByBisection(optionPrice, lowerSigma, upperSigma); } double diff = pnv[0] - optionPrice; boolean above = diff > 0; if (above) { upperSigma = sigma; } else { lowerSigma = sigma; } double trialChange = -diff / pnv[1]; double actChange; if (trialChange > 0.0) { actChange = Math.min(MAX_CHANGE, Math.min(trialChange, upperSigma - sigma)); } else { actChange = Math.max(-MAX_CHANGE, Math.max(trialChange, lowerSigma - sigma)); } int count = 0; while (Math.abs(actChange) > VOL_TOL) { sigma += actChange; pnv = _priceAndVegaFunc.evaluate(sigma); if (pnv[1] == 0 || Double.isNaN(pnv[1])) { return solveByBisection(optionPrice, lowerSigma, upperSigma); } diff = pnv[0] - optionPrice; above = diff > 0; if (above) { upperSigma = sigma; } else { lowerSigma = sigma; } trialChange = -diff / pnv[1]; if (trialChange > 0.0) { actChange = Math.min(MAX_CHANGE, Math.min(trialChange, upperSigma - sigma)); } else { actChange = Math.max(-MAX_CHANGE, Math.max(trialChange, lowerSigma - sigma)); } if (count++ > MAX_ITERATIONS) { return solveByBisection(optionPrice, lowerSigma, upperSigma); } } return sigma + actChange; // apply the final change } private double[] bracketRoot(final double optionPrice, final double sigma) { final BracketRoot bracketer = new BracketRoot(); final Function1D<Double, Double> func = new Function1D<Double, Double>() { @Override public Double evaluate(final Double volatility) { return _priceFunc.evaluate(volatility) / optionPrice - 1.0; } }; return bracketer.getBracketedPoints(func, Math.max(0.0, sigma - BRACKET_STEP), sigma + BRACKET_STEP, 0.0, Double.POSITIVE_INFINITY); } private double solveByBisection(final double optionPrice, final double lowerSigma, final double upperSigma) { final BisectionSingleRootFinder rootFinder = new BisectionSingleRootFinder(VOL_TOL); final Function1D<Double, Double> func = new Function1D<Double, Double>() { @Override public Double evaluate(final Double volatility) { double trialPrice = _priceFunc.evaluate(volatility); return trialPrice / optionPrice - 1.0; } }; return rootFinder.getRoot(func, lowerSigma, upperSigma); } }