Faire concorder le modèle avec le marché
Un modèle de valorisation a des paramètres, et les bonnes valeurs ne se trouvent pas dans un manuel mais dans le marché d'aujourd'hui. La calibration choisit les paramètres pour que les prix du modèle soient aussi proches que possible des prix observés, en général les cotations d'options du jour. La recette a trois parties. Un objectif qui mesure l'écart, presque toujours une somme de carrés des différences, éventuellement pondérée pour que les cotations liquides comptent plus. Une recherche qui déplace les paramètres pour le réduire. Et des vérifications que le résultat est sensé : stable d'un jour à l'autre, dans des bornes plausibles et bon sur des cotations non utilisées dans l'ajustement.
# 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 muchDeux outils de recherche : une racine et un minimum
Deux tâches reviennent constamment. D'abord la volatilité implicite : étant donné un prix de marché, trouver la volatilité pour laquelle le modèle le donne, c'est-à-dire une racine de model(sigma) - price = 0. Comme le prix croît régulièrement avec la volatilité, la bissection fonctionne : partez d'un intervalle où la différence change de signe, coupez-le en deux, gardez la moitié où le signe change encore, et recommencez ; chaque pas double la précision et ne peut pas échouer. Ensuite un minimum : pour une fonction à une seule vallée, la recherche par section dorée réduit l'intervalle du facteur 0,618 à chaque pas en réutilisant l'une des deux valeurs de fonction, donc une évaluation par pas, et sans dérivées. Les bibliothèques (scipy.optimize) sont plus rapides et gèrent de nombreux paramètres, mais ces deux outils montrent les idées.
# 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-8Calibrer une volatilité, et le faire sérieusement
En assemblant les pièces, calibrer une volatilité unique sur plusieurs prix d'options revient à minimiser la somme des carrés des écarts entre price_fn(strike, sigma) et les prix de marché selon sigma, avec price_fn passée en argument pour que le même code serve à n'importe quel modèle. Pour un modèle à plusieurs paramètres (Heston en a cinq, SABR trois une fois beta fixé) la recherche est multidimensionnelle et peut tomber dans un minimum local : partez de plusieurs valeurs initiales, bornez les paramètres, et préférez des erreurs relatives ou en volatilité implicite aux erreurs brutes de prix pour que les options bon marché hors de la monnaie ne soient pas ignorées. Vérifiez ensuite que les paramètres ajustés sont stables d'un jour à l'autre et que le modèle valorise bien des options sur lesquelles il n'a pas été ajusté. Un ajustement parfait mais instable est du surapprentissage.
# 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 calibration choisit les paramètres du modèle qui minimisent une somme (pondérée) de carrés d'erreurs de prix ; vérifiez la stabilité d'un jour à l'autre et la précision sur des cotations non utilisées.
- La bissection trouve une racine (une volatilité implicite) en coupant en deux un intervalle avec changement de signe ; la section dorée trouve le minimum d'une fonction à une vallée avec une évaluation par pas.
- Passez la fonction de valorisation en argument, bornez les paramètres, partez de plusieurs valeurs initiales pour les modèles multi-paramètres et rejetez les ajustements absurdes.
À suivre : les réseaux de neurones pour la prévision, apprendre une prédiction à partir des données au lieu d'écrire la formule.
Lab : ce qui est noté
- Mesurer l'écart avec
sum_squared_error25 pts - Résoudre f(x) = 0 avec
bisect_root25 pts - Trouver le point le plus bas avec
golden_section_minimize25 pts - Ajuster le marché avec
calibrate_sigma25 pts