σμ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 11 · Sistemas de producción y modelos de vanguardia›Tema 11.10·~1 h 30 minExperto

Calibración de modelos

Posición en la rutaMódulo 11 de 11 · Tema 10 de 13
11.10 Calibración de modelos
—Puntuación del lab— / 100XP total—Racha—

Hacer que el modelo concuerde con el mercado

Un modelo de valoración tiene parámetros, y los valores correctos no se encuentran en un libro sino en el mercado de hoy. La calibración elige los parámetros para que los precios del modelo estén lo más cerca posible de los observados, normalmente las cotizaciones de opciones del día. La receta tiene tres partes. Un objetivo que mide el desajuste, casi siempre una suma de diferencias al cuadrado, posiblemente ponderada para que las cotizaciones líquidas cuenten más. Una búsqueda que mueve los parámetros para reducirlo. Y comprobaciones de que el resultado es sensato: estable de un día a otro, dentro de límites plausibles y bueno en cotizaciones no usadas en el ajuste.

# The objective: how far the model's prices are from the market's, as a (weighted) sum of squares
def sum_squared_error(model, market, weights=None):
    if weights is None:
        weights = [1.0] * len(model)
    return sum(w * (m - y) ** 2 for w, m, y in zip(weights, model, market))

market = [4.20, 8.60, 14.10]
model = [4.00, 8.80, 14.20]
print(round(sum_squared_error(model, market), 4))                    # 0.09   0.2^2 + 0.2^2 + 0.1^2
print(round(sum_squared_error(model, market, [1, 4, 1]), 4))         # 0.21   the middle quote counts four times as much

Dos herramientas de búsqueda: una raíz y un mínimo

Dos tareas aparecen constantemente. Primero, la volatilidad implícita: dado un precio de mercado, hallar la volatilidad con la que el modelo lo da, es decir una raíz de model(sigma) - price = 0. Como el precio sube de forma constante con la volatilidad, funciona la bisección: empieza con un intervalo donde la diferencia cambia de signo, pártelo por la mitad, quédate con la mitad donde el signo aún cambia y repite; cada paso duplica la precisión y no puede fallar. Segundo, un mínimo: para una función con un solo valle, la búsqueda de la sección áurea reduce el intervalo por el factor 0,618 en cada paso reutilizando uno de los dos valores de la función, así que necesita una evaluación por paso, y no necesita derivadas. Las bibliotecas (scipy.optimize) son más rápidas y manejan muchos parámetros, pero estas dos muestran las ideas.

# Implied volatility by bisection: the root of (model price - market price) in sigma
import math

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

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

def bisect_root(f, lo, hi, tol=1e-10):
    f_lo = f(lo)
    while hi - lo > tol:
        mid = (lo + hi) / 2
        f_mid = f(mid)
        if f_lo * f_mid <= 0:
            hi = mid
        else:
            lo, f_lo = mid, f_mid
    return (lo + hi) / 2

market_price = bs_call(100.0, 105.0, 1.0, 0.02, 0.27)           # a quote generated with a volatility of 27%
implied = bisect_root(lambda sigma: bs_call(100.0, 105.0, 1.0, 0.02, sigma) - market_price, 0.01, 2.0)
print(round(implied, 6))                                         # 0.27   recovered
# Golden-section search: one function evaluation per step, the interval shrinks by 0.618 each time
import math

def golden_section_minimize(f, lo, hi, tol=1e-8):
    ratio = (math.sqrt(5) - 1) / 2
    a, b = lo, hi
    c, d = b - ratio * (b - a), a + ratio * (b - a)
    fc, fd = f(c), f(d)
    while b - a > tol:
        if fc < fd:
            b, d, fd = d, c, fc
            c = b - ratio * (b - a)
            fc = f(c)
        else:
            a, c, fc = c, d, fd
            d = a + ratio * (b - a)
            fd = f(d)
    return (a + b) / 2

evaluations = []
def valley(x):
    evaluations.append(x)
    return (x - 1.3) ** 2 + 0.5

