σμ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 AcademyRutas
Ctrl K
AO
+1,2 k estudiantes
Iniciar sesiónUnirse ↗
Rutas
AO
+1,2 k estudiantes
Iniciar sesión
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Ruta de Python›Módulo 4 · Valoración de renta fija y opciones›Tema 4.13·~50 minIntermedio

Volatilidad implícita

Posición en la rutaMódulo 4 de 11 · Tema 13 de 16
4.13 Volatilidad implícita
—Puntuación del lab— / 100XP total—Racha—

Usar la fórmula al revés

La fórmula de Black–Scholes convierte una volatilidad en un precio. En el mercado ocurre lo contrario: el precio está cotizado y la volatilidad se desconoce. La volatilidad implícita es la sigma para la que la fórmula reproduce el precio de mercado. No hay inversa algebraica, así que se busca: se calcula el precio para una sigma de prueba, se mira el error de precio (modelo menos mercado) y se mueve sigma hasta anularlo. La búsqueda más simple prueba una malla de valores y se queda con el mejor.

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

Por qué hay una única respuesta

El precio del call sube de forma constante con sigma: la vega es positiva, así que el error cruza cero una sola vez. La respuesta es única y se sabe hacia dónde moverse: si el modelo es demasiado caro, bajar sigma. Solo existe solución si el precio de mercado está estrictamente entre el *suelo intrínseco* max(S - K * exp(-rT), 0) (volatilidad cero) y el spot S (volatilidad infinita). Fuera de esos límites la cotización contradice la ausencia de arbitraje y la respuesta correcta es un error, no un número.

# 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

Dividir el intervalo a la mitad

Una malla es tosca: la respuesta es tan fina como la malla. Mejor usar el signo del error. Se parte de sigma entre un valor muy bajo y uno muy alto, se toma el punto medio y se mira el error: positivo, el medio es demasiado alto, se queda la mitad inferior; negativo, la superior. Cada paso divide el intervalo por dos, así que unos treinta pasos dan ocho cifras. El siguiente tema lo nombra y lo generaliza; aquí es la herramienta que lee el mercado.

# 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

La sonrisa

Si Black–Scholes fuera exacto, todas las opciones sobre la misma acción y fecha implicarían la misma volatilidad. No es así: los strikes bajos suelen implicar más que los altos (el *skew*), o ambas alas más que el centro (la *sonrisa*). El patrón muestra lo que el modelo no capta (saltos, colas gruesas, demanda de protección), por lo que las opciones se cotizan en volatilidad implícita: un número único y comparable entre strikes y vencimientos, del que se deduce el precio.

# 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 precio y una volatilidad son la misma cotizaciónLos creadores de mercado dan una volatilidad y el comprador la convierte en precio con la fórmula acordada; la conversión es una convención, no una creencia en el modelo. Como la correspondencia es biunívoca para un strike, fecha y tipo dados, no se pierde nada en ninguno de los sentidos. Lo que importa es el dato de entrada: la volatilidad implícita vale lo que valgan el tipo, el dividendo y el plazo que se le den.
  • La volatilidad implícita es la sigma que iguala bs_call al precio de mercado: se halla por búsqueda, porque no hay fórmula inversa.
  • El precio sube con sigma: hay una respuesta y el signo del error dice hacia dónde moverse; fuera de los límites no hay ninguna.
  • Dividir el intervalo converge rápido (unos 30 pasos para 8 cifras); la sonrisa es la volatilidad implícita frente al strike.

A continuación: la búsqueda de raíces con bisección y Newton, el método general detrás de esta búsqueda y otro mucho más rápido.

Lab: qué se califica

  • Comparar modelo y mercado con call_price_error25 pts
  • Recorrer una malla de volatilidades con implied_vol_scan25 pts
  • Invertir la fórmula con implied_vol25 pts
  • Leer una sonrisa entera con implied_vol_smile25 pts
Explorador · lab-78-1
▾ lab-78-1
…
bash · en el navegador
learner@quantcraft:~/lab-78-1$
Python aún no cargadoPythonLn 1, Col 1Spaces: 4UTF-8

¿Restablecer este tema?

Todo lo que hay en lab-78-1 vuelve a los archivos iniciales y se limpia el terminal. Tu solución actual se perderá: no se puede deshacer.

Se restablecerá
  • main.py → código inicial
  • Historial del terminal y salida de pruebas
  • Cambios sin enviar
Se conservará
  • Mejor puntuación del lab
  • Tu XP y tu racha
  • Progreso de la lección y notas

Inicia sesión para ejecutar tu código.

Todas las clases se pueden leer gratis. Crea una cuenta o inicia sesión para ejecutar código, lanzar las pruebas y enviar este lab.

  • IDE en el navegador con terminal bash — sin instalar nada
  • Corrección instantánea, XP y tu lugar en la ruta
Crear una cuentaIniciar sesión
O
Continuar con GitHub