Projet de gestion de risque par Paul Leydier et Wenceslas Sanchez
Plan:
Vous pourrez retrouver le code ici
Dans un premier temps, nous avons décidé de prendre comme données le S&P500, sur les 3 dernières années (de 2017-02-14 à 2020-02-13). Pour calculer le rendement de quotidien de l’indice, nous avons considéré le log return $\log(\frac{P_t}{P_{t-1}})$ (composition continue).
Voici donc notre série de rendement:
fig= plt.figure(figsize= (10, 5))
plt.plot(sigtested.index, data_return["Return"].values)
plt.show()
La Value At Risk (VaR) représente la perte maximale possible sur un horizon de temps donné, pour une probabilité fixée (ici 99%). Il y a plusieurs manières pour la calculer:
Dans notre cas:
Nous avons tendance à préférer la VaR empirique, car dans un premier temps c’est la plus sévère, et dans un second temps, nous n’avons pas besoin d’hypothèse sur la distribution de nos rendements; ce qui ne semble pas être le cas avec ce que nous pouvons observer sur le graphique suivant, sur lequel on compare notre distribution à une loi normale ayant les mêmes 2 premiers moments (courbe noire):
compare_hist_to_norm(data_return["Return"].values, bins= 25)
On cherche à caractériser la loi extrème. Le paramètre $\xi$ nous permet de comprendre le comportement de notre queue grâce a la GEV, la distribution généralisée des valeurs extrêmes. Pour calculer $\xi$, nous allons utiliser une méthode nonparamétrique : l’estimateur de Pickands que voici:
$$\xi_{P}^{k,n} = \frac{1}{ln2}*ln(\frac{X_{k,n} - X_{2k,n}}{X_{2k,n} - X_{4k,n}})$$
Il est important de rappeler que l’estimateur de Pickands est très sensible à la valeur k que l’on définit. C’est ce que nous montre ce graphique :
fig= plt.figure(figsize= (10, 5))
plt.plot(pickand_estimatored_k, c= "purple")
plt.title("Variation de l'estimateur de Pickands avec K")
plt.xlabel("k")
plt.ylabel("Pickland value")
plt.savefig('images/pickands_variation.png')
plt.show()
Ce qui nous dérange avec l’estimateur de Pickands, au-delà du fait qu’il est très sensible au choix de k, c’est qu’il ne prend jamais en considération la pire perte possible, et nous ne comprenons pas vraiment pourquoi. Si le but est de définir la loi de notre queue, il est important de considérer la pire des pires pertes. Dans la formule, $X_{k,n}=max(neg_data_return[:n-k+1])$, mis à part pour $k=1$, jamais on ne peut attraper le pire return.
Dans notre cas, en ayant choisi un k = 5, on a eu la chance d’avoir un $\xi$ positif. Ainsi nous avons décidé dans un premier temps de garder k = 5 (à voir si plus tard, on cherchera un “meilleur” k).
Ici: $$\hat{\xi}= 0.32051$$
Le fait qu’il soit positif nous permet de dire que notre queue est épaisse. On va ainsi pouvoir représenter notre queue par une loi de Fréchet.
A partir de l’estimation du paramètre $\xi$, on peut chercher à calculer une VaR grâce à la théorie des valeurs extrèmes par l’estimateur de Pickands: $$VaR_p = \frac{(\frac{k}{n(1-p)})^{\xi^{P}}-1}{1-2^{-\xi^{P}}}(X_{n-k+1:n} + X_{n-2k+1:n}) + X_{n-k+1:n}$$
Ainsi, nous avons: $$VaR_{99%}=-0.02771$$
Pour le moment, c’est la VaR la plus sévère que nous ayons eu l’occasion de calculer.
Nous sommes toujours sur des rendements journaliers, donc toutes les VaR que nous avons calculé sont à un horizon de 1 jour.
L’idée derrière l’indice extrême de Leadbetter, c’est de comptabiliser combien de fois mes rendements sont tombés en dessous d’un seuil de risque que l’on définit (ici, pour calculer cette métrique, nous avons définit un seuil à -3%, mais on aurait pû très bien prendre une des VaR que nous avons précédemment calculé.
Plus précisément, on coupe notre série en B buckets (dans notre exemple 10 valeurs par bucket donc 74 buckets), et on regarde si dans un des buckets, la pire perte dépasse ce seuil. Enfin, on regarde le nombre de buckets qui ont franchi ce seuil par rapport au nombre de fois ou ma série a franchi ce seuil. Ici, nous avons eu un indice de Leadbetter de 0.09814. L’idée derrière la construction de cette indice est le problème de persistance d’une perte extrème d’un jour au lendemain. En effet, le fait de déterminer des buckets nous permet de ne considérer qu’une fois une grosse perte (au sein du bucket) même (et surtout) si cette perte est suivie de d’autres grosses pertes. Donc on a pas de problème de persistance du signal dans cet indice (ce qui est problématique pour construire la loi de distribution de notre queue).
Mais, cette métrique est sensible au nombre de bucket que l’on choisi. Par conséquent, si le nombre de buckets choisis est égal au nombre d’observation de ma série, cela revient uniquement à compter le nombre de fois où ma série a franchi le seuil (indice de Leadbetter = 1). Tout comme avoir un nombre de bucket égal à 1 reviendrait à avoir un indice très faible. Enfin, rappelons que le but est d’avoir des buckets suffisament gros pour se soustraire du problème de persistance d’une grosse perte.
Avec cette idée de seuil et de proportion d’observation qui dépasse ce seuil, on pourrait utiliser la méthode suivante comme nouvelle VaR:
$$VaR_p=u+\frac{\hat{\sigma}}{\hat{\xi}}((\frac{N}{N_u}(1-p))^{-\hat{\xi}}-1)$$
Avec:
On a pas exactement l’indice extrême de Leadbetter, mais on retrouve cette idée de proportion de return qui dépasse notre seuil fixé.
L’exposant de Hurst $H$ est un indicateur qui permet de caractériser la mémoire d’une série temporelle (peut être vu comme une mesure de l’autocorrélation d’une série temporelle). Initialement, c’est l’hydrologue Hurst qui avait remarqué que les niveaux d’eau successifs n’était pas indépendants au cours du temps, alors que tous les modèles étaient fondés sur cette hypothèse lourde de sens.
En finance, on pense que le marché est efficient, au sens de Fama, c’est à dire que le prix contient toute l’information disponible. Dans ce cadre, il est impossible de prévoir pour demain le prix d’un actif, car toute l’information anticipée est dans le prix. Par conséquent, aucun arbitrage n’est possible et l’espérance du prix de demain est le prix d’aujourdhui (le prix suit une marche aléatoire). Certaines statistiques permettent de vérifier ou pas cette hypothèse. L’exposant de Hurst permet ainsi de caractériser la probabilité qu’un évènement soit suivi par un évènement similaire. Si par exemple, un événement extrême se produit suite à un autre évènement extrême, alors notre série subit une sorte de persistance. Dans le cadre des marchés boursiers, on pourrait alors prédire les rendements futurs à l’aide des rendements présent, contrairement à ce que prouve la théorie financière. Il permet aussi de caractériser des relations / pattern un peu plus long (nous verrons cela dans la question suivante).
L’exposant de Hurst est compris entre 0 et 1. Si il est égal à 1/2, pas d’autocorrélation; notre série est un mouvement brownien classique. Ca correspond à une marche aléatoire sur les marchés.
Dans un premier temps, il faut calculer notre exposant de Hurst $H$. Nous allons utiliser la méthode des valeurs absolues de la série aggrégée. Le but de cette méthode est de couper notre dataset en $m$ buckets, et de faire la somme par bucket de nos return et de le diviser par le nombre de bucket que nous avons considéré. Puis on en récupère sa valeur absolue, pour sommer toutes les sommes de bucket. Et cela, on le fait pour différentes tailles de bucket, pour différentes mesures du return (quotidient, mensuel etc.). Le fait de passer par la valeur absolue nous permet de considérer les patterns de returns négatifs (des pertes) de la même façon que les patterns de returns positifs. En fait, en faisant la somme pour chaque bucket, on peut obtenir des mouvements intra et inter bucket de return : par exemple, avec un bucket de 10 returns quotidients, on va remarquer qu’une forte hausse sur 10 jours de return est suivi 3 buckets plus tard par une forte hausse du return sur plusieurs jours aussi.
La force de $H$, c’est de pouvoir à la fois considérer des persistances intra et inter bucket pour différentes tailles de buckets. Pour observer ce lien entre taille de bucket et notre somme de la série agrégée, on fait une regression, avec comme variable explicative le log du nombre de bucket, et à expliquer le log de la somme absolue de la série agrégée. La pente - 1 sera alors la valeur de notre $H$.
Pour notre série de log_return, nous avons un exposant de Hurst de $0.697$, c’est à dire de la persistance dans notre signal, ce qui pourrais sembler logique si on considère l’euphorie des marchés, le comportement moutonier etc.
Le problème, c’est que nous n’avons aucune idée de comment construire une métrique de risque avec le $H$ et la contrainte de Nuzmann-Poor. Néanmoins, ce que nous avons réussi à faire, en considérant ces 2 contraintes, c’est de faire une prédiction du return (prédiction d’un mouvement brownien fractionnaire). On s’est aidé de ce site.
plt.figure(figsize= (10, 5))
plt.plot(rv_actual_r, "blue", label= "Log Return")
plt.plot(rv_predict_r, "red", label= "Prédiction")
plt.legend()
plt.title("Prédiction du log return à l'aide du modèle de Nuzman-Poor")
plt.savefig("images/predicted_actual_vol.png")
plt.show()
Mais, on ne sait pas trop quoi en faire, en tous cas pour créer une mesure du risque.
L’expected shortfall (ES) représente la moyenne de nos log returns au-delà de la VaR. Cette métrique, outre fait d’être une mesure de risque cohérente, permet d’observer le comportement des returns après la VaR: a-t-on des pertes 10 fois plus forte que ma VaR ? etc.
On la calcule soit:
Voici les résultats des ES pour chacun des modèles. Pour les calculer, nous avons simplement calculé une moyenne:
Modèle de VaR | Valeur de la VaR99 | Valeur de l’ES |
---|---|---|
VaR empirique | −0.026 | -0.033 |
VaR paramétrique | −0.01852 | -0.026 |
VaR non paramétrique | −0.01856 | -0.026 |
VaR Pickands | −0.0277 | -0.033 |
Dans un premier temps, nous avons séparé notre dataset en 2: le premier pour calculer les VaR et ES, et le second pour tester la pertinence de ces mesures.
Le graphique que voici représente la distribution de notre datatset de test avec les VaR et ES calculées sur notre dataset de “train”:
fig= plt.figure(figsize= (10, 5))
# Distribution de notre echantillon de backtest
plt.hist(backtest, bins= 30, color= "silver")
# VaR
plt.axvline(empiri_quant_esti, color= "red", label= "VaR empirique")
plt.axvline(parametric_quant_esti, color= "brown", label= "VaR paramétrique")
plt.axvline(nonparametric_quant_esti, color= "orange", label= "VaR non paramétrique")
plt.axvline(pickands_var_estimator_esti, color= "coral", label= "VaR Pickands")
# ES
plt.axvline(empiri_es_esti, color= "dodgerblue", label= "ES empirique")
plt.axvline(parametric_es_esti, color= "aqua", label= "ES paramétrique")
plt.axvline(nonparametric_es_esti, color= "skyblue", label= "ES non paramétrique")
plt.axvline(pickands_es_esti, color= "royalblue", label= "ES Pickands")
plt.legend(loc='best', frameon= True, shadow= True, framealpha= 0.9)
plt.title("Distribution de notre échantillon de test (avec {} observations)".format(len(backtest))\
, size= 18)
plt.xlabel("Rendement")
plt.ylabel("Fréquence")
plt.savefig("images/var_es_backtest.png")
plt.show()
Les valeurs des VaR et ES sont calculées sur le train data. Pour le backtest, on a calculé 2 tests à l’aide du dataset de test: un test non conditionnel (loi normale) et le test de Kupiec (loi du Xi²).
Avec ces tests, on regarde si le seuil (nos métriques) sont cohérentes par rapport au seuil de la loi normale ou du Xi².
A savoir que, l’hypothèse $H0$:“la probabilité de hit est égal à notre seuil $\alpha=99%$” est accepté si la statistique que l’on calcule est inférieure au seuil de la loi pour un $\alpha=99%$.
result_test
Méthode | Valeur | Test non conditionnel | Test Kupiec | |
---|---|---|---|---|
0 | VaR empirique | -0.029147 | On accepte H0 | On rejète H0 |
1 | VaR paramétrique | -0.019511 | On accepte H0 | On rejète H0 |
2 | VaR Non-paramétrique | -0.019544 | On accepte H0 | On rejète H0 |
3 | VaR Pickands | -0.029814 | On accepte H0 | On rejète H0 |
4 | ES empirique | -0.034052 | On accepte H0 | On accepte H0 |
5 | ES paramétrique | -0.027255 | On accepte H0 | On rejète H0 |
6 | ES Non-paramétrique | -0.027255 | On accepte H0 | On rejète H0 |
7 | ES Pickands | -0.034917 | On accepte H0 | On accepte H0 |
On remarque donc que seuls les seuils les plus sévères (ES empirique et Pickands) sont acceptés par nos 2 tests.
Aussi, le test non conditionnel accepte tous les seuils, ce qui semble logique étant donné que le seuil le plus faible est celui de la VaR paramétrique/non paramétrique et qu’ils sont construits à partir de la loi normale.
A vrai dire, il suffit d’avoir un seuil sévère pour être accepté par ces tests. Par conséquent, être validé par ce test ne qualifie pas notre seuil de bon, mais uniquement de suffisament sévère. Un seuil de -10% aurait aussi été accepté par nos test. Voila la limite de ces test, mais ils ont tout de même le mérite de comparer nos seuils avec ceux de distribution connues.
Selon Ibrahima Niang dans son article Quantification et méthodes statistiques pour le risque de modèle publié en 2017, le risque de modèle en finance désigne “le risque de pertes financières résultant de l’utilisation de modèles”. Pour être honnête, cette définition est vague et pour cause, on se sait pas correctement quantifier ce risque contrairement à d’autres types de risque comme celui de crédit.
Selon Thomas Lallement (page 21), le risque de modèle a plusieurs sources:
Risque de spécification: on ne choisi pas le bon modèle / les hypothèses du modèle ne sont pas vérifiées dans la réalité
Risque d’implémentation: erreur dans le code du modèle ou mauvais choix d’algorithme
Risque de traitement des données: les données choisies ne sont pas représentatives, ou n’ont pas bien été retraitées
Risque d’estimation: lié à un mauvais calibrage d’un modèle (hyperparamètre, ne converge pas etc.)
Risque d’application: risque qui provient à l’approximation de modèle beaucoup trop complexe soit à utiliser, soit à cause des données
Pour répondre à cette question, on a calculé 10,000 fois la moyenne et la variance de 10 returns pris au hasard, avec remise, dans notre série de log return. On a alors pu tracer la distribution de notre moyenne et de notre variance et la comparer à la loi normale, et de même, avec ces 2 paramètres, on a pu obtenir la distribution de notre VaR paramétrique.
print("\t\t\t\t\t Distribution de la moyenne")
compare_hist_to_norm(mu_para, 50)
print("\n\t\t\t\t\t Distribution de l'écart-type")
compare_hist_to_norm(sig_para, 50)
Distribution de la moyenne
Distribution de l'écart-type
On remarque ainsi, que si on définit la loi normale uniquement à l’aide des 2 premiers moments, la distribution de notre moyenne semble suivre une loi normale de paramètre $\mu=0.0005$ et $\sigma=0.0026$. L’écart-type néanmoins ne suit pas une loi normale définit à l’aide des 2 premiers moments, ce qui est “logique” étant donné qu’un l’écart-type est strictement positif, et que dans notre cas la moyenne est proche de 0 (ce qui force la distribution a avoir cette forme asymétrique).
Ainsi, si on construit une loi normale à l’aide des 3 premiers moments (on ajoute la skewness, qui représente l’asymétrie de notre distribution), notre distribution de l’écart-type semble suivre une loi normale. Le fait de considérer le paramètre d’asymétrie de la loi est très intéressant pour la distribution de l’écart-type étant donné son impossibilité à avoir des valeurs négatives (si la moyenne est proche de 0).
print("\t\t\t\t\t Distribution de la VaR")
compare_hist_to_norm(var_para, 50)
Distribution de la VaR
La distribution de la VaR est aussi asymétrique, donc en considérant les 3 premiers moments de la loi normale, on a une distribution qui suit plutôt bien notre histogramme.
Ce qu’on aurait pu faire, c’est chercher une loi spécifiquement asymétrique, pour la comparer à la distribution asymétrique de nos paramètres. Mais on pense, que la loi normale est un bon objet pour comparer nos distributions, à partir du moment où on l’a définit avec plus de 2 moments.
Dans un premier temps, on a simulé 10,000 fois 1,000 returns gaussiens; et pour chacune de ces simulations, on a calculé l’estimateur de Pickands, l’extreme K, l’extreme K2, l’extreme K4.
Avec: $$extreme K=max(neg_data_return[:n-k+1])$$ $$extreme K2=max(neg_data_return[:n-k2+1]) $$$$extreme K4=max(neg_data_return[:n-k4+1])$$
Sans réelle surprise, la distribution de tous les paramètres ET de la VaR Pickands suivent une loi normale.
print("\t\t\t\t\t Distribution de l'estimateur de Pickands")
compare_hist_to_norm(pick_, 30)
print("\n\t\t\t\t\t Distribution de l'extrème k")
compare_hist_to_norm(extreme_k_, 30)
print("\n\t\t\t\t\t Distribution de l'extrème k2")
compare_hist_to_norm(extreme_k2_, 30)
print("\n\t\t\t\t\t Distribution de l'extrème k4")
compare_hist_to_norm(extreme_k4_, 30)
Distribution de l'estimateur de Pickands
Distribution de l'extrème k
Distribution de l'extrème k2
Distribution de l'extrème k4
print("\t\t\t\t\t Distribution de la VaR Pickands")
compare_hist_to_norm(var_pick_, 30)
Distribution de la VaR Pickands
Encore une fois, nous avons fait 1000 simulations de Monte Carlo, pour ainsi calculer 1000 VaR non paramétriques. On peut alors tracer la distribution de cette VaR. Mais c’est un histogramme, et donc pas forcément représentatif pour considérer une distribution. On peut alors appliquer un noyau de densité pour assouplir notre histogramme. Nous pouvons de plus choisir le niveau d’ assouplissement, une sorte de fenêtre, le bandwidth.
Comme on peut le voir en dessous, on a affiché différents assouplissements pour notre histogramme en fonction de la valeur de la fenêtre. On peut observer le fait qu’une fenêtre faible aura tendance à dessiner une courbe qui se rapprochera de l’histogramme (cf. courbe jaune), alors qu’une valeur plus forte nous rapprochera d’une courbe plus smooth (cf. courbe verte).
obs_dist= stock_nonpara
fig = plt.figure(figsize=(12, 5))
ax = fig.add_subplot(111)
ax.hist(obs_dist, bins=20, density=True, label='Données',
zorder=5, edgecolor='k', alpha=0.5)
for i in [0.0009, 0.0005, 0.0001, 0.00005]:
kde = sm.nonparametric.KDEUnivariate(obs_dist)
kde.fit(bw= i)
ax.plot(kde.support, kde.density, lw=3, label='KDE avec h={}'.format(i), zorder=10)
plt.title("Distribution de notre VaR nonparamétrique")
ax.legend(loc='best')
ax.grid(True, zorder=-5)
Pour toutes les VaR que nous avons eu l’occasion de calculer, en absolu, le diamètre représentera la différence entre la valeur max et la valeur min des VaR.
Ainsi, pour les VaR au seuil de 99%, le diamètre $D_{99%}^{VaR}$ est $D_{99%}^{VaR}= -0.009188$.
Pour l’ES, $D_{99%}^{ES}=-0.007945$.
Si l’écart entre les modèles pour une même métrique est plus fort que pour une autre métrique, alors la première métrique est plus risquée (dans le sens où on est moins sûr de sa valeur) car il y a plus de variance entre les méthodes utilisées pour la calculer. Dans notre cas, le diamètre est plus faible pour l’ES que pour la VaR, donc l’ES est une métrique plus stable pour les modèles qui ont été considérés (empirique, paramétrique, nonparamétrique, Pickands).
On recalcule nos diamètres pour la VaR et l’ES, mais cette fois-ci on change notre $\alpha$, le seuil de confiance de la VaR:
for i in [0.99, 0.90, 0.95, 0.98, 0.995, 0.999, 0.9999]:
val_min, idx_min = min((val, idx) for (idx, val) in enumerate(compute_all_es(i)))
val_max, idx_max = max((val, idx) for (idx, val) in enumerate(compute_all_es(i)))
print("Diamètre ES pour le seuil {}: {}".format(i, -val_max + val_min))
val_min, idx_min = min((val, idx) for (idx, val) in enumerate(compute_all_var(i)))
val_max, idx_max = max((val, idx) for (idx, val) in enumerate(compute_all_var(i)))
print("Diamètre VaR pour le seuil {}: {}".format(i, -val_max + val_min))
print("\n")
Diamètre ES pour le seuil 0.99: -0.007945211649011769
Diamètre VaR pour le seuil 0.99: -0.009188746576442836
Diamètre ES pour le seuil 0.9: -0.005710228632961899
Diamètre VaR pour le seuil 0.9: -0.00695973660852507
Diamètre ES pour le seuil 0.95: -0.0032166950758197137
Diamètre VaR pour le seuil 0.95: -0.004356918668925779
Diamètre ES pour le seuil 0.98: -0.005628038983000093
Diamètre VaR pour le seuil 0.98: -0.0062978140718848614
Diamètre ES pour le seuil 0.995: -0.013048535758552353
Diamètre VaR pour le seuil 0.995: -0.013581747505721255
Diamètre ES pour le seuil 0.999: -0.010746341914527922
Diamètre VaR pour le seuil 0.999: -0.031125899770141584
Diamètre ES pour le seuil 0.9999: -0.007176013354409136
Diamètre VaR pour le seuil 0.9999: -0.08493517078919029
Dans tous les cas, l’ES est plus stable parmi ses modèles que la VaR. Du coup, ce que nous avions observé dans la question précédente reste de mise.
Mais, il faut avouer que calculer les métriques avec un seuil de $\alpha=0.9999$ n’a pas d’intérêt, voir même peut nous amener à de fausses conclusions, car nous n’avons pas 10,000 observations dans notre dataset (seulement ~750).
Pour cette question, on va ajouter un bruit blanc, mais en faisant varier l’amplitude du bruit (cf $\sigma$). Sur ce dataset bruité, on va calculer la VaR pour chaque modèle, et on calculera la différence de ces VaR (absolues) pour les mêmes modèles, comme ceci:
$$D_{nor-nois} = |VaR_{normal}| - |VaR_{noisy}|$$
Plus cette différence sera faible, moins le bruit aura impacté la valeur de nos VaR.
np.random.seed(1)
ampl= 1
data_noised= noised_dataset(amplitude= ampl)
fig= plt.figure(figsize= (10, 4))
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
Pour un bruit blanc gaussien avec une amplitude de 1,
la différence de valeur de la VaR Historique est: 4.3169597499899404e-05
Pour un bruit blanc gaussien avec une amplitude de 1,
la différence de valeur de la VaR Paramétrique est: 3.4784766005054035e-05
Pour un bruit blanc gaussien avec une amplitude de 1,
la différence de valeur de la VaR Non Paramétrique est: 3.7576264724187985e-05
Pour un bruit blanc gaussien avec une amplitude de 1,
la différence de valeur de la VaR Pickands est: 0.00038896194694186537
np.random.seed(5)
ampl= 5
data_noised= noised_dataset(amplitude= ampl)
fig= plt.figure(figsize= (10, 4))
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
Pour un bruit blanc gaussien avec une amplitude de 5,
la différence de valeur de la VaR Historique est: 0.0011395872061224603
Pour un bruit blanc gaussien avec une amplitude de 5,
la différence de valeur de la VaR Paramétrique est: 0.0005735336705678731
Pour un bruit blanc gaussien avec une amplitude de 5,
la différence de valeur de la VaR Non Paramétrique est: 0.0005341659378254072
Pour un bruit blanc gaussien avec une amplitude de 5,
la différence de valeur de la VaR Pickands est: 3.2463867425905346e-05
np.random.seed(10)
ampl= 10
data_noised= noised_dataset(amplitude= ampl)
fig= plt.figure(figsize= (10, 4))
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
Pour un bruit blanc gaussien avec une amplitude de 10,
la différence de valeur de la VaR Historique est: 7.9179742190464e-05
Pour un bruit blanc gaussien avec une amplitude de 10,
la différence de valeur de la VaR Paramétrique est: 0.0024311052305657326
Pour un bruit blanc gaussien avec une amplitude de 10,
la différence de valeur de la VaR Non Paramétrique est: 0.0023681645099281785
Pour un bruit blanc gaussien avec une amplitude de 10,
la différence de valeur de la VaR Pickands est: 0.00040645315792135414
np.random.seed(100)
ampl= 100
data_noised= noised_dataset(amplitude= ampl)
fig= plt.figure(figsize= (10, 4))
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Historique est: 0.093636257935175
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Paramétrique est: 0.10963077568401737
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Non Paramétrique est: 0.10923757427975433
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Pickands est: 0.09323463211480136
Pour une amplitude = 1, la VaR Pickands est la plus stable, c’est à dire que $D_{nor-nois}$ est le plus faible pour cette métrique.
Mais on se rend compte que pour toutes les autres amplitudes, c’est la VaR paramétrique qui semble la plus stable au bruit (on pourrait aussi considérer la VaR non paramétrique car la différence est ausi très faible et proche de celle de la VaR paramétrique).
Imaginons notre série avec énormément de bruit; le but de la fonction de scaling va être de décomposer le notre série en plusieurs parties, pour séparer bruit et “tendance”.
Nous avons essayé de coder la fonction nous même (avec l’aide internet), mais elle ne donne pas les résultats escomptés. Ainsi, pour cette question et la suivante, nous utiliserons une librairie pywt
.
np.random.seed(100)
ampl= 100
data_noised= noised_dataset(amplitude= ampl)
w = pywt.Wavelet('haar')
x= data_noised["Return"].values
coeffs = pywt.wavedec(x, w, level= 1)
# EN enlevant le premier niveau du signal, on remarque que la diff de VaR est beaucoup plus failbe
# surtout pour la VaR historique
test= pywt.waverec(coeffs[:-1] + [None] * 1, w)#[:-6]
not_noised_data= pd.DataFrame(test, columns= ["Return"])
fig= plt.figure(figsize= (15, 5))
# print("\t\t\t\t\tPour le premier niveau de projection \n")
not_noised_var= compute_all_var(seuil= 0.99, data= not_noised_data)
plt.plot(x, label= "Noisy log return")
plt.plot(not_noised_data, label= "Denoised log return scale 1")
plt.title("Pour le premier niveau de projection")
plt.legend()
plt.show()
# compute_diff_stab(amplitude= ampl, noised_dataset= not_noised_var)
# sans bruit
fig= plt.figure(figsize= (15, 5))
plt.plot(data_return["Return"].values, color= "orange")
plt.title("Dataset initial (sans bruit)")
plt.show()
On va mesurer la différence des VaR. Ces VaR sont calculés à partir du dataset initial sans bruit et à partir de notre série bruitée (avec une amplitude de 100 (très bruitée)) à laquelle on a appliqué notre fonction de projection de Haar. Comme précédement, on va pouvoir calculer $D_{nor-nois}$.
np.random.seed(100)
ampl= 100
data_noised= noised_dataset(amplitude= ampl)
w = pywt.Wavelet('haar')
x= data_noised["Return"].values
coeffs = pywt.wavedec(x, w, level= 3)
# EN enlevant le premier niveau du signal, on remarque que la diff de VaR est beaucoup plus failbe
# surtout pour la VaR historique
test1= pywt.waverec(coeffs[:-3] + [None] * 3, w)[:-6]
not_noised_data1= pd.DataFrame(test1, columns= ["Return"])
test2= pywt.waverec(coeffs[:-2] + [None] * 2, w)[:-6]
not_noised_data2= pd.DataFrame(test2, columns= ["Return"])
test3= pywt.waverec(coeffs[:-1] + [None] * 1, w)[:-6]
not_noised_data3= pd.DataFrame(test3, columns= ["Return"])
# notre signal bruité
# test4= pywt.waverec(coeffs[:4], w)[:-6]
# not_noised_data4= pd.DataFrame(test4, columns= ["Return"])
fig= plt.figure(figsize= (15, 5))
print("Pour le premier niveau de projection \n")
not_noised_var1= compute_all_var(seuil= 0.99, data= not_noised_data1)
compute_diff_stab(amplitude= ampl, noised_dataset= not_noised_var1)
print("\n\n")
print("Pour le deuxième niveau de projection \n")
not_noised_var2= compute_all_var(seuil= 0.99, data= not_noised_data2)
compute_diff_stab(amplitude= ampl, noised_dataset= not_noised_var2)
print("\n\n")
print("Pour le troisième niveau de projection \n")
not_noised_var3= compute_all_var(seuil= 0.99, data= not_noised_data3)
compute_diff_stab(amplitude= ampl, noised_dataset= not_noised_var3)
plt.plot(x, label= "Noisy log return")
plt.plot(not_noised_data3, label= "Denoised log return scale 3")
plt.plot(not_noised_data2, label= "Denoised log return scale 2")
plt.plot(not_noised_data1, label= "Denoised log return scale 1", color= "orange")
plt.title("Série bruité avec nos 3 niveaux de scale")
plt.legend()
plt.show()
# sans bruit
fig= plt.figure(figsize= (15, 5))
plt.plot(data_return["Return"].values, color= "skyblue")
plt.title("Dataset initial (sans bruit)")
plt.show()
Pour le premier niveau de projection
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Historique est: 0.013501957243983016
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Paramétrique est: 0.003968123471733783
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Non Paramétrique est: 0.004046649638552622
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Pickands est: 0.014973152929154394
Pour le deuxième niveau de projection
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Historique est: 0.009469053272469895
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Paramétrique est: 0.014618361500453283
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Non Paramétrique est: 0.014578622612301275
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Pickands est: 0.008778609841501969
Pour le troisième niveau de projection
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Historique est: 0.03333100420431763
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Paramétrique est: 0.0427223678465152
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Non Paramétrique est: 0.04274974888047833
Pour un bruit blanc gaussien avec une amplitude de 100,
la différence de valeur de la VaR Pickands est: 0.03297989559552157
Pour nos 3 projections, $D_{nor-nois}$ est toujours meilleure que sur notre dataset bruité. Mais la deuxième projection est celle qui a $D_{nor-nois}$ la plus faible pour la VaR empirique et la VaR de Pickands.
Sur la figure que nous pouvons observer au dessus, on se rend compte que notre premier niveau de projection manque d’amplitude, on dirait presque qu’il est constant (courbe orange), et donc on risque d’avoir des valeurs de VaR pour chaques modèles qui s’éloignent vraiment de celles qu’on a pu calculer tout au long de ce tp. Cependant, c’est pour ce niveau de scale que nous avons les meilleures $D_{nor-nois}$ pour la VaR paramétrique et nonparamétrique. Pourquoi ? On pense qu’initialement, les VaR paramétrique et nonparamétrique sont celles avec les valeurs plus faibles (plus proche de 0). Peut-être que le manque d’amplitude de ce scale a permis de conserver une moyenne et un écart-type proche de celui du dataset initial (notre série oscille faiblement autour de 0, qui est notre moyenne de log return sur la série non bruitée).
Et si on prend notre troisième niveau, alors on se rend compte que nous avons beaucoup de bruit (courbe verte).
%matplotlib inline
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib as mpl
import seaborn as sns
from sklearn.neighbors import KernelDensity
from sklearn.linear_model import LinearRegression
import random
import statsmodels.api as sm
from scipy import stats
import scipy.stats
import scipy.special as scsp
from scipy.stats import genextreme
import math
import pywt
import warnings
warnings.filterwarnings("ignore")
mpl.style.use("seaborn")
data= pd.read_csv("^GSPC_3.csv")
# data["Return"]= data["Close"].diff() / data["Close"]
data_copy= data.copy()
data["Return"]= data["Close"].shift(-1) / data["Close"]
data["Return"]= data["Return"].apply(np.log)
data.shape
# La gueule de notre série
plt.plot(data["Return"].values)
plt.title("Evolution du log rendement")
plt.savefig("images/data_return.png")
plt.show
data_return= data[["Date", "Return"]].dropna()
# Question 1
# Empirical quantile
empiri_quant= np.percentile(data_return["Return"], 1)
print("Empirical quantile : {}".format(empiri_quant))
# Paramétrique
alpha_01= scipy.stats.norm.ppf(0.01)
mu= np.mean(data_return["Return"], axis= 0)
# mu= np.mean(vol["Mean"])
sigma= np.std(data_return["Return"], axis= 0)
# sigma= np.mean(vol["sig"])
parametric_quant= alpha_01*sigma + mu
print("\nParametric quantile : {}".format(parametric_quant))
# Nonparametric distribution
# Monte Carlo simulation
np.random.seed(42) # pour fixer l'aléatoire car on fait une simulation
simulations = 1000000
sim_returns = np.random.normal(mu, sigma, simulations)
nonparametric_quant= np.percentile(sim_returns, 1)
# plt.hist(sim_returns, bins= 100)
print("\nNonparametric distribution : {}".format(nonparametric_quant))
# Question 2
# We define k = 10, to get the 5 worst returns
def pickands_estimator_func(data_return= data_return, k= 5):
k= 5
neg_data_return= - data_return["Return"].sort_values(ascending= False).values
n= data_return.shape[0]
extreme_return_k= max(neg_data_return[:n-k+1])
extreme_return_k2= max(neg_data_return[:n-2*k+1])
extreme_return_k4= max(neg_data_return[:n-4*k+1])
return (1/np.log(2))*np.log((extreme_return_k - extreme_return_k2)\
/(extreme_return_k2 - extreme_return_k4))
k= 5
neg_data_return= - data_return["Return"].sort_values(ascending= False).values
n= data_return.shape[0]
extreme_return_k= max(neg_data_return[:n-k+1])
extreme_return_k2= max(neg_data_return[:n-2*k+1])
extreme_return_k4= max(neg_data_return[:n-4*k+1])
pickands_estimator= (1/np.log(2))*np.log((extreme_return_k - extreme_return_k2)\
/(extreme_return_k2 - extreme_return_k4))
pickands_estimator= pickands_estimator_func()
print("Pickands Estimator : {}".format(pickands_estimator))
pickand_estimatored_k= []
neg_data_return= - data_return["Return"].sort_values(ascending= False).values
n= data_return.shape[0]
for k in np.arange(1, 100):
extreme_returned_k= max(neg_data_return[:n-k+1])
extreme_returned_k2= max(neg_data_return[:n-2*k+1])
extreme_returned_k4= max(neg_data_return[:n-4*k+1])
pickand_estimatored_k.append((1/np.log(2))*np.log((extreme_returned_k - extreme_returned_k2)\
/(extreme_returned_k2 - extreme_returned_k4)))
plt.plot(pickand_estimatored_k, c= "purple")
plt.title("Variation de l'estimateur de Pickands avec K")
plt.xlabel("k")
plt.ylabel("Pickland value")
plt.savefig('images/pickands_variation.png')
plt.show()
# Question 3
k= 5
p=0.99
coef= (pow((k/(n*(1-p))), pickands_estimator)-1)/(1-pow(2, -pickands_estimator))
pickands_var_estimator= -(coef*(extreme_return_k - extreme_return_k2) + extreme_return_k) # j'ai mis un - devant
# pour revenir au niveau d'une perte
print("La VaR 99%,grâce à la méthode des valeurs extrèmes est : {}"\
.format(pickands_var_estimator))
# Question 4
b= 10
k= int(n/b) # nécessaire de round
seuil= np.log(0.03) # on définit notre seuil perte à 3%
#la valeur est bien positive car je multiplie mon dataset par -1
neg_value= - data_return["Return"].values #on garde l'ordre de notre dataset
somme_seuil= sum((neg_value > seuil))
# pour chaque partition (y en a k), je regarde si la valeur max dépasse mon seuil critique
somme_bucket_seuil= []
for i in np.arange(1, k+1):
a= (i-1)*b
z= i*b
m= neg_value[a-1:z]
if len(m) != 0:
# dans chacun des buckets, je vérifie si le max dépasse mon seuil
somme_bucket_seuil.append(max(neg_value[a-1:z]) > seuil)
extremal_index= sum(somme_bucket_seuil)/somme_seuil
print("Voici la valeur de l'indice extrêmal de Leadbetter : {}".format(extremal_index))
# La gueule de notre série
plt.plot(data["Return"].values)
plt.axhline(-0.03, color= "red")
plt.title("Evolution du log rendement")
plt.savefig("images/leadbetter_value.png")
plt.show()
sum(somme_bucket_seuil)
# Question 6
# La seule méthode que nous avons trouvé qui permettait d'utiliser l'exposant de Hurst et la autocorrélation
# du mouvement brwonien fractionnaire est la prédiction de la log volatilité
# Calculons dans un premier temps un suite de log(return) mais pour plusieurs échelles de tps
data_copy= data.copy()
def return_per_i(i, data_copy= data_copy):
data_copy["Return"+str(i)]= (data_copy["Close"].shift(-i) / data_copy["Close"]).apply(np.log)
stocked= data_copy["Return"+str(i)].values[:-i].tolist()
return stocked
# stock de notre valeur a régresser
final_stock= []
m_stocked= []
for i in [1, 2, 7, 30, 90, 100, 200, 252, 360]:
stocked= return_per_i(i)
n1= len(stocked)
hurst_regression_data= []
m_data= []
for m in np.arange(1,len(stocked)):
m_data.append(m)
k= int(n1/m)
agg_return= []
for i in np.arange(1, k+1):
t= 1+m*(i-1)
agg_return.append(np.sum(stocked[t:k*m])/m)
hurst_regression_data.append(np.sum(np.abs(agg_return[0: k]))/k)
final_stock += hurst_regression_data
m_stocked += m_data
# x_log= np.log(np.arange(1, len(stocked)))
# x_log= np.log(np.arange(1, 668).tolist()*6)[:-3]
x_log= np.log(m_stocked)
intercept= np.repeat(1, len(m_stocked))
x= np.column_stack((intercept, x_log))
y= final_stock
lr= LinearRegression(fit_intercept= True).fit(x, y)
h= lr.coef_[1] + 1 #mon exposant de Hurst
h
sigtested= data[["Date", "Close"]]
sigtested["Close"]= np.log(data["Close"])
sigtested["Date"]= pd.to_datetime(sigtested["Date"], format= "%Y-%m-%d")
sigtested.set_index(["Date"], inplace= True)
sigtested["Return"]= sigtested["Close"].shift(-1) / sigtested["Close"]
sigtested= sigtested[["Return"]].dropna()
def c_tilde(h):
return scsp.gamma(3. / 2. - h) / scsp.gamma(h + 1. / 2.) * scsp.gamma(2. - 2. * h)
def forecast_XTS(rvdata, h, date, nLags, delta, nu):
i = np.arange(nLags)
cf = 1./((i + 1. / 2.) ** (h + 1. / 2.) * (i + 1. / 2. + delta))
ldata = rvdata.truncate(after=date)
# ldata= rvdata
l = len(ldata)
ldata = np.log(ldata.iloc[l - nLags:])
ldata['cf'] = np.fliplr([cf])[0]
ldata = ldata.dropna()
fcst = (ldata.iloc[:, 0] * ldata['cf']).sum() / sum(ldata['cf'])
return math.exp(fcst + 2 * nu ** 2 * c_tilde(h) * delta ** (2 * h))
x = np.arange(1, 100)
def dlsig2(sic, x, pr=False):
if pr:
a= np.array([(sig-sig.shift(lag)).dropna() for lag in x])
a=a ** 2
return [np.mean((sig-sig.shift(lag)).dropna() ** 2) for lag in x]
nu_= []
sig = sigtested
sig = np.log(np.sqrt(sig))
sig = sig.dropna()
model = np.polyfit(np.log(x), np.log(dlsig2(sig, x)), 1)
nu_.append(np.sqrt(np.exp(model[1])))
nu= nu_[0]
rvdata= sigtested
n = len(rvdata)
delta = 1
nLags = 300
dates = rvdata.iloc[nLags:n-delta].index
rv_predict = [forecast_XTS(rvdata, h=h, date=d, nLags=nLags,
delta=delta, nu=nu) for d in dates]
rv_actual = rvdata.iloc[nLags+delta:n].values
nu= nu_[0]
rvdata= sigtested
n = len(rvdata)
delta = 1
nLags = 300
dates = rvdata.iloc[nLags:n-delta].index
rv_predict = [forecast_XTS(rvdata, h=h, date=d, nLags=nLags,
delta=delta, nu=nu) for d in dates]
rv_actual = rvdata.iloc[nLags+delta:n].values
dates
new_date= ['2020-02-12', '2020-02-13', '2020-02-14', '2020-02-15', '2020-02-16'
, '2020-02-17', '2020-02-18', '2020-02-19', '2020-02-20', '2020-02-21'
, '2020-02-22', '2020-02-23', '2020-02-24', '2020-02-25', '2020-02-26']
rv_predict_test = [forecast_XTS(rvdata, h=h, date=d, nLags=nLags,
delta=delta, nu=nu) for d in new_date]
# Prédiction du return en rouge
rv_actual_r = (rvdata.iloc[nLags+delta:n].shift(-1) - rvdata.iloc[nLags+delta:n]).values
rv_predict_r= pd.DataFrame(rv_predict, columns= ["Return"])
rv_predict_r= (rv_predict_r["Return"].shift(-1) - rv_predict_r["Return"]).values
plt.plot(rv_actual_r, "blue")
plt.plot(rv_predict_r, "red")
plt.savefig("images/predicted_actual_vol.png")
plt.show()
# Question 7
# A partir d'une VaR on peut calculer une ES.
# Un des problèmes de la VaR c'est qu'on a aucune idée de ce qu'il
# se passe outre passé notre dite-VaR. L'expected shortfall est la moyenne
# des rendements une fois passé notre VaR. Ainsi, on garde l'information
# de la gueule de notre queue après la VaR.
# On va donc calculer la moyenne aprèsn notre VaR
# ES empirique
empiri_quant
empiri_es= np.mean(data_return[data_return["Return"] < empiri_quant]["Return"])
print(empiri_es)
# # ES parametrique
parametric_quant
parametric_es= np.mean(data_return[data_return["Return"] < parametric_quant]["Return"])
print(parametric_es)
# # ES non-parametrique
nonparametric_quant
nonparametric_es= np.mean(data_return[data_return["Return"] < nonparametric_quant]["Return"])
print(nonparametric_es)
# # ES Pickands
pickands_var_estimator
pickands_es= np.mean(data_return[data_return["Return"] < pickands_var_estimator]["Return"])
print(pickands_es)
# Question 8
from sklearn.model_selection import train_test_split
np.random.seed(55)
estimation, backtest= train_test_split(data_return["Return"], test_size= 0.3)
# Empirical quantile
empiri_quant_esti= np.percentile(estimation, 1)
# Parametric quantile
# alpha_01= -3.090232
# vol_esti= pd.DataFrame()
# vol_esti["Return"]= estimation.copy()
# vol_esti["Return_t1"]= vol_esti["Return"].shift(-1)
# vol_esti["Mean"]= (vol_esti["Return"] + vol_esti["Return_t1"])/2
# vol_esti["sig"]= np.sqrt(pow(vol_esti["Return"] - vol_esti["Mean"], 2) + pow(vol_esti["Return_t1"] - vol_esti["Mean"], 2))
alpha_01= scipy.stats.norm.ppf(0.01)
mu= np.mean(estimation, axis= 0)
# mu_esti= np.mean(vol_esti["Mean"])
sigma= np.std(estimation, axis= 0)
# sigma_esti= np.mean(vol_esti["sig"])
parametric_quant_esti= alpha_01*sigma + mu
# Nonparametric distribution
np.random.seed(42)
simulations = 1000000
sim_returns = np.random.normal(mu, sigma, simulations)
nonparametric_quant_esti= np.percentile(sim_returns, 1)
# Pickands VaR
k= 5
neg_data_return= - estimation.sort_values(ascending= False).values
n= estimation.shape[0]
extreme_return_k= max(neg_data_return[:n-k+1])
extreme_return_k2= max(neg_data_return[:n-2*k+1])
extreme_return_k4= max(neg_data_return[:n-4*k+1])
pickands_estimator_esti= (1/np.log(2))*np.log((extreme_return_k - extreme_return_k2)\
/(extreme_return_k2 - extreme_return_k4))
p=0.99
coef= (pow((k/(n*(1-p))), pickands_estimator_esti)-1)/(1-pow(2, -pickands_estimator_esti))
pickands_var_estimator_esti= -(coef*(extreme_return_k - extreme_return_k2) + extreme_return_k) # j'ai mis un - devant
# pour revenir au niveau d'une perte
# ES
# ES empirique
empiri_es_esti= np.mean(estimation[estimation < empiri_quant_esti])
# # ES parametrique
parametric_es_esti= np.mean(estimation[estimation < parametric_quant_esti])
# # ES non-parametrique
nonparametric_es_esti= np.mean(estimation[estimation < nonparametric_quant_esti])
# # ES Pickands
pickands_es_esti= np.mean(estimation[estimation < pickands_var_estimator_esti])
fig= plt.figure(figsize=(10, 7))
stock_value= {
"VaR empirique":empiri_quant_esti,
"VaR paramétrique":parametric_quant_esti,
"VaR Non-paramétrique":nonparametric_quant_esti,
"VaR Pickands":pickands_var_estimator_esti,
"ES empirique":empiri_es_esti,
"ES paramétrique":parametric_es_esti,
"ES Non-paramétrique":nonparametric_es_esti,
"ES Pickands":pickands_es_esti,
}
# Distribution de notre echantillon de backtest
plt.hist(backtest, bins= 30, color= "silver")
# VaR
plt.axvline(empiri_quant_esti, color= "red", label= "VaR empirique")
plt.axvline(parametric_quant_esti, color= "brown", label= "VaR paramétrique")
plt.axvline(nonparametric_quant_esti, color= "orange", label= "VaR non paramétrique")
plt.axvline(pickands_var_estimator_esti, color= "coral", label= "VaR Pickands")
# ES
plt.axvline(empiri_es_esti, color= "dodgerblue", label= "ES empirique")
plt.axvline(parametric_es_esti, color= "aqua", label= "ES paramétrique")
plt.axvline(nonparametric_es_esti, color= "skyblue", label= "ES non paramétrique")
plt.axvline(pickands_es_esti, color= "royalblue", label= "ES Pickands")
plt.legend(loc='best', frameon= True, shadow= True, framealpha= 0.9)
plt.title("Distribution de notre échantillon de test (avec {} observations)".format(len(backtest))\
, size= 18)
plt.xlabel("Rendement")
plt.ylabel("Fréquence")
plt.savefig("images/var_es_backtest.png")
plt.show()
# backtesting : Conditional Test by Acerbi and Szekely
num_hit= sum(backtest < nonparametric_quant_esti) #nombre de fois ou mon rendement est en dessous de mon seuil
# if num_hist > 0:
# else:
# print("Pas possible")
# np.sum(((backtest[backtest < nonparametric_quant_esti] / nonparametric_es_esti) + 1)) / num_hit
def couverture_nonconditionnelle(seuil, data= backtest):
"""
C'est la proba qu'un rendement soit inférieur à mon seuil
"""
return sum(data < seuil)/len(data)
def test_couverture_nonconditionnelle(seuil, data= backtest, p= 0.01):
"""
On cherche à savoir si la proba de couverture non conditionnelle est statistiquement
différente de 0. On fait donc que le ratio num_hit/n tend vers notre proba de risque alpha
et que le nombre total de num_hit suit une loi binomiale.
On va comparer notre valeur compute au quantile de la loi normale centrée réduite
"""
num_hit= sum(data < seuil)
n= len(data)
return (num_hit - p*n)/np.sqrt(p*(1-p)*n) # suit suivre une loi normale centrée réduite
def test_normality_nonconditionnelle(seuil, data= backtest, p= 0.01):
import scipy.stats
if np.abs(test_couverture_nonconditionnelle(seuil= seuil, data= data, p= p)) < scipy.stats.norm.ppf(1-p):
# notre mesure du risque est "bonne" (on sur-estime le risque)
# print("On accepte l'hypothèse H0 : notre probabilité de hit est égale à notre seuil alpha= {}".format(p))
return "On accepte H0"
else:
# print("On rejète l'hypothèse H0")
return "On rejète H0"
# couverture_nonconditionnelle(nonparametric_quant_esti)
# test_couverture_nonconditionnelle(nonparametric_quant_esti)
test_normality_nonconditionnelle(nonparametric_quant_esti)
test_normality_nonconditionnelle(pickands_es_esti)
def stat_kupiec(seuil, data= backtest, p= 0.01):
num_hit= sum(data < seuil)
n= len(data)
minus= -2*np.log(pow(1-p, n - num_hit)*pow(p, num_hit))
plus= 2*np.log(pow(1- (num_hit/n), n - num_hit)*pow(num_hit, num_hit))
return minus + plus
def test_chi_nonconditionnelle(seuil, data= backtest, p= 0.01, k= 1): # 1 degree de liberté
import scipy.stats
if stat_kupiec(seuil= seuil, data= data, p= p) < scipy.stats.chi2.ppf(1-p, df= k):
# notre mesure du risque est "bonne" (on sur-estime le risque)
# print("On accepte l'hypothèse H0 : notre probabilité de hit est égale à notre seuil alpha= {}".format(p))
return "On accepte H0"
else:
# print("On rejète l'hypothèse H0")
return "On rejète H0"
stock_chi= {
"VaR empirique":test_chi_nonconditionnelle(empiri_quant_esti),
"VaR paramétrique":test_chi_nonconditionnelle(parametric_quant_esti),
"VaR Non-paramétrique":test_chi_nonconditionnelle(nonparametric_quant_esti),
"VaR Pickands":test_chi_nonconditionnelle(pickands_var_estimator_esti),
"ES empirique":test_chi_nonconditionnelle(empiri_es_esti),
"ES paramétrique":test_chi_nonconditionnelle(parametric_es_esti),
"ES Non-paramétrique":test_chi_nonconditionnelle(nonparametric_es_esti),
"ES Pickands":test_chi_nonconditionnelle(pickands_es_esti),
}
stock_norm= {
"VaR empirique":test_normality_nonconditionnelle(empiri_quant_esti),
"VaR paramétrique":test_normality_nonconditionnelle(parametric_quant_esti),
"VaR Non-paramétrique":test_normality_nonconditionnelle(nonparametric_quant_esti),
"VaR Pickands":test_normality_nonconditionnelle(pickands_var_estimator_esti),
"ES empirique":test_normality_nonconditionnelle(empiri_es_esti),
"ES paramétrique":test_normality_nonconditionnelle(parametric_es_esti),
"ES Non-paramétrique":test_normality_nonconditionnelle(nonparametric_es_esti),
"ES Pickands":test_normality_nonconditionnelle(pickands_es_esti),
}
result_test= pd.DataFrame()
result_test["Méthode"]= stock_chi.keys()
result_test["Valeur"]= result_test["Méthode"].map(stock_value)
result_test["Test non conditionnel"]= result_test["Méthode"].map(stock_norm)
result_test["Test Kupiec"]= result_test["Méthode"].map(stock_chi)
print(result_test)
# Question 9
# Question 10
# On va faire N tirage de avec remise dans notre dataset de 10 log return, puis en faire la moyenne
# et l'écart type
random.seed(55)
mu_para= []
sig_para= []
var_para= []
all_return= data_return["Return"].values
k= 10
alpha_01= scipy.stats.norm.ppf(0.01)
for i in range(100000):
random_values= random.choices(all_return, k= k)
mu_test= np.mean(random_values)
sig_test= np.std(random_values)
var_para.append(alpha_01*sig_test + mu_test)
mu_para.append(mu_test)
sig_para.append(sig_test)
def compare_hist_to_norm(data, bins= 25):
fig= plt.figure(figsize= (10, 5))
from scipy.stats import skewnorm
# mu, std, sk, k= scipy.stats.norm.stats(data, moments='mvsk')
mu, std = scipy.stats.norm.fit(data)
# params= scipy.stats.norm.fit(data)
# sk= scipy.stats.skewnorm(data)
# Plot l'hist
plt.hist(data, bins= bins, density=True, alpha=0.6, color='purple', label= "Données")
# Plot le PDF.
xmin, xmax = plt.xlim()
X = np.linspace(xmin, xmax)
plt.plot(X, scipy.stats.norm.pdf(X, mu, std), label= "Normal Distribution")
plt.plot(X, skewnorm.pdf(X, *skewnorm.fit(data)), color= 'black', label= "Skewed Normal Distribution")
mu, std = scipy.stats.norm.fit(data)
sk= scipy.stats.skew(data)
title2 = "Moments mu: {}, sig: {}, sk: {}".format(round(mu, 4), round(std, 4), round(sk, 4))
plt.ylabel("Fréquence", rotation= 90)
plt.title(title2)
plt.legend()
plt.show()
compare_hist_to_norm(mu_para, 50)
compare_hist_to_norm(sig_para, 50)
compare_hist_to_norm(var_para, 50)
# Question 11
# si le prix suit une gaussienne iid, alors on va générer plusieurs distribution de prix en
# prenant compte de la moyenne et de la volatilité de mes prix
# Attention, nous travaillons sur des log(prix)
data_second= data_return["Return"]
# les 2 premiers moments de mes log prix
mean_lp= np.mean(data_second, axis= 0)
sig_lp= np.std(data_second, axis= 0)
pick_= []
extreme_k_= []
extreme_k2_= []
extreme_k4_= []
var_pick_= []
for i in range(10000):
simulations= 1000 # on va générer 1000 prix gaussian iid
log_prix= pd.DataFrame(np.random.normal(mean_lp, sig_lp, simulations), columns= ["Close"])
log_rendement= pd.DataFrame(np.random.normal(mean_lp, sig_lp, simulations), columns= ["Return"])
# calcul var evt
returns_= pd.DataFrame(log_rendement["Return"].values, columns= ["Return"]).dropna()
k= 5
neg_data_return= - returns_["Return"].sort_values(ascending= False).values
n= returns_.shape[0]
extreme_return_k= max(neg_data_return[:n-k+1])
extreme_return_k2= max(neg_data_return[:n-2*k+1])
extreme_return_k4= max(neg_data_return[:n-4*k+1])
pickands_estimator= (1/np.log(2))*np.log((extreme_return_k - extreme_return_k2)\
/(extreme_return_k2 - extreme_return_k4))
k= 5
p=0.99
coef= (pow((k/(n*(1-p))), pickands_estimator)-1)/(1-pow(2, -pickands_estimator))
pickands_var_estimator= -(coef*(extreme_return_k - extreme_return_k2) + extreme_return_k)
pick_.append(pickands_estimator)
extreme_k_.append(extreme_return_k)
extreme_k2_.append(extreme_return_k2)
extreme_k4_.append(extreme_return_k4)
var_pick_.append(pickands_var_estimator)
compare_hist_to_norm(pick_, 30)
compare_hist_to_norm(extreme_k_, 30)
compare_hist_to_norm(extreme_k2_, 30)
compare_hist_to_norm(extreme_k4_, 30)
compare_hist_to_norm(var_pick_, 30)
# Question 12
# Question 13
mu= np.mean(data_return["Return"].values)
sigma= np.std(data_return["Return"].values)
stock_nonpara= []
for i in range(1000):
simulations = 1000
sim_returns = np.random.normal(mu, sigma, simulations)
nonparametric_quant= np.percentile(sim_returns, 1)
stock_nonpara.append(nonparametric_quant)
obs_dist= stock_nonpara
fig = plt.figure(figsize=(12, 5))
ax = fig.add_subplot(111)
ax.hist(obs_dist, bins=20, density=True, label='Données',
zorder=5, edgecolor='k', alpha=0.5)
for i in [0.0009, 0.0005, 0.0001, 0.00005]:
kde = sm.nonparametric.KDEUnivariate(obs_dist)
kde.fit(bw= i)
ax.plot(kde.support, kde.density, lw=3, label='KDE avec h={}'.format(i), zorder=10)
ax.legend(loc='best')
ax.grid(True, zorder=-5)
# QUestion 14
def compute_all_var(seuil= 0.99, data= data_return):
estimation= data["Return"]
# Empirical quantile
empiri_quant_esti= np.percentile(estimation, (1-seuil)*100)
# Parametric quantile
# alpha_01= -3.090232
# vol_esti= pd.DataFrame()
# vol_esti["Return"]= estimation.copy()
# vol_esti["Return_t1"]= vol_esti["Return"].shift(-1)
# vol_esti["Mean"]= (vol_esti["Return"] + vol_esti["Return_t1"])/2
# vol_esti["sig"]= np.sqrt(pow(vol_esti["Return"] - vol_esti["Mean"], 2) + pow(vol_esti["Return_t1"] - vol_esti["Mean"], 2))
alpha_01= scipy.stats.norm.ppf(1-seuil)
mu= np.mean(estimation, axis= 0)
# mu_esti= np.mean(vol_esti["Mean"])
sigma= np.std(estimation, axis= 0)
# sigma_esti= np.mean(vol_esti["sig"])
parametric_quant_esti= alpha_01*sigma + mu
# Nonparametric distribution
# np.random.seed(42)
simulations = 1000000
sim_returns = np.random.normal(mu, sigma, simulations)
nonparametric_quant_esti= np.percentile(sim_returns, (1-seuil)*100)
# Pickands VaR
k= 5
neg_data_return= - estimation.sort_values(ascending= False).values
n= estimation.shape[0]
extreme_return_k= max(neg_data_return[:n-k+1])
extreme_return_k2= max(neg_data_return[:n-2*k+1])
extreme_return_k4= max(neg_data_return[:n-4*k+1])
pickands_estimator_esti= (1/np.log(2))*np.log((extreme_return_k - extreme_return_k2)\
/(extreme_return_k2 - extreme_return_k4))
p= seuil
coef= (pow((k/(n*(1-p))), pickands_estimator_esti)-1)/(1-pow(2, -pickands_estimator_esti))
pickands_var_estimator_esti= -(coef*(extreme_return_k - extreme_return_k2) + extreme_return_k) # j'ai mis un - devant
# pour revenir au niveau d'une perte
return (empiri_quant_esti, parametric_quant_esti, nonparametric_quant_esti, pickands_var_estimator_esti)
def compute_all_es(seuil= 0.99):
estimation= data_return["Return"]
res= compute_all_var(seuil)
# ES empirique
empiri_es_esti= np.mean(estimation[estimation < res[0]])
# # ES parametrique
parametric_es_esti= np.mean(estimation[estimation < res[1]])
# # ES non-parametrique
nonparametric_es_esti= np.mean(estimation[estimation < res[2]])
# # ES Pickands
pickands_es_esti= np.mean(estimation[estimation < res[3]])
return [empiri_es_esti, parametric_es_esti, nonparametric_es_esti, pickands_es_esti]
i= 0.99
val_min, idx_min = min((val, idx) for (idx, val) in enumerate(compute_all_es(i)))
val_max, idx_max = max((val, idx) for (idx, val) in enumerate(compute_all_es(i)))
print("Diameter ES pour le seuil {}: {}".format(i, -val_max + val_min))
val_min, idx_min = min((val, idx) for (idx, val) in enumerate(compute_all_var(i)))
val_max, idx_max = max((val, idx) for (idx, val) in enumerate(compute_all_var(i)))
print("Diameter VaR pour le seuil {}: {}".format(i, -val_max + val_min))
print("\n")
# Question 15
for i in [0.99, 0.90, 0.95, 0.98, 0.995, 0.999, 0.9999]:
val_min, idx_min = min((val, idx) for (idx, val) in enumerate(compute_all_es(i)))
val_max, idx_max = max((val, idx) for (idx, val) in enumerate(compute_all_es(i)))
print("Diameter ES pour le seuil {}: {}".format(i, -val_max + val_min))
val_min, idx_min = min((val, idx) for (idx, val) in enumerate(compute_all_var(i)))
val_max, idx_max = max((val, idx) for (idx, val) in enumerate(compute_all_var(i)))
print("Diameter VaR pour le seuil {}: {}".format(i, -val_max + val_min))
print("\n")
# certaines valeurs ne peuvant pas être calculés (ES) car pour un seuil donné, nous n'avons pas
# suffisamment de données hist.
# Dans tous les cas, l'écart est plus faible avec l'ES que la VaR ==> moins de variance entre
# les modèles ES que VaR
# Question 16
# np.random.seed(55)
def noised_dataset(amplitude= 1):
val= data["Close"]
noise= pd.DataFrame(np.random.normal(0, amplitude, len(val)).tolist(), columns= ["Noise"])
data_noised= pd.DataFrame(val.values, columns= ["Close"])
data_noised["Close_noised"]= data_noised["Close"] + noise["Noise"]
# data_noised.head()
data_noised["Return"]= np.log((data_noised["Close_noised"].shift(-1) / data_noised["Close_noised"]))
data_noised= data_noised.dropna()
return data_noised
data_noised= noised_dataset()
plt.plot(data_noised["Return"])
# archi bruité
good_var= compute_all_var()
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
def compute_diff_stab(amplitude, noised_dataset, gv= good_var):
type_var= ["Historique", "Paramétrique", "Non Paramétrique", "Pickands"]
for i in np.arange(0, 4):
diff= np.abs(np.abs(good_var[i]) - np.abs(noised_dataset[i]))
print("Pour un bruit blanc gaussien avec une amplitude de {},\n la différence de valeur de la VaR {} est: {}\n"\
.format(amplitude, type_var[i], diff))
pass
ampl= 1
data_noised= noised_dataset(amplitude= ampl)
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
# pour un bruit blanc d'amplitude 1, c'est la VaR Pickands qui est la plus robuste
ampl= 5
data_noised= noised_dataset(amplitude= ampl)
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
# pour un bruit blanc d'amplitude 5, c'est la VaR Paramétrique qui est la plus robuste
ampl= 20
data_noised= noised_dataset(amplitude= ampl)
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
# pour un bruit blanc d'amplitude 20, c'est la VaR Historique qui est la plus robuste
ampl= 100
data_noised= noised_dataset(amplitude= ampl)
plt.plot(data_noised["Return"])
noised_var= compute_all_var(seuil= 0.99, data= data_noised)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
# pour un bruit blanc d'amplitude 100, c'est la VaR Paramétrique qui est la plus robuste
# Question 17
import pywt
ampl= 100
data_noised= noised_dataset(amplitude= ampl)
w = pywt.Wavelet('haar')
x= data_noised["Return"].values
# plt.plot(w.dec_lo)
coeffs = pywt.wavedec(x, w, level= 1)
def reconstruction_plot(yyy, **kwargs):
"""Plot signal vector on x [0,1] independently of amount of values it contains."""
plt.plot(np.linspace(0, 1, len(yyy)), yyy, **kwargs)
def haarFWT ( signal, level ):
s = .5; # scaling -- try 1 or ( .5 ** .5 )
h = [ 1, 1 ]; # lowpass filter
g = [ 1, -1 ]; # highpass filter
f = len ( h ); # length of the filter
t = signal; # 'workspace' array
l = len ( t ); # length of the current signal
y = [0] * l; # initialise output
# t = t + [ 0, 0 ]; # padding for the workspace
for i in range ( level ):
y [ 0:l ] = [0] * l; # initialise the next level
l2 = l // 2; # half approximation, half detail
for j in range ( l2 ):
for k in range ( f ):
y [j] += t [ 2*j + k ] * h [ k ] * s;
y [j+l2] += t [ 2*j + k ] * g [ k ] * s;
l = l2; # continue with the approximation
t [ 0:l ] = y [ 0:l ] ;
return y
plt.figure(figsize= (10, 5))
plt.plot(x)
plt.plot(haarFWT(x.tolist(), 1))
# plt.plot(data_return["Return"].values)
# plt.plot(haarFWT(x.tolist(), 2))
# plt.plot(haarFWT(x.tolist(), 3))
# plt.plot(haarFWT(x.tolist(), 4))
# plt.plot(haarFWT(x.tolist(), 5))
plt.legend(["Données", "notre lvl 1"])
# j'arrive pas a le développer moi même car ça ne marche pas
# EN enlevant le premier niveau du signal, on remarque que la diff de VaR est beaucoup plus failbe
# surtout pour la VaR historique
# test= data_noised["Return"].values - pywt.waverec(coeffs[:-3] + [None] * 3, w)[:-6]
test= pywt.waverec(coeffs[:-1] + [None] * 1, w)#[:-6]
not_noised_data= pd.DataFrame(test, columns= ["Return"])
plt.hist(test, bins= 30)
plt.show()
fig= plt.figure(figsize= (10, 5))
ampl= 10
print("Pour le premier niveau de projection \n")
noised_var= compute_all_var(seuil= 0.99, data= not_noised_data)
plt.plot(not_noised_data)
compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
# test= data_noised["Return"].values - pywt.waverec(coeffs[:-2] + [None] * 2, w)[:-6]
# not_noised_data= pd.DataFrame(test, columns= ["Return"])
# ampl= 10
# print("Pour le deuxième niveau de projection \n")
# noised_var= compute_all_var(seuil= 0.99, data= not_noised_data)
# # plt.plot(not_noised_data)
# compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
# test= data_noised["Return"].values - pywt.waverec(coeffs[:-1] + [None] * 1, w)[:-2]
# not_noised_data= pd.DataFrame(test, columns= ["Return"])
# ampl= 10
# print("Pour le troisième niveau de projection \n")
# noised_var= compute_all_var(seuil= 0.99, data= not_noised_data)
# # plt.plot(not_noised_data)
# compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)
# ampl= 10
# data_noised= noised_dataset(amplitude= ampl)
# plt.plot(data_noised["Return"])
# noised_var= compute_all_var(seuil= 0.99, data= data_noised)
# compute_diff_stab(amplitude= ampl, noised_dataset= noised_var)