Value at Risk (VaR)
A differenza della volatilità o della Downside Deviation, che misurano la dispersione dei rendimenti, il VaR cerca di rispondere alla domanda:
"Qual è la perdita massima da aspettarsi, con un certo livello di confidenza, in un determinato intervallo di tempo?"
In altre parole, il VaR quantifica una soglia di perdita associata a una probabilità prefissata.
Se \(R\) è la variabile casuale che rappresenta profitti, positivi, e perdite, negative, il VaR al livello \(α ∈ (0,1)\) è \[\operatorname {VaR} _{\alpha }(R)=-\inf {\big \{}x\in \mathbb {R} :F_{R}(x)>\alpha {\big \}}=F_{-R}^{-1}(1-\alpha ).\]
Fu introdotto nel 1990, dopo il crollo del mercato azionario del 1987, in J.P. Morgan su richiesta di un dirigente che desiderava avere un rapporto riepilogativo sulle possibili perdite e i rischi a cui è esposta l'azienda in un giorno, ed è poi diventato così diffuso che la sua adozione è stata imposta dalle autorità di regolamentazione. Aiuta a valutare la copertura di una posizione finanziaria e, più nello specifico, il livello di protezione del capitale dal rischio di perdita massimo presupponendo prezzi mark-to-market, cioè valutazioni secondo il prezzo di mercato e nessuna negoziazione nel portafoglio, cioè senza l'attività con cui l'intermediario, su ordine del cliente, acquista dal cliente strumenti finanziari o glieli vende.
Esistono tre grandi famiglie.
- VaR storico
È il più semplice. Si usano direttamente i dati osservati raccogliendo i rendimenti, ordinandoli e prendendo il percentile desiderato; senza ipotesi sulla distribuzione.import yfinance as yf import numpy as np df = yf.download( "FTSEMIB.MI", start="2010-01-01", auto_adjust=True, progress=False ) returns = df.Close.pct_change().dropna() # Rendimenti giornalieri N = len(returns) n = 5 ret_Ord = np.sort(returns.values.flatten()) [round(ret_Ord[i*N//5],5) for i in range(n)][np.float64(-0.16927882202019073), np.float64(-0.009064844044136766), np.float64(-0.0019464940473495895), np.float64(0.00345585925227776), np.float64(0.010001837072115327)]il che equivale a[np.percentile(returns,i*100/n) for i in range(n)]o anche a[np.quantile(returns,i/n) for i in range(n)]Possiamo definire una functiondef VaR_storico(returns, confidence=0.95): return -np.percentile(returns, (1-confidence)*100) for level in [0.90,0.95,0.99]: print(f"VaR {int(level*100)}% = {100*VaR_storico(returns, confidence=level):.2f}%")VaR 90% = 1.58% VaR 95% = 2.37% VaR 99% = 4.03%Possiamo darne illustrazione visiva.import matplotlib.pyplot as plt plt.figure(figsize=(10,5)) plt.hist(returns, bins=80, density=True, alpha=0.6) for level,color in zip([0.90,0.95,0.99], ["green","orange","red"]): plt.axvline(-VaR_storico(returns, confidence=level),color=color,linewidth=2,label=f"VaR {int(level*100)}%") plt.legend() plt.grid(True) plt.title("Distribuzione dei rendimenti giornalieri del FTSE MIB") plt.xlabel("Rendimento") plt.show()Dunque per l'indice FTSE MIB mi assicuro con probabilità del 90% una perdita giornaliera massima del 1.6%, ovvero rischio con probabilità del 10% una perdita superiore al 1.6%.
- VaR parametrico
Si assume \(R∼N(μ,σ).\) Il VaR è allora \[VaR=−(μ+zσ)\] dove \(z\) è il quantile della Normale. Per il 95% \(z=−1.645,\) per il 99% \(z=−2.326.\)from scipy.stats import norm def VaR_param(returns, confidence=0.95): μ = returns.values.mean() σ = returns.values.std() return -μ - σ*norm.ppf(1-confidence) for level in [0.90,0.95,0.99]: print(f"VaR {int(level*100)}% = {100*VaR_param(returns, confidence=level):.2f}%")VaR 90% = 1.84% VaR 95% = 2.37% VaR 99% = 3.36%Questo metodo è molto veloce ma tende a sottostimare gli eventi estremi.
Per modellare meglio le code pesanti, si può usare la distribuzione t-Student.from scipy.stats import t def VaR_param_t(returns, confidence=0.95, horizon=1): """ VaR parametrico con distribuzione t-Student. Il parametro horizon (orizzonte temporale) è il periodo di tempo in giorni per il quale calcolare la perdita potenziale """ mu = returns.values.mean() * horizon sigma = returns.values.std() * np.sqrt(horizon) # Stima dei gradi di libertà (df) dai dati # Metodo semplificato: usa la curtosi from scipy.stats import kurtosis k = kurtosis(returns.values, fisher=True).item() df = 4 + 6 / k if k > 0 else 30 # approssimazione # Quantile della t-Student z = t.ppf(confidence, df) VaR = -(mu - z * sigma * np.sqrt((df-2)/df)) # scala per la varianza return VaR for level in [0.90,0.95,0.99]: print(f"VaR {int(level*100)}% = {100*VaR_param_t(returns, confidence=level):.2f}%")VaR 90% = 1.61% VaR 95% = 2.23% VaR 99% = 3.79% - VaR Monte Carlo
Si ottiene simulando un enorme numero di scenari.def VaR_MC(returns, confidence=0.95, horizon=1, n_simulations=100000, method='normal', seed=42): """ Parametri: returns: array di rendimenti storici confidence: livello di confidenza (es. 0.95) horizon: orizzonte temporale in giorni n_simulations: numero di simulazioni method: 'normal', 't_student', o 'historical' (bootstrap) seed: seme per riproducibilità """ np.random.seed(seed) # Stima parametri mu = returns.values.mean() * horizon sigma = returns.values.std() * np.sqrt(horizon) # Simula rendimenti futuri if method == 'normal': # Distribuzione normale simulated_returns = np.random.normal(mu, sigma, n_simulations) elif method == 't_student': # Distribuzione t-Student (code pesanti) if df is None: # Stima df dalla curtosi from scipy.stats import kurtosis k = kurtosis(returns.values, fisher=True).item() # curtosi in eccesso, convertita a scalare df = 4 + 6 / k if k > 0 else 30 df = min(df, 30) # limita per stabilità # Genera da t-Student e scala z = np.random.standard_t(df, n_simulations) simulated_returns = mu + sigma * z * np.sqrt((df-2)/df) elif method == 'historical': # Bootstrap storico (campiona con reinserimento) simulated_returns = np.random.choice(returns.values.flatten(), n_simulations) * horizon else: raise ValueError("method deve essere 'normal', 't_student' o 'historical'") # Ordina i rendimenti simulati sorted_returns = np.sort(simulated_returns) # VaR: quantile a (1-confidence) var_index = int(n_simulations * (1 - confidence)) var = sorted_returns[var_index] # VaR come perdita (negativo) return -var # VaR in valore assoluto (positivo) confidence = 0.95 horizon = 1 # 1 giorno n_simulations = 100000 print(f"VaR Monte Carlo (Normale) {confidence*100:.0f}%: VaR: {VaR_MC(returns, confidence, horizon, n_simulations, method='normal')*100:.2f}%") print(f"VaR Monte Carlo (t-Student) {confidence*100:.0f}%: VaR: {VaR_MC(returns, confidence, horizon, n_simulations, method='t_student')*100:.2f}%") print(f"VaR Monte Carlo (historical) {confidence*100:.0f}%: VaR: {VaR_MC(returns, confidence, horizon, n_simulations, method='historical')*100:.2f}%")VaR Monte Carlo (Normale) 95%: VaR: 2.37% VaR Monte Carlo (t-Student) 95%: VaR: 2.24% VaR Monte Carlo (historical) 95%: VaR: 2.41%È il metodo più flessibile, ma anche il più costoso computazionalmente
- RiskMetrics—Technical Document, by J.P.Morgan/Reuters
- Value-at-Risk (VaR), by Simon Benninga and Zvi Wiener
- The World According to Nassim Taleb