Volatilità implicita


La volatilità implicita, che riflette la paura o l'incertezza del mercato, si valuta dal prezzo delle opzioni ed è il consenso del mercato su quanto si muoverà il sottostante in futuro.
Poiché le opzioni sono strumenti di assicurazione, la volatilità implicita è, in sostanza, il prezzo che il mercato chiede per assicurarsi contro i movimenti del sottostante. Se la volatilità implicita è alta, il mercato ha paura e l'assicurazione è cara, se invece è bassa, il mercato è sereno e l'assicurazione è economica.
Ad esempio per le Obbligazioni Treasury, i titoli di Stato emessi dal governo degli Stati Uniti per finanziare il debito pubblico, la volatilità implicita del 5% mostra che il mercato obbligazionario molto stabile; per le obbligazioni legate a S&P 500 il mercato azionario si mostra relativamente calmo con volatilità implicita intorno al 16%; con una volatilità al 65% il mercato delle criptovalute è estremamente incerto.

La valutazione più o meno corretta del prezzo di una opzione in base al valore attuale del bene sottostante e a quello stimato per il futuro è oggetto di una vasta letteratura matematica; particolarmente affermata è la formula di Black e Scholes che offre una soluzione in forma chiusa, dipende da 5 parametri facilmente osservabili ad esclusione della volatilità e, posto che oggi esistono modelli più complessi, è il punto di partenza per capire il pricing delle opzioni.
Ad esempio il prezzo teorico di una call europea, cioè esercitabile solo a scadenza, è: \[ C = S_0 \cdot N(d_1) - K \cdot e^{-rT} \cdot N(d_2) \] con:

Occorrerebbe poi tener conto del costo di transazione e che le azioni pagano dividendi.
import numpy as np from scipy.optimize import brentq from scipy.stats import norm def black_scholes_price(S, K, T, r, sigma, option_type='call'): if T <= 0 or sigma <= 0: return 0 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 implied_volatility(price, S, K, T, r, option_type='call'): # Se il prezzo è zero o negativo, la volatilità implicita non è definita if price <= 0: return np.nan # σ → prezzo(σ) - price è la funzione di cui cercare dove si annulla def f(sigma): return black_scholes_price(S, K, T, r, sigma, option_type) - price try: # Cerca la radice nell'intervallo [0.001, 5.0] molto ampio essendo 5.0 = 500% di volatilità return brentq(f, 0.001, 5.0, xtol=1e-6) except (ValueError, RuntimeError): return np.nan S = 100 # Prezzo del sottostante K = 105 # Strike T = 1.0 # 1 anno r = 0.05 # 5% risk-free price = 8.0 # Prezzo di mercato dell'opzione iv = implied_volatility(price, S, K, T, r, option_type='call') print(f"Volatilità implicita: {iv*100:.2f}%")
Volatilità implicita: 20.00%
Il metodo scipy.optimize.brentq() fa uso del metodo di Brent per trovare uno zero della funzione su un intervallo in cui cambia segno, che combina metodo di bisezione, quello delle secanti e quello dell'estrapolazione quadratica inversa, a volte noto come metodo van Wijngaarden-Dekker-Brent.

Se nel mondo ideale di Black-Scholes la volatilità implicita sarebbe costante per tutti gli strike e tutte le scadenze, nella realtà:

Il volatility smile visualizza la relazione tra lo strike dell'opzione e la volatilità implicita estratta dal prezzo dell'opzione.

