Setup
Market Context
Implied volatility is the unique value of such that the Black-Scholes price matches the observed market price of an option with strike and expiry . It is the standard language in which option markets quote prices: traders say "I buy the 1-month 25-delta put at 18 vol", not "at price €3.47".
Computing implied vol is therefore an inversion problem, performed thousands of times per second in a live trading system. Correctness and speed both matter. A slow inverter causes pricing latency; a numerically fragile one produces garbage implied vols for deep OTM strikes, polluting the vol surface.
Conventions
Throughout this module:
- : Black-Scholes call price, where is spot, is strike, is the continuously compounded risk-free rate, is the continuous dividend yield, is time to expiry in years (Act/365 unless stated).
- Volatility is annualised, in decimal ().
- : the forward price.
- , .
The Black-Scholes call price:
Assumptions before use:
- Constant volatility over .
- Lognormal spot dynamics under the risk-neutral measure.
- No early exercise (European option).
- Liquid, arbitrage-free market quote with — the no-arbitrage bounds.
Theory: Implied Vol as a Root-Finding Problem
Define the objective function:
We seek such that . This function has the following properties that govern algorithm design:
- Monotonicity. For , is strictly increasing in : , where is the Black-Scholes vega and is the standard normal density. Monotonicity guarantees at most one root.
- Boundary behaviour. (intrinsic value) and (full asset price). For in the arbitrage-free interior, a root exists.
- Vega degeneracy. At extreme strikes (), , so . Newton-Raphson divides by a near-zero vega and can produce catastrophic oscillations or divergence.
Newton-Raphson
Algorithm
Starting from an initial guess , Newton-Raphson iterates:
Convergence rate. Near a simple root, Newton-Raphson converges quadratically: if , then . The number of correct decimal digits doubles per iteration. For a good initial guess, 3–5 iterations suffice.
Formal proof. By Taylor expansion around :
since . Thus:
The error is bounded by where is the asymptotic constant.
Initial Guess
A poor initial guess causes slow convergence or divergence. The Brenner-Subrahmanyam (1988) approximation for near-ATM options provides a first-order starting point:
This is a linearisation of the ATM Black-Scholes formula . For skewed strikes, a better guess uses the Corrado-Miller approximation.
Failure Modes
Deep OTM puts and calls. When , the vega is below per unit notional. Newton-Raphson steps become enormous and overshoot wildly. The algorithm must detect this case and switch strategies.
Near-zero time to expiry. As , the whole vol surface compresses. Vega scales as . Newton-Raphson becomes unreliable for years (less than one trading day).
Near-intrinsic options. If (i.e., the option is priced at near-intrinsic), the root is near and both and are near zero simultaneously. l'Hôpital's rule applies in the limit but the numerical ratio is unstable.
Brent's Method
Brent's method (1973) combines three strategies: bisection, secant, and inverse quadratic interpolation (IQI). It is guaranteed to converge to machine precision within function evaluations, making it the default fallback.
Bracketing
Brent requires a bracket with . Since is monotone and bounded:
This bracket is valid for any arbitrage-free market quote. Initial (intrinsic value is below the market) and (deep-ITM call at 1000 vol recovers full asset value), so the sign condition is satisfied.
Algorithm Sketch
Maintain three points , , with , . At each step:
- IQI if the last two steps used secant and the three points are distinct: fit a quadratic through , , and take the root.
- Secant if the last step was not secant: use the linear interpolant through , .
- Bisection if the candidate from steps 1/2 lies outside or convergence is slow: take the midpoint.
Always bisect when in doubt. The convergence guarantee (superlinear, between order 1 and order 2 in the best case) comes from this fallback. In practice, Brent converges in 5–10 iterations for vol inversion.
Convergence Guarantee
Theorem (Brent 1973). Given a continuous function and bracket with , Brent's method converges to a root in at most function evaluations, where is the tolerance.
For and , this is at most evaluations. In practice, far fewer are needed.
Hybrid Newton/Brent
Production implied vol solvers use a hybrid:
- Initialise with a bracket and a starting guess (Brenner-Subrahmanyam or similar).
- Attempt Newton-Raphson if the candidate lies within the current bracket and vega .
- Fall back to bisection (or Brent) if Newton's candidate exits the bracket or vega is degenerate.
- Update bracket: narrow each iteration using the sign of .
This gives quadratic convergence near ATM (where Newton is well-conditioned) and guaranteed convergence everywhere else. A well-implemented hybrid solves the inverse problem to tolerance in 3–7 function evaluations for the entire vol surface.
Implementation
#include <cmath>
#include <stdexcept>
#include <limits>
#include <algorithm>
// Black-Scholes cumulative normal and density
static double bs_phi(double x) noexcept {
return std::exp(-0.5 * x * x) / std::sqrt(2.0 * M_PI);
}
static double bs_Phi(double x) noexcept {
return 0.5 * std::erfc(-x / std::sqrt(2.0));
}
// Black-Scholes call price and vega
// F: forward price = S * exp((r-q)*T)
// K: strike, T: time to expiry (years), df: discount factor exp(-r*T)
struct BsResult { double price; double vega; };
static BsResult bs_call(double F, double K, double T, double df) noexcept {
if (T <= 0.0) {
double intrinsic = df * std::max(F - K, 0.0);
return {intrinsic, 0.0};
}
// avoid division by zero in degenerate cases
if (K <= 0.0) return {df * F, 0.0};
return {df * F, 0.0}; // placeholder; full impl below
}
// Full implementation
struct ImpliedVolResult {
double vol; // implied vol (annualised)
int iterations; // function evaluations used
bool converged;
};
// Hybrid Newton-Raphson / Brent implied vol inverter.
//
// Assumptions:
// - European call option (put via put-call parity)
// - Continuously compounded rates
// - market_price must satisfy no-arbitrage bounds
//
// Parameters:
// market_price: observed call price
// S : spot price
// K : strike
// r : risk-free rate (continuously compounded, annualised)
// q : continuous dividend yield (annualised)
// T : time to expiry (years)
// tol : target absolute tolerance on vol (default 1e-10)
// max_iter : maximum iterations
ImpliedVolResult implied_vol(
double market_price,
double S, double K,
double r, double q, double T,
double tol = 1.0e-10,
int max_iter = 100
) {
if (T <= 0.0)
throw std::domain_error("T must be positive");
if (S <= 0.0 || K <= 0.0)
throw std::domain_error("S and K must be positive");
const double F = S * std::exp((r - q) * T);
const double df = std::exp(-r * T);
// No-arbitrage bounds check
const double lo_bound = df * std::max(F - K, 0.0);
const double hi_bound = S * std::exp(-q * T);
if (market_price <= lo_bound || market_price >= hi_bound)
throw std::domain_error("market_price violates no-arbitrage bounds");
// BS price and vega as a function of sigma
auto price_and_vega = [&](double sigma) -> BsResult {
const double sqrtT = std::sqrt(T);
const double d1 = (std::log(F / K) + 0.5 * sigma * sigma * T) / (sigma * sqrtT);
const double d2 = d1 - sigma * sqrtT;
const double price = df * (F * bs_Phi(d1) - K * bs_Phi(d2));
const double vega = S * std::exp(-q * T) * sqrtT * bs_phi(d1);
return {price, vega};
};
// Bracket: [sigma_lo, sigma_hi]
double s_lo = 1.0e-8;
double s_hi = 10.0; // 1000% vol — safe upper bound
double f_lo = price_and_vega(s_lo).price - market_price;
double f_hi = price_and_vega(s_hi).price - market_price;
// Should always be true for valid inputs
if (f_lo * f_hi >= 0.0)
throw std::domain_error("Failed to bracket root");
// Brenner-Subrahmanyam initial guess (near-ATM approximation)
double sigma = std::sqrt(2.0 * M_PI / T) * (market_price / (S * std::exp(-q * T)));
sigma = std::clamp(sigma, s_lo, s_hi);
int iters = 0;
for (; iters < max_iter; ++iters) {
auto [price, vega] = price_and_vega(sigma);
double f = price - market_price;
// Update bracket using sign of f
if (f * f_lo < 0.0) {
s_hi = sigma; f_hi = f;
} else {
s_lo = sigma; f_lo = f;
}
// Convergence check
if (std::abs(f) < tol && std::abs(s_hi - s_lo) < tol)
return {sigma, iters + 1, true};
// Newton step — only if vega is non-degenerate
constexpr double vega_floor = 1.0e-12;
if (vega > vega_floor) {
double s_newton = sigma - f / vega;
if (s_newton > s_lo && s_newton < s_hi) {
sigma = s_newton;
continue; // accept Newton step
}
}
// Fallback: bisection
sigma = 0.5 * (s_lo + s_hi);
}
return {sigma, iters, false}; // did not converge
}
Put Options via Put-Call Parity
The inverter above handles calls. For a put:
so convert to a call price first:
// Convert put to synthetic call via put-call parity
double put_to_call(double put_price, double S, double K, double r, double q, double T) {
double F = S * std::exp((r - q) * T);
double df = std::exp(-r * T);
return put_price + df * (F - K); // C = P + df*(F - K)
}
Validation
Round-Trip Test
The fundamental correctness test: generate a Black-Scholes price from a known vol, then invert it and verify recovery.
// Catch2 test — compile with -DCATCH_CONFIG_MAIN
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
TEST_CASE("Implied vol: round-trip ATM", "[implied_vol]") {
const double S = 100.0, K = 100.0, r = 0.05, q = 0.02, T = 1.0;
const double true_vol = 0.20;
// price at true_vol
const double sqrtT = std::sqrt(T), F = S * std::exp((r-q)*T);
const double d1 = (std::log(F/K) + 0.5*true_vol*true_vol*T) / (true_vol*sqrtT);
const double d2 = d1 - true_vol * sqrtT;
const double df = std::exp(-r*T);
const double cprice = df * (F * bs_Phi(d1) - K * bs_Phi(d2));
auto result = implied_vol(cprice, S, K, r, q, T);
REQUIRE(result.converged);
REQUIRE_THAT(result.vol, Catch::Matchers::WithinAbs(true_vol, 1.0e-8));
}
TEST_CASE("Implied vol: deep OTM call", "[implied_vol]") {
const double S = 100.0, K = 150.0, r = 0.05, q = 0.0, T = 0.5;
const double true_vol = 0.30;
const double sqrtT = std::sqrt(T), F = S * std::exp(r*T);
const double d1 = (std::log(F/K) + 0.5*true_vol*true_vol*T) / (true_vol*sqrtT);
const double d2 = d1 - true_vol * sqrtT;
const double df = std::exp(-r*T);
const double cprice = df * (F * bs_Phi(d1) - K * bs_Phi(d2));
auto result = implied_vol(cprice, S, K, r, q, T);
REQUIRE(result.converged);
REQUIRE_THAT(result.vol, Catch::Matchers::WithinAbs(true_vol, 1.0e-7));
}
Convergence Across the Surface
A robust inverter must work for the full range of strikes and maturities encountered on a real vol surface. Typical production test: a 10 × 10 grid of strikes and maturities , at four vol levels (5%, 20%, 50%, 100%). The hybrid Newton/Brent solver described above converges to tolerance in all cases within 10 iterations.
Limitations
Model dependency. Implied vol is defined relative to a pricing model. Black-Scholes implied vol is the conventional market standard, but it is a model-dependent quantity. Under a different model (e.g., normal Black model, shifted lognormal), implied vol means something different. Always state the model convention.
Non-uniqueness for exotic payoffs. For exotic options without closed-form pricing, implied vol is typically not well-defined as a scalar. A vol surface (rather than a single number) is used for calibration.
Negative vega for calendar spreads. Calendar spreads can have non-monotone value in vol, breaking the single-root guarantee. Implied vol is only well-defined for vanilla options (call, put, digital).
Arbitrage-free bounds violation. Market data occasionally contains crossed quotes (bid above no-arbitrage bound, or ask below intrinsic). The inverter must detect this and either error out or apply a bid-offer spread adjustment before inversion.
Numerical precision near intrinsic. When is within a few cents of , the implied vol is near zero and the inversion is numerically ill-conditioned. Tolerances must be relaxed or an alternative formula (e.g., Bachelier normal vol) used.
Interview Angle
L1. Why is implied vol not the same as realised vol? What does it represent? Explain why implied vol is monotone in Black-Scholes — i.e., why does the call price always increase with ?
Implied vol is the market-implied expectation of future vol embedded in option prices, under the Black-Scholes model assumption. Realised vol is an ex-post measurement of actual price moves. They differ because of the variance risk premium (option sellers demand compensation for variance risk), demand/supply imbalances, and jump risk not captured in GBM.
Monotonicity: for all , , because . Higher vol increases the probability of large moves in both directions, always increasing option value.
L2. Why does Newton-Raphson fail for deep OTM options? How does the hybrid solver avoid this? Describe the Brent bracket and the bracketing invariant maintained throughout iteration.
NR failure: Vega is exponentially small for large . The Newton step divides a finite residual by near-zero, producing a step that overshoots the bracket by orders of magnitude. The hybrid detects when the Newton candidate exits the bracket and replaces it with a bisection step, maintaining convergence.
Bracket invariant: At every iteration, and (or vice versa). This is preserved by updating the bracket endpoint whose -value has the same sign as the current iterate.
L3. Analyse the convergence rate of the hybrid solver. Under what conditions does it achieve quadratic convergence, and when does it degrade to linear? Discuss the Corrado-Miller approximation as an alternative to Brenner-Subrahmanyam for off-ATM strikes, and its accuracy relative to the Newton iterate.
Convergence rate: Quadratic convergence is achieved when the Newton step remains within the bracket for every iteration — i.e., when the problem is well-conditioned (vega bounded away from zero). Brent's IQI achieves superlinear convergence (order ~1.84 asymptotically) in the general case. Pure bisection is linear (order 1), halving the bracket width each step.
The Corrado-Miller (1996) approximation improves on Brenner-Subrahmanyam by including a first-order correction for the moneyness :
providing a starting point accurate to in moneyness. For typical equity options, this initial guess is within 1–2 vol points of , reducing Newton iterations from 5–7 to 2–3.