“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

Implied Volatility

Finance

What you need to know first 1 concepts, 1 layers

The requisite-knowledge inventory for this page, bottom-up: the primitives at the base, combined upward until you reach what this page assumes. Skim the layers you already own; start wherever the ground gets unfamiliar.

  1. base
  2. you are here

Implied volatility is the volatility parameter that, when plugged into the Black-Scholes formula, gives the observed market price of an option. It represents the market's expectation of future volatility.

Definition

Given the market price of an option, implied volatility is the solution to:

where is the Black-Scholes formula. This is a root-finding problem since the Black-Scholes formula cannot be inverted analytically.

Vega

Vega measures the sensitivity of option price to volatility:

For the Black-Scholes model:

where is the standard normal probability density function. Vega is always positive, meaning option prices increase with volatility.

Root-Finding Methods

Several numerical methods can be used to find implied volatility:

Volatility Smile

The volatility smile (or skew) is the pattern where implied volatility varies with strike price. This contradicts the Black-Scholes assumption of constant volatility and reflects:

Python Implementation

The following code implements implied volatility calculation using multiple root-finding methods:

import numpy as np
from scipy.stats import norm
import matplotlib.pyplot as plt

def black_scholes(S, K, T, r, sigma, option_type='call'):
    """Black-Scholes option pricing formula."""
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    
    if option_type == 'call':
        return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)
    else:
        return K * np.exp(-r * T) * norm.cdf(-d2) - S * norm.cdf(-d1)

def vega(S, K, T, r, sigma):
    """Vega: sensitivity of option price to volatility."""
    d1 = (np.log(S / K) + (r + 0.5 * sigma**2) * T) / (sigma * np.sqrt(T))
    return S * np.sqrt(T) * norm.pdf(d1)

def bisection_iv(S, K, T, r, market_price, option_type='call', tol=1e-6, max_iter=100):
    """Bisection: robust but slow. Returns (implied vol, iterations used)."""
    low, high = 1e-9, 5.0  # Reasonable bounds for volatility

    for i in range(max_iter):
        mid = (low + high) / 2
        price = black_scholes(S, K, T, r, mid, option_type)
        error = price - market_price

        if abs(error) < tol:
            return mid, i + 1

        if error > 0:
            high = mid
        else:
            low = mid

    return (low + high) / 2, max_iter

def newton_raphson_iv(S, K, T, r, market_price, option_type='call', tol=1e-6, max_iter=100):
    """Newton-Raphson with vega as the derivative. Returns (implied vol, iterations)."""
    sigma = 0.2  # Initial guess

    for i in range(max_iter):
        price = black_scholes(S, K, T, r, sigma, option_type)
        error = price - market_price

        if abs(error) < tol:
            return sigma, i + 1

        # Update using Newton-Raphson: x_new = x - f(x)/f'(x)
        v = vega(S, K, T, r, sigma)
        if v == 0:
            break

        sigma = max(1e-9, min(5.0, sigma - error / v))

    return sigma, max_iter

def secant_iv(S, K, T, r, market_price, option_type='call', tol=1e-6, max_iter=100):
    """Secant: no derivative needed. Returns (implied vol, iterations)."""
    x0, x1 = 0.1, 0.3  # Initial guesses

    for i in range(max_iter):
        f0 = black_scholes(S, K, T, r, x0, option_type) - market_price
        f1 = black_scholes(S, K, T, r, x1, option_type) - market_price

        if abs(f1) < tol:
            return x1, i + 1

        if f1 == f0:
            break

        # Secant update: x_new = x1 - f1 * (x1 - x0) / (f1 - f0)
        x_new = x1 - f1 * (x1 - x0) / (f1 - f0)
        x0, x1 = x1, max(1e-9, min(5.0, x_new))

    return x1, max_iter

# Example: Calculate implied volatility
S = 100  # Stock price
K = 100  # Strike price
T = 0.5  # Time to expiration (6 months)
r = 0.05  # Risk-free rate
true_vol = 0.25  # True volatility (unknown in practice)
market_price = black_scholes(S, K, T, r, true_vol, 'call')  # Simulated market price

print("Market Price: {:.4f}".format(market_price))
print("True Volatility: {:.4f}".format(true_vol))
for name, fn in [("Bisection", bisection_iv), ("Newton-Raphson", newton_raphson_iv), ("Secant", secant_iv)]:
    iv, n = fn(S, K, T, r, market_price, 'call')
    print("Implied Volatility ({}): {:.6f}  ({} iterations)".format(name, iv, n))

