/**
* Implied volatility solver using Newton-Raphson with bisection fallback
*/
import { BlackScholesParams, OptionType } from "./types";
import { priceBlackScholes } from "./blackScholes";
import { normalPDF } from "./utils";
const DEFAULT_MAX_ITERATIONS = 100;
const DEFAULT_TOLERANCE = 1e-6;
const VOL_LOWER_BOUND = 0.001;
const VOL_UPPER_BOUND = 5.0;
const DEFAULT_INITIAL_VOL = 0.2;
/**
* Compute vega for Newton-Raphson (derivative of price w.r.t. vol)
*/
function computeVega(
spot: number,
strike: number,
rate: number,
vol: number,
timeToMaturity: number,
dividendYield: number
): number {
if (vol <= 0 || timeToMaturity <= 0) return 0;
const sqrtT = Math.sqrt(timeToMaturity);
const d1 =
(Math.log(spot / strike) +
(rate - dividendYield + 0.5 * vol * vol) * timeToMaturity) /
(vol * sqrtT);
const dfDiv = Math.exp(-dividendYield * timeToMaturity);
return spot * dfDiv * sqrtT * normalPDF(d1);
}
export interface ImpliedVolOptions {
maxIterations?: number;
tolerance?: number;
initialGuess?: number;
}
/**
* Compute implied volatility from an observed option price
*
* Uses Newton-Raphson with bisection fallback for robustness.
*
* @param observedPrice - The market price of the option
* @param params - Black-Scholes parameters (excluding vol)
* @param options - Solver options
* @returns The implied volatility
* @throws Error if solver fails to converge
*/
export function impliedVol(
observedPrice: number,
params: Omit<BlackScholesParams, "vol">,
options: ImpliedVolOptions = {}
): number {
const {
spot,
strike,
rate,
timeToMaturity,
dividendYield = 0,
optionType,
} = params;
const {
maxIterations = DEFAULT_MAX_ITERATIONS,
tolerance = DEFAULT_TOLERANCE,
initialGuess = DEFAULT_INITIAL_VOL,
} = options;
// Validate inputs
if (observedPrice < 0) {
throw new Error("observedPrice cannot be negative");
}
if (timeToMaturity <= 0) {
// At expiry, price is intrinsic value
const intrinsic =
optionType === "call"
? Math.max(spot - strike, 0)
: Math.max(strike - spot, 0);
if (Math.abs(observedPrice - intrinsic) < tolerance) {
return 0; // Any vol is consistent at expiry
}
throw new Error("At expiry, price must equal intrinsic value");
}
// Check for arbitrage bounds
const df = Math.exp(-rate * timeToMaturity);
const dfDiv = Math.exp(-dividendYield * timeToMaturity);
if (optionType === "call") {
const lowerBound = Math.max(spot * dfDiv - strike * df, 0);
const upperBound = spot * dfDiv;
if (observedPrice < lowerBound - tolerance) {
throw new Error(
`Price ${observedPrice} is below intrinsic value ${lowerBound}`
);
}
if (observedPrice > upperBound + tolerance) {
throw new Error(
`Price ${observedPrice} exceeds theoretical maximum ${upperBound}`
);
}
} else {
const lowerBound = Math.max(strike * df - spot * dfDiv, 0);
const upperBound = strike * df;
if (observedPrice < lowerBound - tolerance) {
throw new Error(
`Price ${observedPrice} is below intrinsic value ${lowerBound}`
);
}
if (observedPrice > upperBound + tolerance) {
throw new Error(
`Price ${observedPrice} exceeds theoretical maximum ${upperBound}`
);
}
}
// Try Newton-Raphson first
let vol = initialGuess;
let converged = false;
for (let i = 0; i < maxIterations; i++) {
const result = priceBlackScholes({ ...params, vol });
const price = result.price;
const diff = price - observedPrice;
if (Math.abs(diff) < tolerance) {
converged = true;
break;
}
const vega = computeVega(
spot,
strike,
rate,
vol,
timeToMaturity,
dividendYield
);
if (vega < 1e-10) {
// Vega too small, switch to bisection
break;
}
const newVol = vol - diff / vega;
// Ensure vol stays within bounds
vol = Math.max(VOL_LOWER_BOUND, Math.min(VOL_UPPER_BOUND, newVol));
}
if (converged) {
return vol;
}
// Fallback to bisection
let lower = VOL_LOWER_BOUND;
let upper = VOL_UPPER_BOUND;
// Check that solution is bracketed
const priceLower = priceBlackScholes({ ...params, vol: lower }).price;
const priceUpper = priceBlackScholes({ ...params, vol: upper }).price;
if (observedPrice < priceLower) {
throw new Error(
`Cannot find IV: price ${observedPrice} is below minimum possible at vol=${lower}`
);
}
if (observedPrice > priceUpper) {
throw new Error(
`Cannot find IV: price ${observedPrice} is above maximum possible at vol=${upper}`
);
}
for (let i = 0; i < maxIterations; i++) {
const mid = (lower + upper) / 2;
const midPrice = priceBlackScholes({ ...params, vol: mid }).price;
if (Math.abs(midPrice - observedPrice) < tolerance) {
return mid;
}
if (midPrice < observedPrice) {
lower = mid;
} else {
upper = mid;
}
if (upper - lower < tolerance) {
return mid;
}
}
throw new Error(
`Implied volatility solver did not converge after ${maxIterations} iterations`
);
}