com.opengamma.strata.pricer.impl.option.GenericImpliedVolatiltySolver Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2012 - present by OpenGamma Inc. and the OpenGamma group of companies
*
* Please see distribution for license.
*/
package com.opengamma.strata.pricer.impl.option;
import java.util.function.Function;
import com.google.common.primitives.Doubles;
import com.opengamma.strata.collect.ArgChecker;
import com.opengamma.strata.math.MathException;
import com.opengamma.strata.math.impl.rootfinding.BisectionSingleRootFinder;
import com.opengamma.strata.math.impl.rootfinding.BracketRoot;
/**
* 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;
/**
* The price function.
*/
private final Function priceFunc;
/**
* The combined price and vega function.
*/
private final Function priceAndVegaFunc;
/**
* Creates an instance.
*
* @param priceAndVegaFunc the combined price and vega function
*/
public GenericImpliedVolatiltySolver(Function priceAndVegaFunc) {
ArgChecker.notNull(priceAndVegaFunc, "priceAndVegaFunc");
this.priceAndVegaFunc = priceAndVegaFunc;
this.priceFunc = new Function() {
@Override
public Double apply(Double sigma) {
return priceAndVegaFunc.apply(sigma)[0];
}
};
}
/**
* Creates an instance.
*
* @param priceFunc the pricing function
* @param vegaFunc the vega function
*/
public GenericImpliedVolatiltySolver(Function priceFunc, Function vegaFunc) {
ArgChecker.notNull(priceFunc, "priceFunc");
ArgChecker.notNull(vegaFunc, "vegaFunc");
this.priceFunc = priceFunc;
this.priceAndVegaFunc = new Function() {
@Override
public double[] apply(Double sigma) {
return new double[] {priceFunc.apply(sigma), vegaFunc.apply(sigma)};
}
};
}
//-------------------------------------------------------------------------
/**
* Computes the implied volatility.
*
* @param optionPrice the option price
* @return the volatility
*/
public double impliedVolatility(double optionPrice) {
return impliedVolatility(optionPrice, VOL_GUESS);
}
/**
* Computes the implied volatility.
*
* @param optionPrice the option price
* @param volGuess the initial guess
* @return the volatility
*/
public double impliedVolatility(double optionPrice, double volGuess) {
ArgChecker.isTrue(volGuess >= 0.0, "volGuess must be positive; have {}", volGuess);
ArgChecker.isTrue(Doubles.isFinite(volGuess), "volGuess must be finite; have {} ", volGuess);
double lowerSigma;
double upperSigma;
try {
double[] temp = bracketRoot(optionPrice, volGuess);
lowerSigma = temp[0];
upperSigma = temp[1];
} catch (MathException e) {
throw new IllegalArgumentException(e.toString() + " No implied Volatility for this price. [price: " + optionPrice + "]");
}
double sigma = (lowerSigma + upperSigma) / 2.0;
double[] pnv = priceAndVegaFunc.apply(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.apply(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(double optionPrice, double sigma) {
BracketRoot bracketer = new BracketRoot();
Function func = new Function() {
@Override
public Double apply(Double volatility) {
return priceFunc.apply(volatility) / optionPrice - 1.0;
}
};
return bracketer.getBracketedPoints(
func,
Math.max(0.0, sigma - BRACKET_STEP),
sigma + BRACKET_STEP,
0d,
Double.POSITIVE_INFINITY);
}
private double solveByBisection(double optionPrice, double lowerSigma, double upperSigma) {
BisectionSingleRootFinder rootFinder = new BisectionSingleRootFinder(VOL_TOL);
Function func = new Function() {
@Override
public Double apply(Double volatility) {
double trialPrice = priceFunc.apply(volatility);
return trialPrice / optionPrice - 1.0;
}
};
return rootFinder.getRoot(func, lowerSigma, upperSigma);
}
}