# Volatility Smile
strike_prices = np.linspace(80, 120, 20)
base_vol = 0.2

# Generate market prices with a smile pattern
def generate_smile_price(S, K, T, r, base_vol, smile_factor=0.3):
    """Generate option price with volatility smile."""
    distance = abs(K - S) / S
    implied_vol = base_vol * (1 + smile_factor * distance**2)
    return black_scholes(S, K, T, r, implied_vol, 'call')

market_prices_smile = [generate_smile_price(S, K_val, T, r, base_vol) for K_val in strike_prices]

# Calculate implied volatilities
implied_vols = [bisection_iv(S, K_val, T, r, price, 'call')[0]
                for K_val, price in zip(strike_prices, market_prices_smile)]

# Measured convergence: iterations each method actually needs, per strike
bis_iters = [bisection_iv(S, Kv, T, r, p, 'call')[1] for Kv, p in zip(strike_prices, market_prices_smile)]
newt_iters = [newton_raphson_iv(S, Kv, T, r, p, 'call')[1] for Kv, p in zip(strike_prices, market_prices_smile)]
sec_iters = [secant_iv(S, Kv, T, r, p, 'call')[1] for Kv, p in zip(strike_prices, market_prices_smile)]

# Plot results
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Left: Volatility smile
axes[0].plot(strike_prices, implied_vols, 'b-o', linewidth=2, markersize=4)
axes[0].axvline(S, color='r', linestyle='--', linewidth=2, label='ATM (S = 100)')
axes[0].set_xlabel('Strike Price')
axes[0].set_ylabel('Implied Volatility')
axes[0].set_title('Volatility Smile (recovered by root-finding)')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Right: measured convergence at each strike
axes[1].plot(strike_prices, bis_iters, 'r-o', label='Bisection', linewidth=2, markersize=4)
axes[1].plot(strike_prices, newt_iters, 'b-o', label='Newton-Raphson', linewidth=2, markersize=4)
axes[1].plot(strike_prices, sec_iters, 'g-o', label='Secant', linewidth=2, markersize=4)
axes[1].set_xlabel('Strike Price')
axes[1].set_ylabel('Iterations to |error| < 1e-6')
axes[1].set_title('Measured convergence, per strike')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('public/figures/implied_volatility_plot.png', dpi=300, bbox_inches='tight')

# Print summary
print("\nVolatility Smile Statistics:")
print("  Minimum IV: {:.4f} at K = {:.2f}".format(min(implied_vols), strike_prices[np.argmin(implied_vols)]))
print("  Maximum IV: {:.4f} at K = {:.2f}".format(max(implied_vols), strike_prices[np.argmax(implied_vols)]))
print("  ATM IV: {:.4f}".format(implied_vols[len(implied_vols)//2]))
print("\nMeasured iterations (min-max across strikes):")
print("  Bisection: {}-{}   (theory: ~log2(range/tol) = {:.1f})".format(min(bis_iters), max(bis_iters), np.log2(5.0 / 1e-6)))
print("  Newton-Raphson: {}-{}".format(min(newt_iters), max(newt_iters)))
print("  Secant: {}-{}".format(min(sec_iters), max(sec_iters)))

Running it (the script lives at scripts/gen_implied_vol.py and is what generated the figure below):

Market Price: 8.2600
True Volatility: 0.2500
Implied Volatility (Bisection): 0.250000  (26 iterations)
Implied Volatility (Newton-Raphson): 0.250000  (3 iterations)
Implied Volatility (Secant): 0.250000  (4 iterations)

Volatility Smile Statistics:
  Minimum IV: 0.2000 at K = 98.95
  Maximum IV: 0.2024 at K = 80.00
  ATM IV: 0.2000

Measured iterations (min-max across strikes):
  Bisection: 22-26   (theory: ~log2(range/tol) = 22.3)
  Newton-Raphson: 2-3
  Secant: 4-9

Visualization

The following plots show the volatility smile and convergence comparison:

Left: recovered volatility smile vs strike. Right: measured iterations to convergence per strike for bisection (22-26), secant (4-9), and Newton-Raphson (2-3).

Key Features

Applications

Implied volatility is used for:

Volatility Surface

Fix a single expiration and plot implied volatility against strike: that is the smile from earlier — a curve. But implied volatility also moves with time to expiration; options on the same strike but different maturities imply different vols. Let both axes vary and the smile sweeps out a two-dimensional volatility surface, — one implied vol for every strike and expiration .

That surface is the desk's working object — the entire listed-options market restated as one number per strike–expiration cell. It is what gets used downstream: