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 muchDos 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-8Calibrar 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 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