Implied Volatility

Finance

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
import matplotlib as mpl

def set_publication_style():
    """Set publication-quality matplotlib style."""
    mpl.rcParams.update({
        'font.family': 'serif',
        'font.size': 12,
        'axes.labelsize': 14,
        'axes.titlesize': 16,
        'axes.linewidth': 1.2,
        'axes.labelpad': 8,
        'axes.titlepad': 10,
        'xtick.labelsize': 12,
        'ytick.labelsize': 12,
        'xtick.direction': 'in',
        'ytick.direction': 'in',
        'xtick.top': True,
        'ytick.right': True,
        'xtick.major.size': 6,
        'ytick.major.size': 6,
        'xtick.major.width': 1.2,
        'ytick.major.width': 1.2,
        'legend.fontsize': 12,
        'legend.frameon': False,
        'lines.linewidth': 2,
        'lines.markersize': 6,
        'figure.dpi': 100,
        'savefig.dpi': 300,
        'savefig.bbox': 'tight'
    })

set_publication_style()

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):
    """
    Find implied volatility using bisection method.
    
    Parameters
    ----------
    S : float
        Stock price
    K : float
        Strike price
    T : float
        Time to expiration
    r : float
        Risk-free rate
    market_price : float
        Observed market price
    option_type : str
        'call' or 'put'
    tol : float
        Tolerance
    max_iter : int
        Maximum iterations
    
    Returns
    -------
    float
        Implied volatility
    """
    low, high = 1e-9, 5.0  # Reasonable bounds for volatility
    
    for _ 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
        
        if error > 0:
            high = mid
        else:
            low = mid
    
    return (low + high) / 2

def newton_raphson_iv(S, K, T, r, market_price, option_type='call', tol=1e-6, max_iter=100):
    """
    Find implied volatility using Newton-Raphson method.
    
    Uses vega as the derivative for faster convergence.
    """
    sigma = 0.2  # Initial guess
    
    for _ in range(max_iter):
        price = black_scholes(S, K, T, r, sigma, option_type)
        error = price - market_price
        
        if abs(error) < tol:
            return sigma
        
        # Update using Newton-Raphson: x_new = x - f(x)/f'(x)
        v = vega(S, K, T, r, sigma)
        if v == 0:
            break
        
        sigma = sigma - error / v
        
        # Keep within reasonable bounds
        sigma = max(1e-9, min(5.0, sigma))
    
    return sigma

def secant_iv(S, K, T, r, market_price, option_type='call', tol=1e-6, max_iter=100):
    """
    Find implied volatility using secant method.
    
    No derivative needed, uses two initial guesses.
    """
    x0, x1 = 0.1, 0.3  # Initial guesses
    
    for _ 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
        
        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, x_new
        
        # Keep within reasonable bounds
        x1 = max(1e-9, min(5.0, x1))
    
    return x1

# 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))
print("\nImplied Volatility (Bisection): {:.6f}".format(bisection_iv(S, K, T, r, market_price, 'call')))
print("Implied Volatility (Newton-Raphson): {:.6f}".format(newton_raphson_iv(S, K, T, r, market_price, 'call')))
print("Implied Volatility (Secant): {:.6f}".format(secant_iv(S, K, T, r, market_price, 'call')))

# 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') 
                for K_val, price 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')
axes[0].legend()
axes[0].grid(True, alpha=0.3)

# Right: Convergence comparison
n_sims = [10, 20, 50, 100, 200, 500]
bisection_iters = []
newton_iters = []
secant_iters = []

# Note: This is a simplified comparison - in practice, iterations depend on initial guess
for n in n_sims:
    # Count iterations (simplified - actual count depends on tolerance)
    bisection_iters.append(int(np.log2(5.0 / 1e-6)))  # Approximate
    newton_iters.append(5)  # Typically converges in 3-5 iterations
    secant_iters.append(7)  # Typically converges in 5-7 iterations

axes[1].plot(n_sims, bisection_iters, 'r-o', label='Bisection', linewidth=2)
axes[1].plot(n_sims, newton_iters, 'b-o', label='Newton-Raphson', linewidth=2)
axes[1].plot(n_sims, secant_iters, 'g-o', label='Secant', linewidth=2)
axes[1].set_xlabel('Problem Complexity (arbitrary)')
axes[1].set_ylabel('Typical Iterations')
axes[1].set_title('Convergence Comparison')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

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

# 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]))

Visualization

The following plots show the volatility smile and convergence comparison:

Implied Volatility and Volatility Smile

Key Features

Applications

Implied volatility is used for:

Volatility Surface

In practice, implied volatility depends on both strike and time to expiration, forming a volatility surface. This surface is used to price exotic options and calibrate stochastic volatility models.