Blog

Gestion de risque

Aug 13, 2020 | 37 minutes de lecture

Projet de gestion de risque par Paul Leydier et Wenceslas Sanchez


Plan:

  1. About various calculation methods for the VaR

  2. From VaR to ES

  3. Model risk as estimation uncertainty

  4. Model risk as specification uncertainty

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()

png

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:

  • VaR empirique: il suffit de tracer la distribution de nos rendement, et de récupérer le seuil à partir duquel 99% des rendements sont supérieurs au seuil.
  • VaR paramétrique: on fait l’hypothèse que nos rendements suivent une loi de probabilité (ici une loi normale). On récupère les 2 premiers moments de notre série historique pour les appliquer à notre loi et déterminer le quantile 99%.
  • VaR non-paramétrique: on va faire l’hypothèse que nos rendements suivent une loi de probabilité (toujours la loi normale), on récupère les caractéristiques de notre série de rendement (moyenne et variance); mais on va simuler des rendements étant donné la loi choisie et les 2 premiers moments (simulation de Monte-Carlo). Dans notre cas, on a simulé 1,000,000 de rendements gaussiens (par conséquent on risque de se rapprocher de la VaR paramétrique)

Dans notre cas:

  • VaR empirique $= -0.02685$
  • VaR paramétrique $= -0.01852$
  • VaR non-paramétrique $= -0.01856$

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)

png

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()

png

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:

  • $u$ notre seuil
  • $N$ le nombre d’observations dans notre queue (côté perte)
  • $N_u$ le nombre d’obsersations dans notre queue côté perte supérieures à notre seuil

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()

png

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:

  • en calculant la moyenne
  • en calculant l’intégrale (même méthode)

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()

png

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

png

                     Distribution de l'écart-type

png

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

png

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

png

                     Distribution de l'extrème k

png

                     Distribution de l'extrème k2

png

                     Distribution de l'extrème k4

png

print("\t\t\t\t\t Distribution de la VaR Pickands")
compare_hist_to_norm(var_pick_, 30)
                     Distribution de la VaR Pickands

png

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)

png

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

png

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

png

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

png

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

png

  • 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()

png

png

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

png

png

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).

Code

%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)