σμ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 11 · Systèmes de production et modèles de pointe›Sujet 11.8·~1 h 30 minExpert

Volatilité locale

Position dans le parcoursModule 11 sur 11 · Sujet 8 sur 13
11.8 Volatilité locale
—Score du lab— / 100XP total—Série—

Une volatilité pour chaque strike et chaque date

Black-Scholes valorise un call avec une seule volatilité, alors que le marché cote une volatilité implicite différente pour chaque strike et chaque échéance. Le modèle de volatilité locale reproduit tous ces prix à la fois en laissant la volatilité dépendre du niveau de prix et du temps, sigma(S, t), tout en gardant une seule source d'aléa. Bruno Dupire a montré en 1994 que cette fonction n'est pas une supposition : elle est déterminée par la surface des prix de call C(K, T) au moyen d'une formule, de sorte que les prix d'options du marché se transforment directement en un modèle pour valoriser des options exotiques de façon cohérente avec eux.

# What "one volatility" means: a call priced with Black-Scholes at three strikes, all with the same sigma
import math

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

def bs_call(s, k, t, r, q, sigma):
    d1 = (math.log(s / k) + (r - q + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
    d2 = d1 - sigma * math.sqrt(t)
    return s * math.exp(-q * t) * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d2)

for k in (90, 100, 110):
    print(k, round(bs_call(100.0, k, 1.0, 0.03, 0.01, 0.2), 4))      # 90 14.6592   100 8.8273   110 4.8947   one sigma, three different prices

La formule de Dupire

La formule de Dupire exprime la variance locale avec trois dérivées de la surface des prix de call : sigma_loc^2(K, T) = (dC/dT + (r - q) * K * dC/dK + q * C) / (0.5 * K^2 * d2C/dK2). Le numérateur contient la pente temporelle des prix (elle doit être positive ou nulle, sinon une option plus longue coûte moins qu'une plus courte : un arbitrage calendaire) et le dénominateur le papillon d2C/dK2, proportionnel à la densité de probabilité implicite du marché pour le prix, qui doit être positive. Avec des cotations réelles, bruitées et clairsemées, les dérivées se calculent par différences finies sur une surface lissée : (f(x+h) - f(x-h)) / (2h) pour la dérivée première et (f(x+h) - 2 f(x) + f(x-h)) / h^2 pour la seconde. Une surface bruitée donne des volatilités locales sauvages : lisser d'abord la surface de volatilité implicite est la pratique courante.

# Dupire's formula recovers a constant volatility from Black-Scholes prices (finite differences on the price surface)
import math

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

def bs_call(s, k, t, r, q, sigma):
    d1 = (math.log(s / k) + (r - q + sigma ** 2 / 2) * t) / (sigma * math.sqrt(t))
    return s * math.exp(-q * t) * norm_cdf(d1) - k * math.exp(-r * t) * norm_cdf(d1 - sigma * math.sqrt(t))

def local_variance(call, dc_dt, dc_dk, d2c_dk2, strike, r, q):
    return (dc_dt + (r - q) * strike * dc_dk + q * call) / (0.5 * strike ** 2 * d2c_dk2)

s, k, t, r, q, sigma = 100.0, 105.0, 1.0, 0.03, 0.01, 0.2
hk, ht = 0.5, 0.002
c = bs_call(s, k, t, r, q, sigma)
dc_dt = (bs_call(s, k, t + ht, r, q, sigma) - bs_call(s, k, t - ht, r, q, sigma)) / (2 * ht)
dc_dk = (bs_call(s, k + hk, t, r, q, sigma) - bs_call(s, k - hk, t, r, q, sigma)) / (2 * hk)
d2c_dk2 = (bs_call(s, k + hk, t, r, q, sigma) - 2 * c + bs_call(s, k - hk, t, r, q, sigma)) / hk ** 2
print(round(math.sqrt(local_variance(c, dc_dt, dc_dk, d2c_dk2, k, r, q)), 3))        # 0.2   the constant volatility comes back

Utiliser la surface dans une simulation

Pour simuler, on tabule sigma_loc sur une grille de prix (pour chaque tranche de temps) et on la consulte au prix courant à chaque pas, en interpolant linéairement entre les points et en gardant les valeurs d'extrémité constantes au-delà de la grille. Un pas est alors le mouvement log-Euler habituel avec cette volatilité, S * exp((mu - sigma^2/2) dt + sigma sqrt(dt) dz). Comparée à Heston, la volatilité locale ajuste exactement les prix d'options d'aujourd'hui par construction, mais la dynamique future de son smile est irréaliste (le skew s'aplatit avec le temps), ce qui compte pour les produits sensibles au smile futur. Beaucoup de salles utilisent donc la volatilité locale-stochastique, un mélange des deux.

# A lookup on a strike grid (linear inside, flat outside) and one simulation step with the volatility it returns
import math

def lookup_local_vol(strikes, vols, k):
    if k <= strikes[0]:
        return vols[0]
    if k >= strikes[-1]:
        return vols[-1]
    for i in range(1, len(strikes)):
        if k <= strikes[i]:
            weight = (k - strikes[i - 1]) / (strikes[i] - strikes[i - 1])
            return vols[i - 1] + weight * (vols[i] - vols[i - 1])

strikes, vols = [90.0, 100.0, 110.0], [0.25, 0.20, 0.18]
print(lookup_local_vol(strikes, vols, 95.0), round(lookup_local_vol(strikes, vols, 105.0), 4), lookup_local_vol(strikes, vols, 200.0))      # 0.225 0.19 0.18

s = 100.0
sigma = lookup_local_vol(strikes, vols, s)
print(round(s * math.exp((0.05 - sigma ** 2 / 2) * (1 / 252) + sigma * math.sqrt(1 / 252) * 0.5), 4))       # 100.6439
# A calendar arbitrage shows up as a negative numerator: a longer option priced below a shorter one
def numerator(call, dc_dt, dc_dk, strike, r, q):
    return dc_dt + (r - q) * strike * dc_dk + q * call

print(round(numerator(10.0, 1.2, -0.45, 100.0, 0.03, 0.01), 6))           # 0.4     prices rise with maturity: a valid local variance
print(numerator(10.0, -2.0, -0.45, 100.0, 0.03, 0.01) < 0)                # True    prices fall with maturity: no model can fit this
Un ajustement parfait n'est pas un modèle parfaitComme la volatilité locale se lit sur les prix, elle les reproduit par construction, y compris leur bruit et tout arbitrage dans les cotations. Vérifiez que la surface est sans arbitrage avant d'utiliser Dupire (papillons et écarts calendaires non négatifs), plafonnez et planchez les volatilités obtenues, et testez les prix simulés contre les options vanilles dont vous êtes parti. Un modèle qui colle parfaitement à aujourd'hui peut quand même mal couvrir demain, car son idée de la façon dont le smile bouge est simpliste.
  • La volatilité locale sigma(S, t) reproduit toute la surface des prix de call avec une seule source d'aléa ; la formule de Dupire la donne directement à partir des prix.
  • Dupire : (dC/dT + (r - q) K dC/dK + q C) / (0.5 K^2 d2C/dK2) ; calculez les dérivées par différences finies sur une surface lissée et sans arbitrage.
  • Simulez en consultant sigma_loc au prix courant à chaque pas ; un ajustement parfait aujourd'hui ne garantit pas une dynamique réaliste du smile demain.

À suivre : le modèle SABR, un modèle à volatilité stochastique avec un smile en forme close.

Lab : ce qui est noté

  • Dériver numériquement avec finite_difference25 pts
  • Appliquer la formule de Dupire dans local_variance25 pts
  • Lire la surface avec lookup_local_vol25 pts
  • Simuler un pas avec local_vol_step25 pts
Explorateur · lab-137-1
▾ lab-137-1
…
bash · dans le navigateur
learner@quantcraft:~/lab-137-1$
Python pas encore chargéPythonLn 1, Col 1Spaces: 4UTF-8

Réinitialiser ce sujet ?

Tout le contenu de lab-137-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