import yfinance as yf import matplotlib.pyplot as plt import pandas as pd from datetime import datetime, timedelta def get_volatility_smile(symb='^SPX', expiration_date=None, risk_free_rate=0.035, min_strikes=10): """ Parametri: symb: simbolo del sottostante (es. '^SPX', 'SPY') expiration_date: data di scadenza (None = la più vicina) risk_free_rate: tasso risk-free min_strikes: numero minimo di strike da mantenere Ritorna: DataFrame con: strike, mid_price, implied_vol, moneyness """ stock = yf.Ticker(symb) # Scarica dati del sottostante hist = stock.history(period='1d') if hist.empty: raise ValueError(f"Nessun dato per {symb}") S = hist.Close.iloc[-1] # Prezzo corrente expirations = stock.options # Opzioni disponibili if not expirations: raise ValueError(f"Nessuna opzione disponibile per {symb}") if expiration_date is None: # Usa la scadenza più vicina che sia almeno a 10 giorni expiration_date = [exp for exp in expirations if (datetime.strptime(exp, '%Y-%m-%d').date() - datetime.now().date()).days >=10][0] print(f"📊 Sottostante: {symb}: Prezzo corrente: ${S:.2f}, Scadenza opzioni: {expiration_date}") chain = stock.option_chain(expiration_date) # Scarica la catena di opzioni # Calcola il tempo alla scadenza (in anni) exp_date = datetime.strptime(expiration_date, '%Y-%m-%d').date() T = (exp_date - datetime.now().date()).days / 365.25 T = max(T, 0.01) # minimo 1 giorno # Unisce call e put calls = chain.calls.copy() calls['option_type'] = 'call' puts = chain.puts.copy() puts['option_type'] = 'put' all_options = pd.concat([calls, puts], ignore_index=True) # Filtra opzioni illiquide (bid=0) all_options = all_options[all_options['bid'] > 0] # Calcola prezzo medio all_options['mid_price'] = (all_options.bid + all_options.ask) / 2 # Calcola moneyness (S/K) - più intuitivo per il grafico all_options['moneyness'] = S / all_options.strike # Calcola la volatilità implicita per ogni opzione all_options['implied_vol'] = all_options.apply( lambda row: implied_volatility( price=row['mid_price'], S=S, K=row['strike'], T=T, r=risk_free_rate, option_type=row['option_type'] ), axis=1 ) # Rimuove NaN all_options = all_options.dropna(subset=['implied_vol']) # Se abbiamo abbastanza opzioni, filtra strike estremi (per evitare distorsioni) if len(all_options) > min_strikes: all_options = all_options[(all_options.strike > S * 0.6) & (all_options.strike < S * 1.4)] return all_options, S, T def compare_term_structure(symb='^SPX', risk_free_rate=0.035): """ Confronta il sorriso di volatilità per diverse scadenze. """ expirations = yf.Ticker(symb).options if not expirations: print("Nessuna opzione disponibile") return # Sceglie 3 scadenze: breve, media, lunga today = datetime.now().date() exp_dates = [] for exp in expirations[:10]: # Prime 10 scadenze days = (datetime.strptime(exp, '%Y-%m-%d').date() - today).days if days >= 10: exp_dates.append((exp, days)) if len(exp_dates) > 3: # Prendi: breve (prima), media (metà), lunga (ultima) exp_dates = [exp_dates[0], exp_dates[len(exp_dates)//2], exp_dates[-1]] fig, axes = plt.subplots(1, 2, figsize=(14, 6)) colors = ['blue', 'green', 'red'] for i, (exp, days) in enumerate(exp_dates): print(f"Caricamento scadenza: {exp} ({days} giorni)") try: data, S, T = get_volatility_smile(symb, expiration_date=exp, risk_free_rate=risk_free_rate) # Grafico: volatilità vs strike calls = data[data['option_type'] == 'call'] puts = data[data['option_type'] == 'put'] ax = axes[0] if not calls.empty: ax.scatter(calls.strike, calls.implied_vol * 100, color=colors[i], alpha=0.5, s=20, label=f'Call {days}d') if not puts.empty: ax.scatter(puts.strike, puts.implied_vol * 100, color=colors[i], alpha=0.5, s=20, marker='x', label=f'Put {days}d') # Grafico: volatilità vs moneyness ax = axes[1] if not calls.empty: ax.scatter(calls.moneyness, calls.implied_vol * 100, color=colors[i], alpha=0.5, s=20, label=f'Call {days}d') if not puts.empty: ax.scatter(puts.moneyness, puts.implied_vol * 100, color=colors[i], alpha=0.5, s=20, marker='x', label=f'Put {days}d') except Exception as e: print(f" ❌ Errore per {exp}: {e}") axes[0].axvline(x=S, color='black', linestyle='--', alpha=0.5, label='ATM') axes[0].set_xlabel('Strike') axes[0].set_ylabel('Volatilità Implicita (%)') axes[0].set_title('Confronto Scadenze - Vol vs Strike') axes[0].legend() axes[0].grid(True, alpha=0.3) axes[1].axvline(x=1.0, color='black', linestyle='--', alpha=0.5, label='ATM') axes[1].set_xlabel('Moneyness (S/K)') axes[1].set_ylabel('Volatilità Implicita (%)') axes[1].set_title('Confronto Scadenze - Vol vs Moneyness') axes[1].invert_xaxis() axes[1].legend() axes[1].grid(True, alpha=0.3) plt.tight_layout() plt.show() compare_term_structure(symb='^SPX', risk_free_rate=0.035)
Caricamento scadenza: 2026-07-01 (10 giorni) 📊 Sottostante: ^SPX: Prezzo corrente: $7500.58, Scadenza opzioni: 2026-07-01 Caricamento scadenza: 2026-07-02 (11 giorni) 📊 Sottostante: ^SPX: Prezzo corrente: $7500.58, Scadenza opzioni: 2026-07-02 Caricamento scadenza: 2026-07-06 (15 giorni) 📊 Sottostante: ^SPX: Prezzo corrente: $7500.58, Scadenza opzioni: 2026-07-06
Questa forma a "sorriso", o meglio, a "smorfia", per gli indici azionari, con coda sinistra più pronunciata, riflette appunto il fatto che il mercato assegna una probabilità maggiore a eventi estremi rispetto a quanto previsto dalla distribuzione normale.

Riferimenti sito/bibliografici