σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyParcours
Ctrl K
AO
+1,2 k apprenants
ConnexionRejoindre ↗
Parcours
AO
+1,2 k apprenants
Connexion
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Parcours Python›Module 4 · Valorisation des produits de taux et d'options›Sujet 4.13·~50 minIntermédiaire

Volatilité implicite

Position dans le parcoursModule 4 sur 11 · Sujet 13 sur 16
4.13 Volatilité implicite
—Score du lab— / 100XP total—Série—

Faire tourner la formule à l'envers

La formule de Black–Scholes transforme une volatilité en prix. Sur le marché, c'est l'inverse : le prix est coté et la volatilité est inconnue. La volatilité implicite est le sigma pour lequel la formule redonne le prix du marché. Il n'y a pas d'inverse algébrique, donc on cherche : on calcule le prix pour un sigma d'essai, on regarde l'écart de prix (modèle moins marché), et on déplace sigma jusqu'à l'annuler. La recherche la plus simple essaie une grille de valeurs et garde la meilleure.

import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def bs_call(spot, strike, rate, sigma, t):
    d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))

market = 8.0                                   # the quoted price of the 100-strike, 6-month call on a stock at 100
for sigma in (0.10, 0.15, 0.20, 0.25, 0.30):
    model = bs_call(100, 100, 0.05, sigma, 0.5)
    print(sigma, round(model, 4), "error", round(model - market, 4))

Pourquoi il n'y a qu'une seule réponse

Le prix du call croît régulièrement avec sigma : le véga est positif, donc l'écart ne passe par zéro qu'une fois. La réponse est donc unique, et on sait dans quel sens bouger : si le modèle est trop cher, on baisse sigma. Une solution n'existe que si le prix du marché est strictement compris entre le *plancher intrinsèque* max(S - K * exp(-rT), 0) (volatilité nulle) et le spot S (volatilité infinie). En dehors, la cotation contredit l'absence d'arbitrage, et la bonne réponse est une erreur, pas un nombre.

# The bounds: no volatility can explain a price outside them
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def bs_call(spot, strike, rate, sigma, t):
    d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))

spot, strike, rate, t = 100, 90, 0.05, 1
floor = max(spot - strike * math.exp(-rate * t), 0)
print(round(floor, 4), spot)                                 # 14.3894 100
print(round(bs_call(spot, strike, rate, 0.0001, t), 4))      # 14.3894   tiny volatility: the floor
print(round(bs_call(spot, strike, rate, 5.0, t), 4))         # 98.8513   huge volatility: close to the spot

Couper l'intervalle en deux

Une grille est grossière : la réponse n'est aussi fine que la grille. Mieux vaut utiliser le signe de l'écart. On part de sigma entre une valeur très basse et une très haute, on prend le milieu et on regarde l'écart : positif, le milieu est trop haut, on garde la moitié basse ; négatif, la haute. Chaque étape divise l'intervalle par deux, donc une trentaine d'étapes donnent huit chiffres. Le prochain sujet nomme et généralise cela ; ici, c'est l'outil qui lit le marché.

# Implied volatility by halving the interval, then reprice to check it
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def bs_call(spot, strike, rate, sigma, t):
    d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))

def implied_vol(price, spot, strike, rate, t, tol=1e-8):
    low, high = 0.0001, 5.0
    while high - low > tol:
        mid = (low + high) / 2
        if bs_call(spot, strike, rate, mid, t) > price:
            high = mid
        else:
            low = mid
    return (low + high) / 2

iv = implied_vol(8.0, 100, 100, 0.05, 0.5)
print(round(iv, 6))                                        # 0.240534
print(round(bs_call(100, 100, 0.05, iv, 0.5), 6))          # 8.0

Le sourire

Si Black–Scholes était exact, toutes les options sur la même action et la même date impliqueraient la même volatilité. Ce n'est pas le cas : les prix d'exercice bas impliquent en général plus que les hauts (le *skew*), ou les deux ailes plus que le centre (le *sourire*). La forme montre ce que le modèle ignore (sauts, queues épaisses, demande de protection), ce qui explique que l'on cote les options en volatilité implicite : un nombre unique et comparable entre prix d'exercice et échéances, dont le prix découle.

# A smile: market prices at five strikes, converted to implied volatilities
import math

def norm_cdf(x):
    return 0.5 * (1 + math.erf(x / math.sqrt(2)))

def bs_call(spot, strike, rate, sigma, t):
    d1 = (math.log(spot / strike) + (rate + 0.5 * sigma ** 2) * t) / (sigma * math.sqrt(t))
    return spot * norm_cdf(d1) - strike * math.exp(-rate * t) * norm_cdf(d1 - sigma * math.sqrt(t))

def implied_vol(price, spot, strike, rate, t, tol=1e-8):
    low, high = 0.0001, 5.0
    while high - low > tol:
        mid = (low + high) / 2
        low, high = (low, mid) if bs_call(spot, strike, rate, mid, t) > price else (mid, high)
    return (low + high) / 2

strikes = [90, 95, 100, 105, 110]
quotes = [bs_call(100, k, 0.05, v, 0.5) for k, v in zip(strikes, [0.28, 0.24, 0.21, 0.22, 0.25])]   # prices from a made-up smile
for strike, quote in zip(strikes, quotes):
    print(strike, round(quote, 4), round(implied_vol(quote, 100, strike, 0.05, 0.5), 4))
Un prix et une volatilité sont la même cotationLes teneurs de marché donnent une volatilité et l'acheteur la convertit en prix avec la formule convenue ; la conversion est une convention, pas une foi dans le modèle. L'application étant bijective pour un prix d'exercice, une date et un taux donnés, rien n'est perdu dans un sens ou l'autre. Ce qui compte, c'est l'entrée : la volatilité implicite ne vaut que ce que valent le taux, le dividende et la durée fournis.
  • La volatilité implicite est le sigma qui rend bs_call égal au prix du marché : on le trouve par recherche, faute de formule inverse.
  • Le prix croît avec sigma : une seule réponse, et le signe de l'écart indique le sens ; hors des bornes, aucune.
  • Couper l'intervalle en deux converge vite (environ 30 étapes pour 8 chiffres) ; le sourire est la volatilité implicite en fonction du prix d'exercice.

À suivre : la recherche de racines par dichotomie et Newton, la méthode générale derrière cette recherche et une bien plus rapide.

Lab : ce qui est noté

  • Comparer modèle et marché avec call_price_error25 pts
  • Parcourir une grille de volatilités avec implied_vol_scan25 pts
  • Inverser la formule avec implied_vol25 pts
  • Lire tout un sourire avec implied_vol_smile25 pts
Explorateur · lab-78-1
▾ lab-78-1
…
bash · dans le navigateur
learner@quantcraft:~/lab-78-1$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-78-1 revient aux fichiers de départ et le terminal est vidé. Votre solution actuelle sera perdue : cette action est irréversible.

Sera réinitialisé
  • main.py → code de départ
  • Historique du terminal et sortie des tests
  • Modifications non soumises
Sera conservé
  • Meilleur score du lab
  • Votre XP et votre série
  • Progression du cours et notes

Connectez-vous pour exécuter votre code.

Tous les cours sont gratuits à lire. Créez un compte ou connectez-vous pour exécuter du code, lancer les tests et soumettre ce lab.

  • IDE dans le navigateur avec terminal bash — rien à installer
  • Correction instantanée, XP et votre place dans le parcours
Créer un compteSe connecter
OU
Continuer avec GitHub