Demander « et si ? » plutôt que « quelle probabilité ? »
La VaR et l'expected shortfall sont estimées à partir des rendements passés, donc elles ne voient pas ce qui ne s'est jamais produit. Un test de résistance abandonne les probabilités et applique un scénario que l'on choisit : un krach de 20 % des actions, une hausse de 2 points des taux, la crise de 2008 rejouée, une dévaluation. La perte du portefeuille dans ce scénario est un chiffre dont un comité des risques peut discuter, et elle révèle des concentrations qu'une moyenne statistique cache.
import numpy as np
values = np.array([500000.0, 300000.0, -200000.0]) # two longs and a short
shocks = np.array([-0.20, -0.10, 0.15]) # one scenario: stocks -20%, bonds -10%, the shorted stock +15%
pnl = values * shocks # per position
print(pnl) # [-100000. -30000. -30000.] the short loses when its stock rises
print(float(pnl.sum())) # -160000.0Un tableau de scénarios
Les équipes de risque tiennent une liste permanente de scénarios, historiques (1987, 2008, mars 2020) et hypothétiques. Placez-les dans une matrice avec une ligne par scénario et une colonne par position : la perte totale sous chaque scénario est alors un seul produit matrice-vecteur, scenarios @ values. Prendre le minimum de ce vecteur désigne le pire cas, et le scénario qui fait le plus mal est en soi une information, car il dit où le portefeuille est exposé.
import numpy as np
values = np.array([500000.0, 300000.0, -200000.0])
scenarios = np.array([[-0.20, -0.10, 0.15], # equity crash
[-0.05, 0.02, 0.00], # mild dip
[ 0.10, 0.05, -0.20]]) # the shorted stock falls 20%: the short gains
names = ["equity crash", "mild dip", "rally and short gain"]
pnl = scenarios @ values
for name, p in zip(names, pnl):
print(name, round(float(p)))
worst = int(np.argmin(pnl))
print("worst:", names[worst], round(float(pnl[worst])))# A shock does not have to be a price move: a rate jump hits a bond through its duration
duration = 7.5 # years, from the duration topic
bond_value = 1_000_000.0
rate_jump = 0.02 # +2 percentage points
print(round(bond_value * (-duration * rate_jump))) # -150000 the shock expressed as a price return of -15%Le test de résistance inversé
Inversons la question : quelle gravité de choc nous coûterait un montant donné, par exemple le coussin de capital ? Prenons une direction de chocs (par exemple, tout baisse ensemble) et multiplions-la par un facteur k. La perte est -k * (values @ direction), linéaire en k, donc le facteur qui perd exactement loss_limit est loss_limit / -(values @ direction). Si la direction ne fait pas perdre d'argent, aucun facteur n'atteint jamais la limite.
import numpy as np
def reverse_stress(values, direction, loss_limit):
change = float(values @ direction)
if change >= 0:
return float("inf")
return loss_limit / -change
values = np.array([500000.0, 300000.0, -200000.0])
direction = np.array([-1.0, -1.0, 0.0]) # the two longs fall by the same percentage
k = reverse_stress(values, direction, 100000.0)
print(round(k, 4)) # 0.125 a 12.5% fall in both longs loses 100,000
print(round(float(-(values @ (k * direction))), 2)) # 100000.0- Un test de résistance applique des chocs choisis : le pnl d'une position est
values * shocks, négatif pour les pertes, positif pour la vente à découvert qui gagne. - Plusieurs scénarios à la fois, c'est un produit,
scenarios @ values; le minimum désigne le pire cas et montre où le portefeuille est exposé. - Test inversé : le multiple d'un choc qui perd exactement la limite est
loss_limit / -(values @ direction), ou l'infini si la direction ne perd jamais.
À suivre : les modèles ARIMA, prévoir une série à partir de son propre passé.
Lab : ce qui est noté
- Frapper chaque position avec
shocked_pnl25 pts - Lancer plusieurs scénarios d'un coup avec
scenario_pnl25 pts - Nommer le pire cas avec
worst_scenario25 pts - Remonter d'une perte avec
reverse_stress25 pts