print(round(golden_section_minimize(valley, 0.0, 5.0), 5), len(evaluations) < 60)       # 1.3 True   about 40 evaluations for 1e-8

Calibrar una volatilidad y hacerlo con responsabilidad

Juntando las piezas, calibrar una única volatilidad con varios precios de opciones significa minimizar la suma de errores al cuadrado entre price_fn(strike, sigma) y los precios de mercado sobre sigma, con price_fn pasada como argumento para que el mismo código sirva para cualquier modelo. Para un modelo con varios parámetros (Heston tiene cinco, SABR tres tras fijar beta) la búsqueda es multidimensional y puede caer en un mínimo local: parte de varias estimaciones iniciales, acota los parámetros y prefiere errores relativos o en volatilidad implícita a errores brutos de precio para no ignorar las opciones baratas fuera del dinero. Luego comprueba que los parámetros ajustados sean estables de un día a otro y que el modelo valore bien opciones en las que no se ajustó. Un ajuste perfecto pero inestable es sobreajuste.

# Calibrate one volatility to three option prices: minimise the sum of squared errors over sigma
import math

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

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

def golden_section_minimize(f, lo, hi, tol=1e-8):
    ratio = (math.sqrt(5) - 1) / 2
    a, b = lo, hi
    c, d = b - ratio * (b - a), a + ratio * (b - a)
    fc, fd = f(c), f(d)
    while b - a > tol:
        if fc < fd:
            b, d, fd = d, c, fc
            c = b - ratio * (b - a)
            fc = f(c)
        else:
            a, c, fc = c, d, fd
            d = a + ratio * (b - a)
            fd = f(d)
    return (a + b) / 2

def calibrate_sigma(strikes, market_prices, price_fn, lo, hi):
    def error(sigma):
        return sum((price_fn(k, sigma) - p) ** 2 for k, p in zip(strikes, market_prices))
    return golden_section_minimize(error, lo, hi)

strikes = [90.0, 100.0, 110.0]
quotes = [bs_call(100.0, k, 1.0, 0.02, 0.23) for k in strikes]                  # market quotes made with sigma = 23%
sigma = calibrate_sigma(strikes, quotes, lambda k, s: bs_call(100.0, k, 1.0, 0.02, s), 0.01, 1.5)
print(round(sigma, 4))                                                           # 0.23
La calibración es donde los modelos fallan en silencioUna calibración que converge a un valor absurdo (una volatilidad del 4.000 %, una correlación exactamente 1) es una advertencia, no un resultado. Muestra siempre los parámetros ajustados y el error residual, compáralos con los de ayer y rechaza usar un ajuste cuyo error sea mayor que el diferencial bid-ask de las cotizaciones. Mantén los límites lo bastante ajustados para que el optimizador no se pierda por regiones donde la fórmula no es válida y conserva los datos de mercado ajustados, para poder reproducir y auditar la calibración más tarde.
  • La calibración elige los parámetros del modelo que minimizan una suma (ponderada) de errores de precio al cuadrado; comprueba la estabilidad entre días y la precisión en cotizaciones no usadas en el ajuste.
  • La bisección halla una raíz (una volatilidad implícita) partiendo por la mitad un intervalo con cambio de signo; la sección áurea halla el mínimo de una función de un valle con una evaluación por paso.
  • Pasa la función de valoración como argumento, acota los parámetros, parte de varias estimaciones en modelos multiparámetro y rechaza los ajustes absurdos.

A continuación: redes neuronales para pronóstico, aprender una predicción de los datos en vez de escribir la fórmula.

Lab: qué se califica

  • Medir el desajuste con sum_squared_error25 pts
  • Resolver f(x) = 0 con bisect_root25 pts
  • Hallar el punto más bajo con golden_section_minimize25 pts
  • Ajustar el mercado con calibrate_sigma25 pts
Explorador · lab-139-1
▾ lab-139-1
…
bash · en el navegador
learner@quantcraft:~/lab-139-1$
Python aún no cargadoPythonLn 1, Col 1Spaces: 4UTF-8

¿Restablecer este tema?

Todo lo que hay en lab-139-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