#!pip install yfinance
#!pip install arch
Modélisation de la value at risk
PROJET STATISTIQUE DES RISQUES EXTRÊMES
Init
Import des données
import pandas as pd
import numpy as np
import seaborn as sns
import math
import matplotlib.pyplot as plt
import warnings
from scipy import stats
'ignore')
warnings.filterwarnings(42) np.random.seed(
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# Télécharger les données historiques du CAC 40
= yf.Ticker("^FCHI")
cac_40 = cac_40.history("max")
data
# Formater l'index en année-mois-jour
= data.index.strftime('%Y-%m-%d')
data.index
= data
ts_data ts_data
Open | High | Low | Close | Volume | Dividends | Stock Splits | |
---|---|---|---|---|---|---|---|
Date | |||||||
1990-03-01 | 1836.000000 | 1838.000000 | 1827.000000 | 1832.000000 | 0 | 0.0 | 0.0 |
1990-03-02 | 1831.000000 | 1860.000000 | 1831.000000 | 1860.000000 | 0 | 0.0 | 0.0 |
1990-03-05 | 1866.000000 | 1874.000000 | 1862.000000 | 1874.000000 | 0 | 0.0 | 0.0 |
1990-03-06 | 1869.000000 | 1875.000000 | 1866.000000 | 1872.000000 | 0 | 0.0 | 0.0 |
1990-03-07 | 1874.000000 | 1881.000000 | 1874.000000 | 1880.000000 | 0 | 0.0 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... |
2024-04-17 | 7958.520020 | 8060.990234 | 7958.000000 | 7981.509766 | 59951900 | 0.0 | 0.0 |
2024-04-18 | 8013.910156 | 8039.189941 | 7976.399902 | 8023.259766 | 57583000 | 0.0 | 0.0 |
2024-04-19 | 7971.680176 | 8039.479980 | 7946.839844 | 8022.410156 | 0 | 0.0 | 0.0 |
2024-04-22 | 8068.140137 | 8071.810059 | 8007.450195 | 8040.359863 | 56900500 | 0.0 | 0.0 |
2024-04-23 | 8064.319824 | 8111.169922 | 8052.569824 | 8105.779785 | 0 | 0.0 | 0.0 |
8669 rows × 7 columns
Mise en forme et nettoyage des données
= ts_data[[ 'Close']] ts_close
### Visualisation des valuers manquantes
print(f"Y'a t-il des valeurs manquantes ? {any(pd.isna(ts_close['Close']))}")
'Close'])] ts_close[pd.isna(ts_close[
Y'a t-il des valeurs manquantes ? False
Close | |
---|---|
Date |
Nous n’avons aucune valeur manquante dans nos données
calcul du log rendement
'log_return'] = np.log(ts_close['Close'] / ts_close['Close'].shift(1))
ts_close[= ts_close.dropna(subset=['log_return'])
ts_close print(ts_close.head())
Close log_return
Date
1990-03-02 1860.0 0.015168
1990-03-05 1874.0 0.007499
1990-03-06 1872.0 -0.001068
1990-03-07 1880.0 0.004264
1990-03-08 1917.0 0.019490
Représentation de la série des cours et de la série des log rendements en fonction de la date
=(12, 12))
plt.figure(figsize
= plt.subplots(1)
fig, ax1
'Date')
ax1.set_xlabel('historical log returns', color='red')
ax1.set_ylabel('log_return'], color='red')
ax1.plot(ts_close.index, ts_close[='y', labelcolor='red')
ax1.tick_params(axis
= ax1.twinx()
ax2 'historical CAC 40', color='blue')
ax2.set_ylabel('Close'], color='blue')
ax2.plot(ts_close.index, ts_close[='y', labelcolor='blue')
ax2.tick_params(axis
'CAC40')
plt.title( plt.show()
<Figure size 1200x1200 with 0 Axes>
Le graphique ci-dessus représente la série des cours et la série des log rendements du CAC40 en fonction de la date. La série des cours est assez volatile et a une tendance à la hausse non-linéaire. Il semble qu’elle ne soit pas stationnaire. Cependant, la série des log rendements a une tendance constante et semble être stationnaire. Nous vérifierons cela également à travers les fonctions d’autocorrélation et d’autocorrélation partielle et par le test de Dickey-Fuller.
Fonction d’autocorrélation et d’autocorrélation partielle de la série des log rendements
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
= plt.subplots(1,2, figsize=(8, 4))
fig, ax 'log_return'].dropna(), ax=ax[0],title='ACF')
plot_acf(ts_close['log_return'].dropna(), ax=ax[1],title='PACF')
plot_pacf(ts_close[ plt.show()
Comme nous pouvons le voir sur les graphiques ci-dessus, l’autocorrélation et l’autocorrélation partielle des log rendements sont très faibles. Cela signifie que les log rendements sont stationnaires. Nous vérifierons cela à travers le test de Dickey-Fuller.
Etude de la stationnarité
## Test de dickey fuller
from statsmodels.tsa.stattools import adfuller
= adfuller(ts_close['log_return'].dropna(), regression='ct')
adf_result print(f"ADF Statistic: {adf_result[0]}" and f"p-value: {adf_result[1]}")
p-value: 0.0
La pvalue étant nulle, on rejette l’hypothèse nulle de présence d’une racine unitaire. La série des log rendements est donc stationnaire. Par conséquent, nous travaillerons avec cette série pour modéliser la VaR à horizon 1 jour.
Découpage train test
# Nous considérons 75% de nos données dans l'échantillon d'apprentissage et 25% dans l'échantillon de test
= int(len(ts_close)*0.75)
train_size = len(ts_close)-train_size
test_size
= ts_close.iloc[0:train_size,:].dropna()
train_close = ts_close.iloc[train_size:len(ts_close),:]
test_close print("Taille de l'ensemble d'apprentissage :", len(train_close))
print("Taille de l'ensemble de test :", len(test_close))
sum() pd.isna(train_close).
Taille de l'ensemble d'apprentissage : 6501
Taille de l'ensemble de test : 2167
Close 0
log_return 0
dtype: int64
Statistiques descriptives des rendements
'log_return'].describe() train_close[
count 6501.000000
mean 0.000153
std 0.014051
min -0.094715
25% -0.006990
50% 0.000383
75% 0.007740
max 0.105946
Name: log_return, dtype: float64
'log_return'].describe() test_close[
count 2167.000000
mean 0.000228
std 0.011795
min -0.130983
25% -0.004818
50% 0.000643
75% 0.005964
max 0.080561
Name: log_return, dtype: float64
Sur les deux jeux de données les log rendements sont relativement volatiles avec une dispersion plus importante pour la base d’apprentissage que pour la base de test. On remarque par ailleurs que le min est plus petit et le max plus grand dans la base d’apprentissage que dans la base de test. Nous pouvions nous y attendre car la base d’apprentissage couvre une période plus longue. Les deux séries sont malgré tout assez semblables en terme de moyenne avec une moyenne d’environ 0.015%.
Modélisation de la VaR.
La var est définie comme la perte potentielle maximale que peut subir un portefeuille sur un horizon avec un certain niveau de confiance. Dans ce notebook, nous adopterons une vision rentabilité. Dans ce cas, nous allons modéliser : \(P[R<VaR_h] =1- \alpha\).
Nous allons appliquer les différentes méthodes de modélisation de la Var:
- Méthodes non-paramétriques
- VaR historique
- VaR bootstrap
- Méthodes paramétriques
- VaR gaussienne
- VaR skew student
- VaR TVE (Théorie des valeurs extrêmes)
- VaR AR-GARCH
VaR non paramétrique
VaR historique :
Ici on estime la distribution des rendements R par la fonction de répartition empirique du vecteur d’observations. La VaR est alors donnée par le quantile empirique d’ordre \(1-\alpha\) :
\(\hat{VaR}_h(\alpha) = \hat{F_n^{-1}}(1-\alpha)\) avec \(\hat{F_n}(1-\alpha)= \frac{1}{n} \sum_{i=1}^{n} 1_{R_i \leq (1-\alpha)}\)
Nous utiliserons ainsi la fonction numpy.percentile
pour calculer la VaR historique.
def hist_var(returns, index, fenetre, seuil):
"""Cette fonction calcule la Value at Risk (VaR) historique d'une série temporelle de log rendements
Args:
returns (numpy_array ): serie de log rendements
index (int): indice maximal de la série à considérer pour le calcul
fenetre (int): nombre de jours sur lesquels on calcule la VaR
seuil (float): niveau de confiance de la VaR
Return:
float: VaR historique
"""
return np.percentile(returns[index-fenetre:index], 100*(1-seuil))
= hist_var(train_close["log_return"], train_size, 251*10, 0.99) #on utilise 10 ans comme fenêtre
var_histprint(var_hist)
-0.04320825141346711
Backtesting
=(12, 8))
plt.figure(figsize0:train_size], train_close['log_return'], label="historical train log returns", color = 'gray')
plt.plot(ts_close.index[= ts_close.index[train_size-251*10-1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
plt.axvline(x 'log_return'], label="historical test log returns", color = 'blue')
plt.plot(ts_close.index[train_size:], test_close[for i in range(test_size)], label="Non parametric VaR", color = 'red')
plt.plot(ts_close.index[train_size:], [var_hist = [i for i in range(len(test_close['log_return'])) if test_close['log_return'][i]<var_hist]
list_exceptions_np 'log_return'][list_exceptions_np], color='red', label='Exceptions')
plt.scatter(test_close.index[list_exceptions_np], test_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
print(f"Le nombre d'exceptions pour la VaR non paramétrique est: {len(list_exceptions_np)}")
print(f"Le pourcentage d'exceptions pour la VaR non paramétrique est: {len(list_exceptions_np)/test_size}")
Le nombre d'exceptions pour la VaR non paramétrique est: 10
Le pourcentage d'exceptions pour la VaR non paramétrique est: 0.0046146746654360865
Nous allons maintenant vérifier si la probabilité d’exception est statiquement égale à \(\alpha\).
from scipy import stats
= stats.binomtest(len(list_exceptions_np), test_size, p = 0.01)
test_except_np print(f'la p-value du test binomial est: {test_except_np.pvalue}')
la p-value du test binomial est: 0.009043369613659823
La pvalue du test est inférieure au seuil de 5%. On rejette donc l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%. Nous concluons donc que notre VaR n’est pas satisfaisante.
= np.mean([r for r in train_close['log_return'] if r<var_hist])
ES_np print(f"L'Expected Shortfall associée à la VaR calculée est: {ES_np}")
L'Expected Shortfall associée à la VaR calculée est: -0.054387464763168906
VaR historique Bootstrap
Pour la VaR historique boostrap, nous allons construire B réplications bootstrap de la série des log rendements. Pour chaque réplication(b), nous allons calculer la VaR historique. La VaR historique bootstrap est alors donnée par la moyenne des VaR historiques des B réplications bootstrap.
\(\hat{VaR}_{h,bootstrap}(\alpha) = \frac{1}{B} \sum_{b=1}^{B} \hat{VaR}_{h,b}(\alpha)\)
Nous avons aussi calculé l’intervalle de confiance à 95% de la VaR historique bootstrap. Cet intervalle est donné par les quantiles empiriques d’ordre 2.5% et 97.5% des VaR historiques des B réplications bootstrap.
def VaR_Hist_Bootstrap(returns, seuil, num_simulations, alpha_IC, n_B):
= np.zeros(num_simulations)
VaRs_boot
for i in range(num_simulations):
= np.random.choice(returns, n_B, replace=True)
sample = hist_var(sample, len(sample), len(sample), seuil)
VaRs_boot[i]
= np.mean(VaRs_boot)
VaR
= np.percentile(VaRs_boot, 100 * (1-alpha_IC) / 2)
lower_bound = np.percentile(VaRs_boot, 100 * (1 - (1-alpha_IC) / 2))
upper_bound = (lower_bound, upper_bound)
IC
return VaR, IC
= 0.99
seuil = 0.9
seuil_IC = 5000
num_simulations = 251*10 #on utilise 10 ans de données comme taille d'échantillon bootstrap
n_B = VaR_Hist_Bootstrap(train_close["log_return"], seuil, num_simulations,
var_hist_boot, IC_hist_boot
seuil_IC, n_B) print(f"La VaR historique bootstrap: {var_hist_boot}")
print(f"L'intervalle de confiance associé est: {IC_hist_boot}")
La VaR historique bootstrap: -0.03986492710978063
L'intervalle de confiance associé est: (-0.04339186830234299, -0.036106484963320654)
Notre estimation bootstrap de la VaR se trouve bien dans l’intervalle de confiance à 90%. Nous pouvons donc conclure que notre estimation de la VaR est satisfaisante.
Backtesting
=(12, 8))
plt.figure(figsize0:train_size], train_close['log_return'], label="historical train log returns", color = 'gray')
plt.plot(ts_close.index['log_return'], label="historical test log returns", color = 'blue')
plt.plot(ts_close.index[train_size:], test_close[for i in range(test_size)], label="Non parametric Bootstrap VaR", color = 'red')
plt.plot(ts_close.index[train_size:], [var_hist_boot = [i for i in range(len(test_close['log_return'])) if test_close['log_return'][i]<var_hist_boot]
list_exceptions_np_boot 'log_return'][list_exceptions_np_boot], color='red', label='Exceptions')
plt.scatter(test_close.index[list_exceptions_np_boot], test_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
print(f"Le nombre d'exceptions pour la VaR Bootstrap non paramétrique est: {len(list_exceptions_np_boot)}")
print(f"Le pourcentage d'exceptions pour la VaR Bootstrap non paramétrique est: {len(list_exceptions_np_boot)/test_size}")
Le nombre d'exceptions pour la VaR Bootstrap non paramétrique est: 16
Le pourcentage d'exceptions pour la VaR Bootstrap non paramétrique est: 0.007383479464697739
= stats.binomtest(len(list_exceptions_np_boot), test_size, p = 0.01)
test_except_np_boot print(f'la p-value du test binomial est: {test_except_np_boot.pvalue}')
la p-value du test binomial est: 0.2783933759125071
Le test binomial vient confirmer les conclusions faites à partir de l’intervalle de confiance estimé.
= np.mean([r for r in train_close['log_return'] if r<var_hist_boot])
ES_np_boot print(f"L'Expected Shortfall associée à la VaR calculée est: {ES_np_boot}")
L'Expected Shortfall associée à la VaR calculée est: -0.05028777316523479
Méthodes paramétriques d’estimation de la VaR
VaR gaussienne
La VaR gaussienne suppose que les rendements suivent une loi normale. Dans ce cas, on a \(P(R<VaR_h(\alpha)) = 1-\alpha\) qui est équivalent à \(P(\frac{R-\mu}{\sigma} < \frac{VaR_h(\alpha)-\mu}{\sigma}) = 1-\alpha\) en supposant que R suit une loi normale de moyenne \(\mu\) et d’écart type \(\sigma\). On a alors \(VaR_h(\alpha) = \mu + \sigma \Phi^{-1}(1-\alpha)\). On peut estimer les paramètres à partir de l’échantillon. On a alors \(\hat{VaR}_h(\alpha) = \hat{\mu} + \hat{\sigma} \Phi^{-1}(1-\alpha)\).
from scipy import stats
def var_gaussienne(returns, index, fenetre, seuil):
= returns[index-fenetre:index]
rendements = np.mean(rendements)
mean_returns = np.std(rendements)
sd_returns = mean_returns + sd_returns * stats.norm.ppf(1-seuil)
VaR return VaR
## VaR gaussienne sur base d'apprentissage
= var_gaussienne(train_close["log_return"], train_size, 251*10, 0.99) #on utilise 10 ans comme fenêtre
var_gaus print(var_gaus)
-0.034948710985183845
Validation
# analyse graphique avec les densités des distributions
= (12,8))
plt.figure(figsize "log_return"], bins=30, density=True, color='blue', label = 'log rendements')
plt.hist(train_close[='red', linestyle='dashed', linewidth=2, label=f'VaR Gaussienne ({var_gaus})')
plt.axvline(var_gaus, color
= plt.xlim()
xmin, xmax = np.linspace(xmin, xmax, 100)
x = stats.norm.pdf(x, np.mean(train_close["log_return"]), np.std(train_close["log_return"]))
p = 'Loi normale', linewidth=2)
plt.plot(x, p, label
plt.legend()
plt.tight_layout() plt.show()
On remarque que la queue de la loi normale n’est pas assez lourde pour nos données. La loi normale aura donc tendance à mal estimer les queues de distribution.
## Analyse graphique avec le QQ-plot
=(12, 8))
plt.figure(figsize= stats.probplot(train_close["log_return"],
probplot = (np.mean(train_close["log_return"]), np.std(train_close["log_return"])),
sparams ='norm', plot=plt)
dist0][0], probplot[0][0], color='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(probplot['QQ-plot') plt.title(
Text(0.5, 1.0, 'QQ-plot')
Nous constatons que lorsqu’on utilise la loi normale pour la modélisation de la VaR, au niveau des queues de distribution,les quantiles théoriques sont moins élévés que les quantiles empiriques à gauche et plus élevés à droite. Cela signifie que la loi normale sous-estime la probabilité d’exception. La loi normale semble donc ne pas être adaptée pour modéliser la VaR.
## Test d'adéquation
# Test de Kolmogorov-Smirnov
= stats.kstest(train_close["log_return"], 'norm', args = (np.mean(train_close["log_return"]), np.std(train_close["log_return"])))
ks_statistic, ks_p_value print(f"Test de Kolmogorov-Smirnov - Statistique : {ks_statistic},\nP-value : {ks_p_value}")
Test de Kolmogorov-Smirnov - Statistique : 0.05703854924225127,
P-value : 7.95942063378766e-19
On rejette l’hypothèse nulle selon laquelle les log rendements suivent une distribution normale.
Représentation graphique
=(12, 8))
plt.figure(figsize0:train_size], train_close['log_return'], label="historical train log returns", color = 'gray')
plt.plot(ts_close.index[= ts_close.index[train_size-251*10-1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
plt.axvline(x 'log_return'], label="historical test log returns", color = 'blue')
plt.plot(ts_close.index[train_size:], test_close[for i in range(test_size)], label="gaussian VaR", color = 'red')
plt.plot(ts_close.index[train_size:], [var_gaus = [i for i in range(len(test_close['log_return'])) if test_close['log_return'][i]<var_gaus]
list_exceptions_gaus 'log_return'][list_exceptions_gaus], color='red', label='Exceptions')
plt.scatter(test_close.index[list_exceptions_gaus], test_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
Analyse des exceptions
print(f"Le nombre d'exceptions pour la VaR gaussienne est: {len(list_exceptions_gaus)}")
print(f"Le pourcentage d'exceptions pour la VaR gaussienne est: {len(list_exceptions_gaus)/test_size}")
Le nombre d'exceptions pour la VaR gaussienne est: 24
Le pourcentage d'exceptions pour la VaR gaussienne est: 0.011075219197046609
= stats.binomtest(len(list_exceptions_gaus), test_size, p = 0.01)
test_except_gaus print(f'la p-value du test binomial est: {test_except_gaus.pvalue}')
la p-value du test binomial est: 0.5883566372303766
La pvalue du test est supérieure au seuil de 5%. On ne peut donc pas rejeter l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%.
VaR gaussienne à 10 jours
## Var gaussienne à 10 jours par la méthode de scaling
= 10
periode = np.sqrt(periode)*var_gaus
var_gaus_scaling print(f"La VaR gaussienne à 10 jours par la méthode de scaling est: {var_gaus_scaling}")
La VaR gaussienne à 10 jours par la méthode de scaling est: -0.11051752800012811
## VaR gaussienne à 10 jours par la méthode de diffusion
from numpy import random
= 10
periode = 10000
n_simul = train_close['Close'].iloc[-1]
S0 = np.mean(train_close["log_return"])
mean_returns = np.std(train_close["log_return"])
sd_returns = []
simulations for k in range(n_simul):
=[S0]
simul_kfor _ in range(periode):
= random.standard_normal()
Z = simul_k[-1]*mean_returns + simul_k[-1]*sd_returns*Z
dS -1]+dS)
simul_k.append(simul_k[
simulations.append(simul_k)
= [np.log(simul[10] / S0) for simul in simulations]
rend10
= 0.99
alpha = np.percentile(rend10, 100*(1-alpha))
var_gaus_diff print(f"La VaR gaussienne à 10 jours par la méthode de diffusion est: {var_gaus_diff}")
La VaR gaussienne à 10 jours par la méthode de diffusion est: -0.10788230585355442
VaR gaussienne pondérée :
Une façon de corriger la VaR, est de pondérer la moyenne et l’écart type des rendements. On peut utiliser une moyenne mobile pondérée.
from scipy import stats
def var_gaussienne_ewma(returns, index, fenetre, seuil, lambd):
= returns[index-fenetre:index]
rendements = len(rendements)
n = [(lambd**i)*(1-lambd) for i in range(n)]
poids = sum(poids)
denom = [poid/denom for poid in poids]
poids_pond = np.sum([poids_pond[i]*rendements[n-i-1] for i in range(n)])
moy_pond = np.sum([poids_pond[i]*(rendements[n-i-1]-moy_pond)**2 for i in range(n)])
variance_pond = moy_pond + np.sqrt(variance_pond) * stats.norm.ppf(1-seuil)
VaR return moy_pond, variance_pond, VaR
## Calcul
= 0.9
lambd1= 0.95
lambd2 = 0.99
lambd3 = var_gaussienne_ewma(train_close["log_return"], train_size, 251*10, 0.99, lambd1)
moy_pond1, variance_pond1, VaR1 = var_gaussienne_ewma(train_close["log_return"], train_size, 251*10, 0.99, lambd2)
moy_pond2, variance_pond2, VaR2 = var_gaussienne_ewma(train_close["log_return"], train_size, 251*10, 0.99, lambd3)
moy_pond3, variance_pond3, VaR3
= [i for i in range(len(test_close['log_return'])) if test_close['log_return'][i]<VaR1]
list_exceptions_gaus1 print(100*"_"+ f"\nlambda = {lambd1}\nMoyenne pondérée: {moy_pond1}\nVariance pondérée: {variance_pond1}\nVaR gaussienne EWMA: {VaR1}\nNombre d'exceptions: {len(list_exceptions_gaus1)}\nPourcentage d'exceptions: {len(list_exceptions_gaus1)/test_size}")
= plt.subplots(1, 3, figsize=(15, 5))
fig, axes
# Premier graphique
0].plot(ts_close.index[0:train_size], train_close['log_return'], label="Historical train log returns", color='gray')
axes[0].axvline(x=ts_close.index[train_size - 251*10 - 1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
axes[0].plot(ts_close.index[train_size:], test_close['log_return'], label="Historical test log returns", color='blue')
axes[0].plot(ts_close.index[train_size:], [VaR1 for _ in range(test_size)], label=f"Gaussian EWMA VaR (lambda= {lambd1})", color='red')
axes[0].scatter(test_close.index[list_exceptions_gaus1], [test_close['log_return'][i] for i in list_exceptions_gaus1], color='red', label='Exceptions')
axes[0].set_title('Lambda = 0.9')
axes[0].legend()
axes[
= [i for i in range(len(test_close['log_return'])) if test_close['log_return'][i]<VaR2]
list_exceptions_gaus2 print(100*"_"+ f"\nlambda = {lambd2}\nMoyenne pondérée: {moy_pond2}\nVariance pondérée: {variance_pond2}\nVaR gaussienne EWMA: {VaR2}\nNombre d'exceptions: {len(list_exceptions_gaus2)}\nPourcentage d'exceptions: {len(list_exceptions_gaus2)/test_size}")
# Deuxième graphique
1].plot(ts_close.index[0:train_size], train_close['log_return'], label="Historical train log returns", color='gray')
axes[1].axvline(x=ts_close.index[train_size - 251*10 - 1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
axes[1].plot(ts_close.index[train_size:], test_close['log_return'], label="Historical test log returns", color='blue')
axes[1].plot(ts_close.index[train_size:], [VaR2 for _ in range(test_size)], label=f"Gaussian EWMA VaR (lambda= {lambd2})", color='red')
axes[1].scatter(test_close.index[list_exceptions_gaus2], [test_close['log_return'][i] for i in list_exceptions_gaus2], color='red', label='Exceptions')
axes[1].set_title('Lambda = 0.95')
axes[1].legend()
axes[
= [i for i in range(len(test_close['log_return'])) if test_close['log_return'][i]<VaR3]
list_exceptions_gaus3 print(100*"_"+ f"\nlambda = {lambd3}\nMoyenne pondérée: {moy_pond3}\nVariance pondérée: {variance_pond3}\nVaR gaussienne EWMA: {VaR3}\nNombre d'exceptions: {len(list_exceptions_gaus3)}\nPourcentage d'exceptions: {len(list_exceptions_gaus3)/test_size}")
# Troisième graphique
2].plot(ts_close.index[0:train_size], train_close['log_return'], label="Historical train log returns", color='gray')
axes[2].axvline(x=ts_close.index[train_size - 251*10 - 1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
axes[2].plot(ts_close.index[train_size:], test_close['log_return'], label="Historical test log returns", color='blue')
axes[2].plot(ts_close.index[train_size:], [VaR3 for _ in range(test_size)], label=f"Gaussian EWMA VaR (lambda= {lambd3})", color='red')
axes[2].scatter(test_close.index[list_exceptions_gaus3], [test_close['log_return'][i] for i in list_exceptions_gaus3], color='red', label='Exceptions')
axes[2].set_title('Lambda = 0.99')
axes[2].legend()
axes[
plt.tight_layout() plt.show()
____________________________________________________________________________________________________
lambda = 0.9
Moyenne pondérée: 0.003249804834415138
Variance pondérée: 0.00010705135281489561
VaR gaussienne EWMA: -0.020819898531762165
Nombre d'exceptions: 67
Pourcentage d'exceptions: 0.03091832025842178
____________________________________________________________________________________________________
lambda = 0.95
Moyenne pondérée: 0.0022669059482574804
Variance pondérée: 0.00018626179534529804
VaR gaussienne EWMA: -0.029482569211930626
Nombre d'exceptions: 35
Pourcentage d'exceptions: 0.016151361329026302
____________________________________________________________________________________________________
lambda = 0.99
Moyenne pondérée: 0.0004258509321423379
Variance pondérée: 0.00022231763612603746
VaR gaussienne EWMA: -0.034260739803697746
Nombre d'exceptions: 26
Pourcentage d'exceptions: 0.011998154130133826
= stats.binomtest(len(list_exceptions_gaus1), test_size, p = 0.01)
test_except_gaus1 print(f'la p-value du test binomial est: {test_except_gaus1.pvalue}')
la p-value du test binomial est: 3.0751996180426066e-15
= stats.binomtest(len(list_exceptions_gaus2), test_size, p = 0.01)
test_except_gaus2 print(f'la p-value du test binomial est: {test_except_gaus2.pvalue}')
la p-value du test binomial est: 0.006672492499143829
= stats.binomtest(len(list_exceptions_gaus3), test_size, p = 0.01)
test_except_gaus3 print(f'la p-value du test binomial est: {test_except_gaus3.pvalue}')
la p-value du test binomial est: 0.3304256909292766
Seule la VaR estimée avec \(\lambda\) = 0.99 a une p value supérieure au seuil de 5%.
VaR dynamique :
Une autre façon de corriger la VaR est de la modéliser par un modèle dynamique. Ici on a pas besoin de diviser notre échantillon en base d’apprentissage et de test. Il suffit de choisir une fenêtre de temps pour estimer notre VaR. Ici nous allons choisir (251 jours) soit un an en considérant les jours ouvrés. Puis nous allons la faire glisser de 1 jour en 1 jour et la backtester le lendemain
La fonction hist_var écrite pour la VaR non paramétrique a été conçue pour pouvoir également calculer la VaR dynamique. Il suffit pour cela de faire varier l’index en paramètre.
= list(map(lambda index: hist_var(list(ts_close['log_return']), index, 251, 0.99), range(251, len(ts_close['log_return'])))) #on suppose qu'une année correspond à 251 jours ouvrés vars_dyn
Réprésentation graphique
=(12, 8))
plt.figure(figsize'log_return'], label="historical log returns", color = 'blue')
plt.plot(ts_close.index, ts_close[251:], vars_dyn, label='VaR dynamique', color = 'red')
plt.plot(ts_close.index[= [i for i in range(251, len(ts_close['log_return'])) if ts_close['log_return'][i]<vars_dyn[i-251]]
list_exceptions_dyn 'log_return'][list_exceptions_dyn], color='red', label='Exceptions')
plt.scatter(ts_close.index[list_exceptions_dyn], ts_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
print(f"Le nombre d'exceptions pour la VaR dynamique est: {len(list_exceptions_dyn)}")
print(f"Le pourcentage d'exceptions pour la VaR dynamique est: {len(list_exceptions_dyn)/len(vars_dyn)}")
Le nombre d'exceptions pour la VaR dynamique est: 133
Le pourcentage d'exceptions pour la VaR dynamique est: 0.01580135440180587
from scipy import stats
= stats.binomtest(len(list_exceptions_dyn), len(vars_dyn), p = 0.01)
test_except_dyn print(f'la p-value du test binomial est: {test_except_dyn.pvalue}')
la p-value du test binomial est: 7.255952037114441e-07
La p-value du test binomial est en dessous du seuil de 5%. On rejette donc l’hypothèse selon laquelle la probabilité d’exception est de 1%. Le modèle VaR estimé n’est pas satisfaisant.
VaR skew-Student :
La loi student a pour avantage de prendre en compte les queues de distribution des rendements. Dans cette partie, nous allons supposer que les rendements suivent une loi skew-student généralisée. Nous allons estimer les paramètres de la loi skew-student à partir de la série des log rendements par maximum de vraisemblance. Enfin, nous allons calculer la VaR à partir des paramètres estimés.
fonction de densité de la skew-student
from scipy import special
def SkStudentPdf(x:float, mu, sigma, gamma, nu):
= gamma*(x-mu)*np.sqrt((nu+1)/(((x-mu)/sigma)**2+nu))/sigma
point return 2*stats.t.pdf(x, df = nu, loc = mu, scale=sigma)*stats.t.cdf(point, nu+1)
= -0.1
mu = 0.1
sigma= 1
gamma = 4
nu 0.5, mu, sigma, gamma, nu) SkStudentPdf(
0.022681128494391908
Fonction de log vraisemblance
def minus_log_vrais(params, returns):
= params
mu, sigma, gamma, nu = SkStudentPdf(returns, mu, sigma, gamma, nu)
densities_values = [np.log(value) for value in densities_values]
log_densities_values return -np.sum(log_densities_values)
fonction d’optimisation
# Fonction d'optimisation
from scipy.optimize import minimize
def optim_func(data):
# Estimer les paramètres par maximum de vraisemblance
= [np.mean(data), np.std(data), 1, 4]
initial_guess = minimize(minus_log_vrais, initial_guess, args=(data,), method='Nelder-Mead')
result
# Obtenez les paramètres estimés
= result.x
est_params
return est_params
Estimation des paramètres sur base d’apprentissage
= optim_func(train_close['log_return'])
est_params = est_params
mu_est, sigma_est, gamma_est, nu_est print(f"La valeur estimée des paramètres est:\nmu: {mu_est}\nsigma: {sigma_est}\ngamma: {gamma_est}\nnu: {nu_est}")
La valeur estimée des paramètres est:
mu: 0.0024085659232927596
sigma: 0.01024400244829116
gamma: -0.2295168624277934
nu: 3.9899826715655733
Le code ci-dessus a consisté à construire la densité de la skew-student, la fonction de la log vraisemblance et la fonction d’optimisation. Nous avons ensuite estimé les paramètres de la skew-student sur la base d’apprentissage. Nous pouvons évaluer la qualité de notre estimation par des analyses graphiques avant de passer à la modélisation de la VaR.
Comparaison entre densité théorique et densité empirique de la skew-student sur les log rendements
= np.linspace(min(test_close['log_return']), max(test_close['log_return']), 1000)
x_values
=est_params
mu, sigma, skew, df
= SkStudentPdf(x_values, mu, sigma, skew, df)
theoretical_density = (10,8))
plt.figure(figsize 'log_return'], bins=30, density=True, alpha=0.5, label='Données empiriques')
plt.hist(test_close[
='Densité Skew Student', color='red')
plt.plot(x_values, theoretical_density, label
# Personnalisation du graphique
'Rendements')
plt.xlabel('Densité')
plt.ylabel('Comparaison entre les données et la densité théorique')
plt.title(
plt.legend()
# Affichage du graphique
plt.show()
Le graphique ci-dessus est satisfaisant. La densité théorique de la skew-student est très proche de la densité empirique des log rendements. Nous pouvons donc conclure que la skew-student est une bonne modélisation des log rendements.
Fonction de repartition de la skew-student et fonction quantile
## Intégration de la fonction de densité
from scipy import integrate
from scipy.optimize import minimize_scalar
def integrale_SkewStudent(x):
= -np.inf
borne_inf = integrate.quad(lambda x: SkStudentPdf(x, mu_est, sigma_est, gamma_est, nu_est), borne_inf, x)
resultat_integration, erreur return resultat_integration
def fonc_minimize(x, alpha):
= integrale_SkewStudent(x)-alpha
value return abs(value)
def theoretical_quantile(alpha):
if alpha <0 or alpha >1:
raise Exception("Veuillez entrer un niveau alpha entre 0 et 1")
else:
= minimize_scalar(lambda x: fonc_minimize(x, alpha))
resultat_minimisation return resultat_minimisation.x
Le code ci-dessus nous a permis de construire la fonction de repartition de la skew-student ainsi que la fonction quantile qui est l’inverse de cette fonction de repartition. Cette fonction quantile est le coeur de la modélisation de la VaR.
QQ plot :
Le graphique de QQ plot nous permettra de discuter de la qualité d’ajustement de la loi skew-student à la série des log rendements.
= np.arange(0.001, 1, 0.001)
niveaux_quantiles
= np.quantile(train_close['log_return'], niveaux_quantiles)
quantiles_empiriques = [theoretical_quantile(alpha) for alpha in niveaux_quantiles] quantiles_theoriques
# Créer le QQ plot
=(8, 8))
plt.figure(figsize
plt.scatter(quantiles_theoriques, quantiles_empiriques)='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(quantiles_theoriques, quantiles_theoriques, color'QQ Plot - Quantiles empiriques vs Quantiles théoriques')
plt.title('Quantiles théoriques (distribution Skew Student)')
plt.xlabel('Quantiles empiriques')
plt.ylabel(True)
plt.grid( plt.show()
Nous constatons que la loi skew-student est une bonne approximation de la distribution des log rendements. Même si on constate des écarts aux queues de distribution, la loi skew-student semble bien modéliser la distribution des log rendements.
comparaison entre loi gaussienne et loi de Skew Student
=(12, 8))
plt.figure(figsize1, 2, 1)
plt.subplot(= stats.probplot(train_close["log_return"],
probplot = (np.mean(train_close["log_return"]), np.std(train_close["log_return"])),
sparams ='norm', plot=plt)
dist0][0], probplot[0][0], color='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(probplot['Quantiles théoriques (distribution loi normale)')
plt.xlabel('Quantiles empiriques')
plt.ylabel("QQ-plot d'une modélisation par loi normale")
plt.title(True)
plt.grid(
1, 2, 2)
plt.subplot(
plt.scatter(quantiles_theoriques, quantiles_empiriques)='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(quantiles_theoriques, quantiles_theoriques, color"QQ Plot d'une modélisation par loi Skew Student")
plt.title('Quantiles théoriques (distribution Skew Student)')
plt.xlabel('Quantiles empiriques')
plt.ylabel(True)
plt.grid(
plt.tight_layout() plt.show()
Comme on peut le voir sur le graphique ci-dessus, la loi skew-student est une meilleure approximation de la distribution des log rendements que la loi normale. En effet, les écarts aux queues de distribution sont moins importants pour la loi skew-student que pour la loi normale.
Calcul de la VaR Skew Student
def var_skewstudent(returns, index, fenetre, seuil):
= returns[index-fenetre:index]
rendements = np.mean(rendements)
mean_returns = np.std(rendements)
sd_returns = theoretical_quantile(1-seuil)
VaR return VaR
## VaR Skew Student sur base d'apprentissage
= var_skewstudent(train_close["log_return"], train_size, train_size, 0.99) #on utilise 10 ans comme fenêtre
var_skew print(var_skew)
-0.03966428515565673
=(12, 8))
plt.figure(figsize0:train_size], train_close['log_return'], label="historical train log returns", color = 'gray')
plt.plot(ts_close.index[= ts_close.index[train_size-251*10-1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
plt.axvline(x 'log_return'], label="historical test log returns", color = 'blue')
plt.plot(ts_close.index[train_size:], test_close[for i in range(test_size)], label="VaR Skew Student", color = 'red')
plt.plot(ts_close.index[train_size:], [var_skew = [i for i in range(len(test_close['log_return'])) if test_close['log_return'][i]<var_skew]
list_exceptions_skew 'log_return'][list_exceptions_skew], color='red', label='Exceptions')
plt.scatter(test_close.index[list_exceptions_skew], test_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
print(f"Le nombre d'exceptions pour la VaR Skew student est: {len(list_exceptions_skew)}")
print(f"Le pourcentage d'exceptions pour la VaR Skew Student est: {len(list_exceptions_skew)/test_size}")
Le nombre d'exceptions pour la VaR Skew student est: 16
Le pourcentage d'exceptions pour la VaR Skew Student est: 0.007383479464697739
= stats.binomtest(len(list_exceptions_skew), test_size, p = 0.01)
test_except_skew print(f'la p-value du test binomial est: {test_except_skew.pvalue}')
la p-value du test binomial est: 0.2783933759125071
La p-value du test binomial est au dessus du seuil de 5%. On ne peut donc pas rejeter l’hypothèse selon laquelle la probabilité d’exception est de 1%. Le modèle VaR Skew Student estimé semble donc satisfaisant.
VaR TVE: Approche maxima par bloc
Construction de l’échantillon de maxima
La TVE s’intéresse aux queues de distribution droite (maximum, excès par valeurs supérieures). On commence donc par convertir nos données en prenant l’opposé des données
= [-rtrn for rtrn in train_close["log_return"]] minus_train_returns
def construct_max_data(returns, block_size):
= len(returns)
n = n // block_size
num_blocks
= []
maxima_sample
for i in range(num_blocks):
= i * block_size
block_start = (i + 1) * block_size
block_end
= returns[block_start:block_end]
block_data
= np.max(block_data)
max_in_block
maxima_sample.append(max_in_block)
return maxima_sample
= 21 # nombre de jours ouvrés dans le mois
s= construct_max_data(minus_train_returns, s)
maxima_train_sample
=(10, 8))
plt.figure(figsize= 'blue')
plt.plot(maxima_train_sample ,color "historical maxima of log returns")
plt.title('Value')
plt.ylabel(
plt.plot() plt.show()
Gumbel Plot
= []
quantiles_theoriques_gumbel for i in range(1,len(maxima_train_sample)+1):
= -np.log(-np.log((i-0.5)/len(maxima_train_sample)))
val
quantiles_theoriques_gumbel.append(val)
# Tracer le Gumbel plot
=(10, 8))
plt.figure(figsize='o')
plt.scatter(quantiles_theoriques_gumbel, np.sort(maxima_train_sample), marker'Gumbel Plot')
plt.title('Quantiles théoriques de Gumbel')
plt.xlabel('Quantiles observés')
plt.ylabel( plt.show()
Le Gumbel plot semble relativement linéaire. Ainsi il semble que l’hypothèse selon laquelle \(\xi\) est nul soit acceptable. La distribution adaptée à nos données semble donc être une distribution de Gumbel.
from scipy.stats import genextreme as gev
from scipy.stats import gumbel_r
= gev.fit(maxima_train_sample)
params_gev = gumbel_r.fit(maxima_train_sample)
params_gumbel
# Afficher les paramètres estimés
print("Paramètres estimés de la distribution GEV:")
print("Forme (xi) =", params_gev[0])
print("Localisation (mu) =", params_gev[1])
print("Echelle (sigma) =", params_gev[2])
print(150*"-")
print("Paramètres estimés de la distribution de Gumbel (xi nul):")
print("Localisation (mu) =", params_gumbel[0])
print("Echelle (sigma) =", params_gumbel[1])
Paramètres estimés de la distribution GEV:
Forme (xi) = -0.7718190267913189
Localisation (mu) = 0.01718333713722632
Echelle (sigma) = 0.011341548066836963
------------------------------------------------------------------------------------------------------------------------------------------------------
Paramètres estimés de la distribution de Gumbel (xi nul):
Localisation (mu) = 0.01878066648242991
Echelle (sigma) = 0.009390992075910511
=(10, 8))
plt.figure(figsize=30, density=True, label='Maxima observées')
plt.hist(maxima_train_sample, bins
= plt.xlim()
xmin, xmax = np.linspace(xmin, xmax, 100)
x
# Densité GEV ajustée
= gev.pdf(x, *params_gev)
p_gev 'k', linewidth=2, label='GEV ajustée')
plt.plot(x, p_gev,
# Densité Gumbel ajustée
= gumbel_r.pdf(x, *params_gumbel)
p_gumbel 'r', linewidth=2, label='Gumbel ajustée')
plt.plot(x, p_gumbel,
= "Comparaison GEV vs Gumbel"
title
plt.title(title)
plt.legend() plt.show()
On remarque la loi GEV (\(\xi\) non nul) estimée a une queue plus lourde que la loi de Gumbel (\(\xi\) nul) estimée. Toutefois, de manière générale, la distribution de Gumbel estimée semble plus coller à l’histogramme représenté.
= np.arange(0.001,1, 0.001)
niveaux_quantiles = np.quantile(maxima_train_sample, niveaux_quantiles)
quantiles_empiriques_TVE = gev.ppf(niveaux_quantiles, params_gev[0], loc = params_gev[1], scale = params_gev[2])
quantiles_theoriques_GEV = gumbel_r.ppf(niveaux_quantiles, *params_gumbel) quantiles_theoriques_Gumb
=(12, 8))
plt.figure(figsize1, 2, 1)
plt.subplot(
plt.scatter(quantiles_theoriques_GEV, quantiles_empiriques_TVE)='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(quantiles_theoriques_GEV, quantiles_theoriques_GEV, color"QQ Plot d'une modélisation par loi GEV")
plt.title('Quantiles théoriques (Loi GEV)')
plt.xlabel('Quantiles empiriques')
plt.ylabel(True)
plt.grid(
1, 2, 2)
plt.subplot(
plt.scatter(quantiles_theoriques_Gumb, quantiles_empiriques_TVE)='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(quantiles_theoriques_Gumb, quantiles_theoriques_Gumb, color"QQ Plot d'une modélisation par loi Gumbel")
plt.title('Quantiles théoriques (Loi Gumbel)')
plt.xlabel('Quantiles empiriques')
plt.ylabel(True)
plt.grid(
plt.tight_layout() plt.show()
La modélisation par loi de Gumbel semble beaucoup mieux correspondre aux données. Cette distribution semble également satisfaisante au vu de l’adéquation avec les quantiles empiriques. Nous utilisons donc cette loi pour la suite.
Calcul de la VAR
= 0.99
alpha = 1-s*(1-alpha)
alpha_bm = -gumbel_r.ppf(alpha_bm, *params_gumbel)
var_TVE_MB print("VaR TVE par maxima par blocs : ", var_TVE_MB)
VaR TVE par maxima par blocs : -0.03235159588828838
Backtesting
=(12, 8))
plt.figure(figsize0:train_size], train_close['log_return'], label="historical train log returns", color = 'gray')
plt.plot(ts_close.index['log_return'], label="historical test log returns", color = 'blue')
plt.plot(ts_close.index[train_size:], test_close[for i in range(test_size)], label="VaR TVE MB", color = 'red')
plt.plot(ts_close.index[train_size:], [var_TVE_MB = [i for i in range(len(test_close['log_return'])) if test_close['log_return'].iloc[i]<var_TVE_MB]
list_exceptions_TVE_MB 'log_return'].iloc[list_exceptions_TVE_MB], color='red', label='Exceptions')
plt.scatter(test_close.index[list_exceptions_TVE_MB], test_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
print(f"Le nombre d'exceptions pour la VaR TVE maxima par blocs est: {len(list_exceptions_TVE_MB)}")
print(f"Le pourcentage d'exceptions pour la VaR TVE maxima par blocs est: {len(list_exceptions_TVE_MB)/test_size}")
Le nombre d'exceptions pour la VaR TVE maxima par blocs est: 31
Le pourcentage d'exceptions pour la VaR TVE maxima par blocs est: 0.01430549146285187
= stats.binomtest(len(list_exceptions_TVE_MB), test_size, p = 0.01)
test_except_TVE_MB print(f'la p-value du test binomial est: {test_except_TVE_MB.pvalue}')
la p-value du test binomial est: 0.051029877183224145
La p-value du test binomial est en dessous du seuil de 5%. On rejette donc l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%. Le modèle VaR TVE MB estimé ne semble donc pas satisfaisant.
VAR TVE: Approche Peak over threshold
Fonction pour le Mean Excess Plot
def mean_excess_plot(returns):
=np.sort(returns)
sorted_returns = np.linspace(np.min(sorted_returns), np.max(sorted_returns), num = 1000)
listeu = listeu[listeu < np.percentile(listeu, 80)] ## Retirer les valeurs trop extrêmes
listeu = []
mean_excess =len(sorted_returns)
n
for u in listeu:
if u > 0:
= []
terms =0
NUfor i in range(n):
= sorted_returns[i]-u
term max(term,0))
terms.append(if term >0:
= NU + 1
NU try:
1/NU) * np.sum(terms))
mean_excess.append( (except:
= listeu[listeu!=u]
listeu else:
= listeu[listeu!=u]
listeu
= (10,8))
plt.figure(figsize
plt.plot(listeu, mean_excess)'Mean excess Plot')
plt.title('Moyenne des excès empirique')
plt.ylabel('Seuils')
plt.xlabel(
plt.plot()
Détermination de u par analyse graphique
mean_excess_plot(minus_train_returns)=0.009, color='red')
plt.axvline(x=0.031, color='red')
plt.axvline(x plt.show()
Par analyse graphique, nous pouvons garder u = 0.03
Estimation des paramètres de la loi GPD
= 0.03
u = [value - u for value in minus_train_returns if value >= u] excess_values
from scipy.stats import genpareto
= genpareto.fit(excess_values)
params_gpd
# Afficher les paramètres estimés
print("Paramètres estimés de la distribution GPD:")
print("Forme (xi) =", params_gpd[0])
print("Localisation (mu) =", params_gpd[1])
print("Echelle (sigma) =", params_gpd[2])
Paramètres estimés de la distribution GPD:
Forme (xi) = 1.4010389110423458
Localisation (mu) = 0.00016228471050365933
Echelle (sigma) = 0.006463507758681829
Validation Ex ante
=(10, 8))
plt.figure(figsize=30, density=True, label='Données observées des excès')
plt.hist(excess_values, bins
= plt.xlim()
xmin, xmax = np.linspace(xmin, xmax, 100)
x
# Densité GPD ajustée
= genpareto.pdf(x, *params_gpd)
p_gpd 'r', linewidth=2, label='GPD ajustée')
plt.plot(x, p_gpd,
= "Distribution GPD"
title
plt.title(title)
plt.legend() plt.show()
Nous remarquons que la distribution GPD correspond relativement bien à notre distribution des excès.
= np.arange(0.01, 1, 0.01)
niveaux_quantiles = np.quantile(excess_values, niveaux_quantiles)
quantiles_empiriques_POT = genpareto.ppf(niveaux_quantiles, *params_gpd) quantiles_theoriques_GDP
=(10, 8))
plt.figure(figsize
plt.scatter(quantiles_theoriques_GDP, quantiles_empiriques_POT)#plt.plot(quantiles_theoriques_GDP, quantiles_theoriques_GDP, color='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
"QQ Plot d'une modélisation par loi GPD")
plt.title('Quantiles théoriques (Loi GPD)')
plt.xlabel('Quantiles empiriques')
plt.ylabel(True) plt.grid(
Le QQ plot estimé indique que les quantiles théoriques de la loi GPD sont beaucoup plus grands que les quantiles empiriques observés dans notre distribution des excès.
Calcul de la VAR
= 0.99
alpha = len([term for term in minus_train_returns if term-u>0])
NU = 1-len(minus_train_returns)*(1-alpha)/NU
alpha_pot = -genpareto.ppf(alpha_pot, *params_gpd)-u
var_TVE_POT print("VaR TVE Peak Over Threshold : ",var_TVE_POT)
VaR TVE Peak Over Threshold : -0.04029514148506361
Backtesting
=(12, 8))
plt.figure(figsize0:train_size], train_close['log_return'], label="historical train log returns", color = 'gray')
plt.plot(ts_close.index['log_return'], label="historical test log returns", color = 'blue')
plt.plot(ts_close.index[train_size:], test_close[for i in range(test_size)], label="VaR TVE POT", color = 'red')
plt.plot(ts_close.index[train_size:], [var_TVE_POT = [i for i in range(len(test_close['log_return'])) if test_close['log_return'].iloc[i]<var_TVE_POT]
list_exceptions_TVE_POT 'log_return'].iloc[list_exceptions_TVE_POT], color='red', label='Exceptions')
plt.scatter(test_close.index[list_exceptions_TVE_POT], test_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
print(f"Le nombre d'exceptions pour la VaR TVE Peak Over Threshold est: {len(list_exceptions_TVE_POT)}")
print(f"Le pourcentage d'exceptions pour la VaR TVE Peak Over Threshold est: {len(list_exceptions_TVE_POT)/test_size}")
Le nombre d'exceptions pour la VaR TVE Peak Over Threshold est: 14
Le pourcentage d'exceptions pour la VaR TVE Peak Over Threshold est: 0.006460544531610522
= stats.binomtest(len(list_exceptions_TVE_POT), test_size, p = 0.01)
test_except_TVE_POT print(f'la p-value du test binomial est: {test_except_TVE_POT.pvalue}')
la p-value du test binomial est: 0.10473377125459593
La p-value du test binomial est au dessus du seuil de 5%. On ne peut donc pas rejeter l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%. Le modèle VaR TVE MB estimé semble donc satisfaisant.
Comparaison des différentes approches
= {"Méthode" : ["VaR non paramétrique", "VaR Bootstrap non paramétrique", "VaR gaussienne", 'VaR Gaussienne EWMA (lambda =0.9)',
dict_comp 'VaR Gaussienne EWMA (lambda =0.95)','VaR Gaussienne EWMA (lambda =0.99)', "VaR dynamique", "VaR Skew Student",
"VaR TVE MB", "VaR TVE POT"],
"VaR": [var_hist, var_hist_boot, var_gaus, VaR1, VaR2, VaR3, None, var_skew, var_TVE_MB, var_TVE_POT],
"Nombre d'exceptions": [len(list_exceptions_np),len(list_exceptions_np_boot), len(list_exceptions_gaus),len(list_exceptions_gaus1),
len(list_exceptions_gaus2),len(list_exceptions_gaus3),len(list_exceptions_dyn),len(list_exceptions_skew),
len(list_exceptions_TVE_MB),len(list_exceptions_TVE_POT)],
"Taux d'exceptions": [len(list_exceptions_np)/test_size,len(list_exceptions_np_boot)/test_size, len(list_exceptions_gaus)/test_size,len(list_exceptions_gaus1)/test_size,
len(list_exceptions_gaus2)/test_size,len(list_exceptions_gaus3)/test_size,len(list_exceptions_dyn)/len(vars_dyn),len(list_exceptions_skew)/test_size,
len(list_exceptions_TVE_MB)/test_size,len(list_exceptions_TVE_POT)/test_size],
"P-value du test binomial": [round(test_except_np.pvalue,3),round(test_except_np_boot.pvalue,3), round(test_except_gaus.pvalue,3),
round(test_except_gaus1.pvalue,3),round(test_except_gaus2.pvalue,3), round(test_except_gaus3.pvalue,3),
round(test_except_dyn.pvalue,3), round(test_except_skew.pvalue,3),round(test_except_TVE_MB.pvalue,3), round(test_except_TVE_POT.pvalue,3)]}
pd.DataFrame(dict_comp)
Méthode | VaR | Nombre d'exceptions | Taux d'exceptions | P-value du test binomial | |
---|---|---|---|---|---|
0 | VaR non paramétrique | -0.043208 | 10 | 0.004615 | 0.009 |
1 | VaR Bootstrap non paramétrique | -0.039865 | 16 | 0.007383 | 0.278 |
2 | VaR gaussienne | -0.034949 | 24 | 0.011075 | 0.588 |
3 | VaR Gaussienne EWMA (lambda =0.9) | -0.020820 | 67 | 0.030918 | 0.000 |
4 | VaR Gaussienne EWMA (lambda =0.95) | -0.029483 | 35 | 0.016151 | 0.007 |
5 | VaR Gaussienne EWMA (lambda =0.99) | -0.034261 | 26 | 0.011998 | 0.330 |
6 | VaR dynamique | NaN | 133 | 0.015801 | 0.000 |
7 | VaR Skew Student | -0.039664 | 16 | 0.007383 | 0.278 |
8 | VaR TVE MB | -0.032352 | 31 | 0.014305 | 0.051 |
9 | VaR TVE POT | -0.040295 | 14 | 0.006461 | 0.105 |
Commentaires
Nous avons estimé jusque là 10 valeurs différentes de la VaR. Ces différentes valeurs estimées correspondent à des approches différentes. Nous avons à chaque fois estimer une VaR 99% à horizon 1 jour. Le taux d’exceptions attendu afin de juger de la bonne qualité de la VaR est de 1%. - La VaR dynamique est particulière car, comme son nom l’indique, elle est dynamique donc pas constante au cours du temps. Elle a plus d’exceptions car elle est testée sur plus de données que les autres VaRs. Toutefois son taux d’exceptions se rapproche de celui des autres VaRs. - Le test binomial fournit une p-value inférieure au seuil de 5% pour les VaRs non paramétrique, Gaussienne EWMA (\(\lambda\) = 0.9 et \(\lambda\) = 0.95), dynamique et TVE par maxima par blocs. Ce qui sous entend que le taux d’exception est statistiquement différent du taux attendu de 1%. On en déduit donc que les modèles utilisés sont peu adéquats pour nos données. - La VaR non paramétrique et les VaR Gaussiennes EWMA pour \(\lambda\) faible ont tendance à sous estimer la VaR tandis que la VaR dynamique et la VaR TVE par maxima par blocs ont tendance à sur estimer la vraie valeur de la VaR - De plus, la validation ex-ante de la VaR TVE POT nous indique une inadéquation entre les quantiles théoriques et les quantiles empiriques des excès. - Nous avons également montré que la VaR Skew Student semblait nettement plus valide que la VaR gaussienne. Toutefois, nous avons également estimé une amélioration de la VaR gaussienne qui est la VaR gaussienne pondérée par la méthode EWMA. - Les VARs qui nous semblent donc les plus pertinentes sont la VaR Bootstrap non paramétrique, la VaR gaussienne EWMA (\(\lambda\)=0.99) et la VaR Skew Student.
Calcul de la VaR en utilisant un modèle AR-GARCH
Dans les modélisations précédentes, nous avons supposé que les rendements avaient une moyenne constante au cours du temps, une volatilité constante et qu’elles sont non auto-correlées. Cependant, ces hypothèses ne sont pas vérifiées dans la réalité. En effet, les rendements connaissent des périodes relativement stable et des périodes de volatilité élevée. De plus, les rendements sont auto-corrélés. Pour prendre en compte ces caractéristiques, nous allons modéliser les rendements par un modèle AR-GARCH.
Nous savons déjà que la série des log rendements est stationnaire. De plus les fonctions d’autocorrélation et d’autocorrélation partielle sont exponentielles décroissantes et s’annulent après quelques retards. Cela nous conduit à envisager un modèle AR(p) avec p=1,2,3,4,5,6,7,8,9,10. Nous allons comparer les modèles AR(p) et choisir le modèle qui minimise le BIC.
# Construction des modèles AR(p) for p in [1,2,3,4,5] et choix du modèle avec le plus petit BIC
from statsmodels.tsa.ar_model import AutoReg
= {}
bic for p in range(1,6):
= AutoReg(train_close['log_return'],lags=p).fit()
model = model.bic
bic[p]
print(f"le modèle avec le plus petit BIC est le modèle AR({min(bic, key=bic.get)})")
le modèle avec le plus petit BIC est le modèle AR(1)
c:\Users\johns\AppData\Local\Programs\Python\Python312\Lib\site-packages\statsmodels\tsa\base\tsa_model.py:473: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
self._init_dates(dates, freq)
c:\Users\johns\AppData\Local\Programs\Python\Python312\Lib\site-packages\statsmodels\tsa\base\tsa_model.py:473: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
self._init_dates(dates, freq)
c:\Users\johns\AppData\Local\Programs\Python\Python312\Lib\site-packages\statsmodels\tsa\base\tsa_model.py:473: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
self._init_dates(dates, freq)
c:\Users\johns\AppData\Local\Programs\Python\Python312\Lib\site-packages\statsmodels\tsa\base\tsa_model.py:473: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
self._init_dates(dates, freq)
c:\Users\johns\AppData\Local\Programs\Python\Python312\Lib\site-packages\statsmodels\tsa\base\tsa_model.py:473: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
self._init_dates(dates, freq)
Le modèle AR(1) est celui qui a le plus petit BIC. Nous maintenant étudier l’adéquation de ce modèle.
Analyse des résidus
Nous allons allons vérifier à l’aide du test de portemanteau si les résidus sont des bruits blancs. Nous utiliserons le test de Ljung-Box. L’hypothèse nulle est qu’il n’existe pas d’autocorrélation dans les résidus.
# Test de Ljung-Box pour les résidus du modèle AR(1)
from statsmodels.stats.diagnostic import acorr_ljungbox
= AutoReg((train_close['log_return']-np.mean(train_close['log_return'])),lags=1).fit()
model = model.resid
residus = acorr_ljungbox(residus, lags=10)
result_lyung_box
result_lyung_boxprint(model.summary())
AutoReg Model Results
==============================================================================
Dep. Variable: log_return No. Observations: 6501
Model: AutoReg(1) Log Likelihood 18500.695
Method: Conditional MLE S.D. of innovations 0.014
Date: Wed, 24 Apr 2024 AIC -36995.390
Time: 01:52:09 BIC -36975.052
Sample: 1 HQIC -36988.356
6501
=================================================================================
coef std err z P>|z| [0.025 0.975]
---------------------------------------------------------------------------------
const -2.314e-06 0.000 -0.013 0.989 -0.000 0.000
log_return.L1 -0.0100 0.012 -0.805 0.421 -0.034 0.014
Roots
=============================================================================
Real Imaginary Modulus Frequency
-----------------------------------------------------------------------------
AR.1 -100.1865 +0.0000j 100.1865 0.5000
-----------------------------------------------------------------------------
c:\Users\johns\AppData\Local\Programs\Python\Python312\Lib\site-packages\statsmodels\tsa\base\tsa_model.py:473: ValueWarning: A date index has been provided, but it has no associated frequency information and so will be ignored when e.g. forecasting.
self._init_dates(dates, freq)
Il est en resort que l’hypothèse nulle du test rejettée pour les 10 premiers lags, ce qui signifie que les résidus sont corrélés. Nous pouvons déjà conclure que le modèle AR(1) n’est pas satisfaisant. L’homoscédasticité des résidus peut être vérifiée aussi à l’aide du test de Portemanteau : en étudiant la corrélation des carrés des résidus. Nous pouvons aussi étudier la normalité des résidus à l’aide du test de Jarque-Bera et du QQ plot.
Test de Portemanteau sur les carrés des résidus
= acorr_ljungbox(residus**2, lags=10)
result_lyung_box print( result_lyung_box)
lb_stat lb_pvalue
1 231.953319 2.235445e-52
2 595.630965 4.574875e-130
3 952.903163 2.962284e-206
4 1235.609836 3.035897e-266
5 1701.848851 0.000000e+00
6 1928.023750 0.000000e+00
7 2124.968386 0.000000e+00
8 2319.905665 0.000000e+00
9 2595.596981 0.000000e+00
10 2892.265379 0.000000e+00
L’hypothèse nulle qui suppose que les résidus sont homoscédastiques est rejetée. Nous pouvons donc conclure que les résidus ne sont pas homoscédastiques et pensez à un modèle GARCH sur les résidus.
QQ plot des résidus
import matplotlib.pyplot as plt
import statsmodels.api as sm
='s')
sm.qqplot(residus, line 'residusB Returns - QQ Plot')
plt.title( plt.show()
Test de Jarque-Bera
from scipy.stats import jarque_bera
# Test de Jarque-Bera pour la normalité
= jarque_bera(residus)
jb_test_statistic, jb_p_value print("Jarque-Bera test statistic:", jb_test_statistic)
print("Jarque-Bera test p-value:", jb_p_value)
Jarque-Bera test statistic: 5204.275903184189
Jarque-Bera test p-value: 0.0
On peut déjà constater à partir de ce qqplot que les résidus ne suivent pas une loi normale. En effet, les quantiles théoriques et les quantiles empiriques ne sont pas alignés. Nous avons également rejeté l’hypothèse nulle du test de Jarque-Bera.
Nous allons maintenant modéliser le résidus de ce AR(1).
Adéquation du modèle GARCH sur les résidus
Afin de vérifier si le modèle GARCH est adéquat pour les résidus, nous allons ajuster un modèle GARCH(1,1) aux résidus et ensuite nous allons vérifier à partir des résidus au carrée s’il y’a de l’hetéroscedasticité dans les résidus.
from arch import arch_model
from statsmodels.graphics.tsaplots import plot_acf, plot_pacf
# Fit the GARCH(1,1) model in the résidus and take the square
= arch_model(residus, vol='Garch', p=1, q=1).fit(disp='off')
residus_garch_model = residus_garch_model.resid
residuals_garch = residuals_garch**2
squared_residuals_garch
= plt.subplots(1, 3, figsize=(15, 5))
fig, axes
=axes[0], title="")
plot_acf(squared_residuals_garch, ax=axes[1], title="")
plot_pacf(squared_residuals_garch, ax='s',ax=axes[2])
sm.qqplot(squared_residuals_garch, line
plt.show()
Le graphique ci-dessous confirme la présence d’hetéroscedasticité dans les résidus. Nous pouvons donc conclure que le modèle GARCH est adéquat pour les résidus. Appliquons dons un modèle GARCH(1,1) aux résidus et voyons ses résultats. Nous allons procéder par étape :
- Nous allons itérer sur les modèles ARIMA(p,d,q) pour trouver le modèle qui minimise le BIC.
- Nous allons prendre le modèle GARCH(p,q) en fonction du modèle ARIMA(p,d,q) qui minimise le BIC.
- Appliquer le modèle GARCH(p,q) aux résidus et examiner les résidus du modèle et les résidus du modèle au carré pour vérifier la présence d’hetéroscedasticité.
Avant d’aller plus loin, vérifions statistiquement la présence d’un effet ARCH dans les résidus.
## Test d'un effet ARCH sur les résidus
import statsmodels.api as sm
= sm.stats.diagnostic.het_arch(squared_residuals_garch)
arch_test
print(f'ARCH test results:\n')
print(f'LM Statistic: {arch_test[0]}')
print(f'p-value: {arch_test[1]}')
print(f'F Statistic: {arch_test[2]}')
print(f'p-value: {arch_test[3]}')
ARCH test results:
LM Statistic: 629.1578940701382
p-value: 9.916223442056947e-129
F Statistic: 69.55167742116966
p-value: 1.425288439064573e-135
Nous pouvons conclure qu’il y’a un effet ARCH dans les résidus. Nous allons donc appliquer un modèle GARCH(1,1) aux résidus pour capturer la volatilité. L’hypothèse d’homoscédasticité est rejetée. Donc il y’a une présence d’hétéroscédasticit conditionnelle dans les résidus.
Modélisation du GARCH sur les résidus.
Nous allons appliquer un modèle GARCH(1,1) sur les résidus.
= arch_model(residus, vol='Garch', p=1, q=1).fit(disp='off')
garch_final_model print(garch_final_model.summary())
Constant Mean - GARCH Model Results
==============================================================================
Dep. Variable: None R-squared: 0.000
Mean Model: Constant Mean Adj. R-squared: 0.000
Vol Model: GARCH Log-Likelihood: 19340.9
Distribution: Normal AIC: -38673.8
Method: Maximum Likelihood BIC: -38646.6
No. Observations: 6500
Date: Wed, Apr 24 2024 Df Residuals: 6499
Time: 01:52:11 Df Model: 1
Mean Model
============================================================================
coef std err t P>|t| 95.0% Conf. Int.
----------------------------------------------------------------------------
mu 3.3282e-04 1.553e-04 2.144 3.207e-02 [2.851e-05,6.371e-04]
Volatility Model
============================================================================
coef std err t P>|t| 95.0% Conf. Int.
----------------------------------------------------------------------------
omega 3.9475e-06 9.753e-13 4.047e+06 0.000 [3.948e-06,3.948e-06]
alpha[1] 0.1000 3.135e-03 31.894 3.255e-223 [9.385e-02, 0.106]
beta[1] 0.8800 3.953e-03 222.603 0.000 [ 0.872, 0.888]
============================================================================
Covariance estimator: robust
Les résultats du modèle GARCH(1,1) se trouvent dans le tableau ci-dessus. Nous pouvons constater que les paramètres sont tous significatifs. Nous allons maintenant vérifier la qualité d’ajustement du modèle GARCH(1,1) aux résidus.
Analyse des résidus du modèle GARCH
La figure ci-dessous représente les différents graphiques : - Le graphique des résidus du modèle GARCH - La volatilité conditionnelle estimée - Le graphique des résidus standardisés
Le résidu dont l’analyse de la blancheur nous intéresse est le résidu standardisé. Il est le résultat du rapport entre le résidu du modèle GARCH et la volatilité conditionnelle estimée. Nous allons donc vérifier si ce résidu est un bruit blanc. C’est ce résidu que nous allons utiliser pour modéliser la VaR.
= garch_final_model.resid / garch_final_model.conditional_volatility
std_residus = plt.subplots(3, 1, figsize=(15, 6))
fig, axes
0].plot(garch_final_model.resid)
axes[0].set_title("Résidus")
axes[1].plot(garch_final_model.conditional_volatility)
axes[1].set_title("Volatilité conditionnelle")
axes[2].plot(std_residus)
axes[2].set_title("Résidus standardisés")
axes[
plt.show()
Vérifions que les résidus de ce modèle GARCH sont des bruits blancs.
Nous allons faire le test de Ljung-Box pour vérifier l’autocorrélation des résidus. Et nous allons également faire ce test sur les carrés des résidus pour vérifier l’absence d’autocorrélation des résidus au carré d’où l’absence d’hetéroscedasticité.
# test de Ljung-Box sur les résidus et les résidus au carré
= acorr_ljungbox(std_residus, lags=[10], return_df=False)
test_lyungbow print(test_lyungbow)
= acorr_ljungbox(std_residus**2, lags=[10], return_df=False)
test_lyungbow_squared print(test_lyungbow_squared)
lb_stat lb_pvalue
10 16.281508 0.091852
lb_stat lb_pvalue
10 9.337446 0.500406
Dans les deux cas, la p-value est supèrieur à 5%. Nous ne pouvons donc pas rejeter l’hypothèse nulle selon laquelle les résidus sont des bruits blancs. Nous pouvons donc conclure que le modèle GARCH(1,1) est satisfaisant. Nous pouvons faire d’autres tests pour vérifier la qualité de notre modèle tel que le test de Jarque-Bera et le QQ plot.
Les estimations des paramètres du modèle AR(1)-GARCH(1,1) sont significatifs. Nous pouvons donc conclure que le modèle AR(1)-GARCH(1,1) est adéquat pour les log-rendements. Les paramètres estimés sont : - alpha = 0.0858 - beta = 0.8978 - omega = 0.0322 - phi = -0.0092
Estimation des paramètres du modèle AR(1)-GARCH(1,1).
Nous pouvons directement estimer les paramètres du modèle AR(1)-GARCH(1,1) en utilisant la fonction arch_model de la bibliothèque arch. C’est ce que nous allons faire dans la cellule suivante.
## estimation des paramètres du modèle AR(1) -GARCH(1,1)
= arch_model(train_close['log_return'], vol='Garch', p=1, q=1, mean='AR', lags=1).fit(disp='off')
ar_garch_model print(ar_garch_model.summary())
AR - GARCH Model Results
==============================================================================
Dep. Variable: log_return R-squared: -0.000
Mean Model: AR Adj. R-squared: -0.001
Vol Model: GARCH Log-Likelihood: 19340.9
Distribution: Normal AIC: -38671.8
Method: Maximum Likelihood BIC: -38637.9
No. Observations: 6500
Date: Wed, Apr 24 2024 Df Residuals: 6498
Time: 01:54:07 Df Model: 2
Mean Model
=================================================================================
coef std err t P>|t| 95.0% Conf. Int.
---------------------------------------------------------------------------------
Const 4.8457e-04 1.423e-04 3.404 6.636e-04 [2.056e-04,7.636e-04]
log_return[1] -9.9994e-03 1.272e-02 -0.786 0.432 [-3.494e-02,1.494e-02]
Volatility Model
============================================================================
coef std err t P>|t| 95.0% Conf. Int.
----------------------------------------------------------------------------
omega 3.9495e-06 7.237e-12 5.457e+05 0.000 [3.949e-06,3.950e-06]
alpha[1] 0.1000 6.899e-03 14.495 1.305e-47 [8.648e-02, 0.114]
beta[1] 0.8800 6.474e-03 135.923 0.000 [ 0.867, 0.893]
============================================================================
Covariance estimator: robust
# les paramètres du modèle AR(1)-GARCH(1,1) sont les suivants
print(f"Les paramètres du modèle AR(1)-GARCH(1,1) sont les suivants:\n{ar_garch_model.params}")
= ar_garch_model.params constance,phi, omega, alpha, beta
Les paramètres du modèle AR(1)-GARCH(1,1) sont les suivants:
Const 0.000485
log_return[1] -0.009999
omega 0.000004
alpha[1] 0.100000
beta[1] 0.880000
Name: params, dtype: float64
Les résultats ci-dessus donnent les mêmes résultats que ceux obtenus précédemment. Nous pouvons donc conclure que le modèle AR(1)-GARCH(1,1) est adéquat pour les log-rendements.
Reproduction de la dynamique historique de \(\sigma_t\) et de \(\mu_t\)
import numpy as np
def forcast_mu_sigma(r, phi_hat, omega_hat, alpha_hat, beta_hat):
"""
Calcule les prévisions de mu et sigma^2 pour une série de log-rendements
:param r: Vecteur de log-rendements (numpy array).
:param phi_hat: Estimation de phi (float).
:param omega_hat: Estimation de omega (float).
:param alpha_hat: Estimation de alpha (float).
:param beta_hat: Estimation de beta (float).
:return: Tuple de numpy arrays (mu_hat, sigma2_hat).
"""
# Initialisation des vecteurs mu_hat et sigma2_hat
= np.zeros(len(r))
mu_hat = np.zeros(len(r))
sigma2_hat
# Initialisation de la première valeur de mu_hat
0] = np.mean(r)
mu_hat[
# Initialisation de la première valeur de sigma2_hat en utilisant la variance long terme
0] = omega_hat / (1 - alpha_hat - beta_hat)
sigma2_hat[
# Calcul des valeurs de mu_hat et sigma2_hat en utilisant les formules GARCH(1,1)
for t in range(1, len(r)):
= mu_hat[0] + phi_hat * r[t-1]
mu_hat[t] = omega_hat + alpha_hat * (r[t-1] - mu_hat[t-1])**2 + beta_hat * sigma2_hat[t-1]
sigma2_hat[t]
return mu_hat, sigma2_hat
= ar_garch_model.params
constance,phi, omega, alpha, beta =forcast_mu_sigma(ts_close['log_return'],ar_garch_model.params['log_return[1]'], ar_garch_model.params['omega'], ar_garch_model.params['alpha[1]'], ar_garch_model.params['beta[1]'])
mu_hat, sigma2_hat
Une fois que l’on a reproduire la dynamique de \(\sigma_t\) et de \(\mu_t\), nous pouvons faire des représentations graphiques pour vérifier la qualité de cette reproduction. Dans le graphique ci-dessous, nous allons représenter la série de \(\sigma_t\) et de \(\mu_t\) en fonction de la date ainsi que la volatilité conditionnelle estimée par le modèle AR(1)-GARCH(1,1).
= plt.subplots(1,3,figsize=(15,5))
fig,axes 0].plot( mu_hat, label='Prévision de mu')
axes[0].set_title('Prévision de mu')
axes[0].legend()
axes[1].plot( sigma2_hat, label='Prévision de $\sigma^2$')
axes[1].set_title('Prévision de $\sigma^2$')
axes[1].legend()
axes[2].plot(garch_final_model.conditional_volatility, label='Volatilité conditionnelle')
axes[2].set_title('Volatilité conditionnelle')
axes[2].legend() axes[
Avant de calculer la VaR dynamique, nous allons estimer le quantile de \(1-\alpha\) des résidus \(\epsilon_t\). Rappelons que notre modèle AR(1)-GARCH(1,1) est donné par :
L’équation du modèle AR(1) pour la moyenne conditionnelle : \[\hat{\mu_t} = \hat{\mu} + \hat{\phi} R_{t-1}\]
L’équation du modèle GARCH(1,1) pour la variance conditionnelle : \[\hat{\sigma_t}^2 = \hat{\omega} + \hat{\alpha} (R_{t-1} - \hat{\mu_t})^2 + \hat{\beta} \hat{\sigma_{t-1}}^2\]
où : - \(\hat{\mu_t}\) est la moyenne conditionnelle au temps \(t\), - \(R_{t-1}\) est le rendement à \(t-1\), - \(\hat{\sigma_t}^2\) est la variance conditionnelle au temps \(t\), - \(\hat{\mu}\), \(\hat{\phi}\), \(\hat{\omega}\), \(\hat{\alpha}\), et \(\hat{\beta}\) sont les paramètres estimés du modèle.
Et enfin le résidu \(\epsilon_t\) est donné par :
\[\epsilon_t = \frac{R_t - \hat{\mu_t}}{\hat{\sigma_t}}\] qui est le résidu standardisé.
Calcul de la VaR sur les résidus standardisés \(\epsilon_t\)
Pour calculer la VaR sur les résidus standardisés, nous allons utiliser pour l’approche non-paramétrique, la VaR historique boostrap et pour la méthode TVE, nous allons utiliser l’approche maxima par bloc.
VaR historique boostrap sur les résidus standardisés et calcul de la VaR dynamique
### VaR historique bootstrap sur les résidus du modèle AR(1)-GARCH(1,1)
= 0.99
seuil = 0.9
seuil_IC = 5000
num_simulations = 251*10 #on utilise 10 ans de données comme taille d'échantillons bootstrap
n_B
= VaR_Hist_Bootstrap(std_residus, seuil, num_simulations, seuil_IC, n_B)[0]
var_residu_boostrap
= (mu_hat + np.sqrt(sigma2_hat) * var_residu_boostrap) var_garchdyn_boostrap
Backtesting
= [i for i in range(train_size,len(ts_close['log_return'])) if ts_close['log_return'][i]<var_garchdyn_boostrap[i]]
list_exceptions_garchdyn_boot len(list_exceptions_garchdyn_boot)
26
print(f"Le nombre d'exceptions pour la VaR GARCH est: {len(list_exceptions_garchdyn_boot)}")
print(f"Le pourcentage d'exceptions pour la VaR GARCH est: {len(list_exceptions_garchdyn_boot)/test_size*100.:0.2f}%")
Le nombre d'exceptions pour la VaR GARCH est: 26
Le pourcentage d'exceptions pour la VaR GARCH est: 1.20%
= stats.binomtest(len(list_exceptions_garchdyn_boot), test_size, p = 0.01)
test_except_gaus print(f'la p-value du test binomial est: {test_except_gaus.pvalue:.2f}')
la p-value du test binomial est: 0.33
La pvalue du test est supérieure au seuil de 5%. On ne peut donc pas rejeter l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%. Nous concluons donc que notre VaR est satisfaisante.
=(12, 8))
plt.figure(figsize'log_return'], label="historical log returns", color = 'blue')
plt.plot(ts_close.index, ts_close[='VaR dynamique', color = 'red')
plt.plot(ts_close.index[train_size:], var_garchdyn_boostrap[train_size:], label= ts_close.index[train_size-251*10-1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
plt.axvline(x 'log_return'][list_exceptions_garchdyn_boot], color='red', label='Exceptions')
plt.scatter(ts_close.index[list_exceptions_garchdyn_boot], ts_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
VaR par la méthode maxima par bloc sur les résidus standardisés et calcul de la VaR dynamique
La TVE s’intéressant aux queues de distribution droite, nous allons d’abord convertir les résidus standardisés en prenant l’opposé.
= -std_residus std_residus_minus
= construct_max_data(std_residus_minus, 21)
std_residus_maxima
=(10, 8))
plt.figure(figsize= 'blue')
plt.plot(std_residus_maxima ,color "residus maxima ")
plt.title(
plt.plot()
plt.legend() plt.show()
No artists with labels found to put in legend. Note that artists whose label start with an underscore are ignored when legend() is called with no argument.
= []
quantiles_theoriques_gumbel_residus for i in range(1,len(std_residus_maxima)+1):
= -np.log(-np.log((i-0.5)/len(std_residus_maxima)))
val
quantiles_theoriques_gumbel_residus.append(val)
# Tracer le Gumbel plot
=(10, 8))
plt.figure(figsize='o')
plt.scatter(quantiles_theoriques_gumbel_residus, np.sort(std_residus_maxima), marker
# Étape 1 : Calculer les coefficients de la droite d'ajustement
= np.polyfit(quantiles_theoriques_gumbel_residus, np.sort(std_residus_maxima), 1)
coefficients
# Étape 2 : Générer les valeurs de la droite d'ajustement
= np.poly1d(coefficients)
droite_ajustement = np.array(quantiles_theoriques_gumbel_residus)
x = droite_ajustement(x)
y_ajustement
# Étape 3 : Tracer la droite d'ajustement
='red')
plt.plot(x, y_ajustement, color
'Gumbel Plot')
plt.title('Quantiles théoriques de Gumbel')
plt.xlabel('Quantiles observés')
plt.ylabel( plt.show()
Le gumbel plot semble linéaire et ne présente pas de courbure. Nous allons donc utiliser la loi de Gumbel pour la suite.
from scipy.stats import genextreme as gev
from scipy.stats import gumbel_r
= gev.fit(std_residus_maxima)
params_gev_residus = gumbel_r.fit(std_residus_maxima)
params_gumbel_residus
# Afficher les paramètres estimés
print("Paramètres estimés de la distribution GEV:")
print("Forme (xi) =", params_gev_residus[0])
print("Localisation (mu) =", params_gev_residus[1])
print("Echelle (sigma) =", params_gev_residus[2])
print(150*"-")
print("Paramètres estimés de la distribution de Gumbel (xi nul):")
print("Localisation (mu) =", params_gumbel_residus[0])
print("Echelle (sigma) =", params_gumbel_residus[1])
Paramètres estimés de la distribution GEV:
Forme (xi) = -0.022284230963655637
Localisation (mu) = 1.7112620288571874
Echelle (sigma) = 0.596567928970954
------------------------------------------------------------------------------------------------------------------------------------------------------
Paramètres estimés de la distribution de Gumbel (xi nul):
Localisation (mu) = 1.71881001440757
Echelle (sigma) = 0.6004004139302063
# Comparaison des distributions GEV et Gumbel
=(10, 8))
plt.figure(figsize=30, density=True, label='Données observées')
plt.hist(std_residus_maxima, bins= plt.xlim()
xmin, xmax = np.linspace(xmin, xmax, 100)
x
# Densité GEV ajustée
= gev.pdf(x, *params_gev_residus)
p_gev_residus 'k', linewidth=2, label='GEV ajustée')
plt.plot(x, p_gev_residus,
# Densité Gumbel ajustée
= gumbel_r.pdf(x, *params_gumbel_residus)
p_gumbel_residus 'r', linewidth=2, label='Gumbel ajustée')
plt.plot(x, p_gumbel_residus,
'Comparaison GEV vs Gumbel')
plt.title(
plt.legend() plt.show()
Les deux distributions semblent bien modéliser la queue de distribution des résidus standardisés.
= np.arange(0.001, 1, 0.001)
niveaux_quantiles_residus = np.quantile(std_residus_maxima, niveaux_quantiles_residus)
quantiles_empiriques_TVE_residus = gev.ppf(niveaux_quantiles_residus, *params_gev_residus)
quantiles_theoriques_GEV_residus = gumbel_r.ppf(niveaux_quantiles_residus, *params_gumbel_residus) quantiles_theoriques_Gumb_residus
=(12, 8))
plt.figure(figsize1, 2, 1)
plt.subplot(
plt.scatter(quantiles_theoriques_GEV_residus, quantiles_empiriques_TVE_residus)='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(quantiles_theoriques_GEV_residus, quantiles_theoriques_GEV_residus, color"QQ Plot d'une modélisation par loi GEV")
plt.title('Quantiles théoriques (Loi GEV)')
plt.xlabel('Quantiles empiriques')
plt.ylabel(True)
plt.grid(
1, 2, 2)
plt.subplot(
plt.scatter(quantiles_theoriques_Gumb_residus, quantiles_empiriques_TVE_residus)='red', linestyle='dashed', linewidth=2, label='Première bissectrice')
plt.plot(quantiles_theoriques_Gumb_residus, quantiles_theoriques_Gumb_residus, color"QQ Plot d'une modélisation par loi Gumbel")
plt.title('Quantiles théoriques (Loi Gumbel)')
plt.xlabel('Quantiles empiriques')
plt.ylabel(True)
plt.grid(
plt.tight_layout() plt.show()
Les deux modélisations semblent bien correspondre aux données. Comme le Gumbel plot est linéaire, nous allons utiliser la loi de Gumbel pour modéliser la GEV et calculer la VaR.
Calcul de la VaR
# redressement de alpha
= 0.99
alpha = 21
s = 1-s*(1-alpha)
alpha_bm = -gumbel_r.ppf(alpha_bm, *params_gumbel_residus)
var_std_residus
= (mu_hat + np.sqrt(sigma2_hat) * var_std_residus) var_dyn_TVE_residus
Backtesting
= [i for i in range(train_size,len(ts_close['log_return'])) if ts_close['log_return'][i]<var_dyn_TVE_residus[i]]
list_exceptions_TVE_residus len(list_exceptions_TVE_residus)
25
print(f"Le nombre d'exceptions pour la VaR TVE est: {len(list_exceptions_TVE_residus)}")
print(f"Le pourcentage d'exceptions pour la VaR TVE est: {len(list_exceptions_TVE_residus)/test_size*100.:0.2f}%")
Le nombre d'exceptions pour la VaR TVE est: 25
Le pourcentage d'exceptions pour la VaR TVE est: 1.15%
= stats.binomtest(len(list_exceptions_TVE_residus), test_size, p = 0.01)
test_except_TVE_residus print(f'la p-value du test binomial est: {test_except_TVE_residus.pvalue:.2f}')
la p-value du test binomial est: 0.45
La pvalue du test est supérieure au seuil de 5%. On ne peut donc pas rejeter l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%. Nous concluons donc que notre VaR dynamique est satisfaisante.
=(12, 8))
plt.figure(figsize'log_return'], label="historical log returns", color = 'blue')
plt.plot(ts_close.index, ts_close[='VaR dynamique', color = 'red')
plt.plot(ts_close.index[train_size:], var_dyn_TVE_residus[train_size:], label= ts_close.index[train_size-251*10-1], color='green', linestyle='-', label='Limite de données pour le calcul de la VaR')
plt.axvline(x 'log_return'][list_exceptions_TVE_residus], color='red', label='Exceptions')
plt.scatter(ts_close.index[list_exceptions_TVE_residus], ts_close['CAC40')
plt.title('Values')
plt.ylabel(
plt.plot()
plt.legend() plt.show()
Conclusion
Ce projet a eu pour objectif d’estimer la VaR à horizon 1 jour et la VaR dynamique de différentes façons. Nous avons utilisé les données historiques du CaC40. Nous avons utilisé des méthodes non-paramétriques telles que la VaR historique et la VaR historique bootstrap. Nous avons également utilisé des méthodes paramétriques telles que la VaR gaussienne, la VaR skew-student et nous avons utilisé la théorie des valeurs extrêmes pour modéliser la VaR. Nous avons également estimé de deux façons différentes la VaR dynamique. La première méthode consiste à calculer la VaR historique sur une fenêtre glissante de 251 jours. La deuxième méthode consiste à modéliser la VaR par un modèle AR-GARCH. Nous avons également validé nos modèles par des tests de backtesting.
Les différentes approches de modélisation estime la VaR à horizon 1 jour à environ -0.03 avec un niveau de confiance de 99%. Ceci signifie que la perte maximale que peut subir un portefeuille sur un horizon de 1 jour avec un niveau de confiance de 99% est de 3%. Nous avons également validé nos modèles par des tests de backtesting qui ne rejettent pas l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%.
La VaR dynamique estimée sur une fenêtre glissante de 251 jours n’a pas été validée par le test de backtesting. En effet, la pvalue du test est inférieure au seuil de 5%. Nous rejetons donc l’hypothèse nulle selon laquelle la probabilité d’exception est de 1%. Cependant, la VaR dynamique estimée par le modèle AR(1)-GARCH(1,1) donne des meilleurs résultats. L’hypothèse nulle du test selon laquelle le pourcentage d’exceptions est inférieur à 1% n’a pas été rejetée.