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:
- \(S_0\) prezzo corrente del sottostante, prezzo spot oggi,
- \(K\) strike price, prezzo d'esercizio, a cui comprare a scadenza,
- \(T\) tempo alla scadenza in anni,
- \(r\) tasso annuale continuo risk-free supposto costante, anche se in realtà varia,
- \(\sigma\) volatilità ovvero deviazione standard annualizzata dei rendimenti supposta costante, anche se in realtà varia,
- \(d \rightarrow N(d)\) funzione di ripartizione della normale standard sebbene in realtà i rendimenti abbiano code più pesanti,
- \(d_1 = \frac{\ln\frac{S_0}{K} + \left(r + \frac{\sigma^2}{2}\right) T}{\sigma \sqrt{T}}\) legato alla probabilità risk-neutral, pesata per il valore del sottostante, che la call venga esercitata,
- \(d_2 = d_1 - \sigma \sqrt{T}\) legato alla probabilità risk-neutral che la call venga esercitata,
- \(S_0 \cdot N(d_1)\) valore atteso del sottostante alla scadenza, solo nei casi in cui la call termina in-the-money, ossia \(S_T > K,\) attualizzato al presente,
- \(e^{-rT}\) fattore di attualizzazione, valore attuale di di ciò che avrà valore 1 a scadenza,
- \(K \cdot e^{-rT} \cdot N(d_2)\) valore attuale del prezzo \(K\) che si paga a scadenza, solo nei casi in cui la call termina in-the-money.
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à:
- la volatilità implicita varia con la scadenza (Term structure); per periodi brevi riflette eventi specifici, per periodi lunghi tende a una media storica;
- per gli indici azionari, la volatilità implicita delle put OTM è più alta di quella delle call OTM, un'asimmetria nella volatilità implicita dei prezzi di esercizio delle opzioni che in genere riflette la maggiore paura degli investitori di crolli al ribasso rispetto ai rally al rialzo;
- per indici azionari, la volatilità implicita è più alta per strike estremi, soprattutto per le opzioni out-of-the-money (OTM) che hanno volatilità implicita più alta di quelle at-the-money (ATM), indicando che il mercato assegna una probabilità più alta a eventi estremi (coda sinistra per le put, coda destra per le call) rispetto a quanto previsto da una distribuzione normale, riflettendo così il rischio di coda.
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.
- Implied volatility, From Wikipedia, the free encyclopedia
- Volatility smile, From Wikipedia, the free encyclopedia
- Implied Volatility Tutorials and Reference, on macroption