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.
- base
- ↳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:
- Bisection method: Robust but slow
- Newton-Raphson: Fast convergence, requires derivative (vega)
- Secant method: Fast, no derivative needed
- Brent's method: Combines bisection, secant, and inverse quadratic interpolation
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:
- Market expectations of extreme moves
- Supply and demand imbalances
- Risk preferences of market participants
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:
Key Features
- Root-finding problem: cannot be solved analytically
- Multiple numerical methods available
- Vega provides fast convergence for Newton-Raphson
- Volatility smile reveals market expectations
- Important for risk management and trading
Applications
Implied volatility is used for:
- Option pricing and valuation
- Risk management (Vega hedging)
- Market sentiment analysis
- Trading strategies (volatility arbitrage)
- Model calibration
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:
- Pricing exotics consistently. A barrier, cliquet, or autocallable must agree with the vanilla options it is hedged against; the surface is the constraint they are priced to match.
- Model calibration. Stochastic- and local-volatility models (Heston, SABR, Dupire) fix their parameters by fitting the observed surface, then fill in strikes and maturities the market quotes thinly.
- Marking and risk. Interpolating the surface gives a vol for any strike and maturity, and its shape drives a book's Vega, vanna, and volga.
- Reading the market. The skew's steepness and the term structure of vol encode the priced-in view of crash risk and event timing.
- Relative value. Trades appear where one region of the surface is rich or cheap against its neighbors.