Eine Volatilität für jeden Strike und jede Zeit
Black-Scholes bewertet einen Call mit einer einzigen Volatilität, doch der Markt notiert für jeden Strike und jede Laufzeit eine andere implizite Volatilität. Das Local-Volatility-Modell reproduziert alle diese Preise zugleich, indem die Volatilität vom Preisniveau und von der Zeit abhängt, sigma(S, t), bei nur einer Zufallsquelle. Bruno Dupire zeigte 1994, dass diese Funktion keine Vermutung ist: Sie ist durch die Fläche der Call-Preise C(K, T) über eine Formel bestimmt, sodass sich die Optionspreise des Marktes direkt in ein Modell verwandeln lassen, mit dem man exotische Optionen konsistent bewertet.
# 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 pricesDupires Formel
Dupires Formel drückt die lokale Varianz durch drei Ableitungen der Call-Preisfläche aus: sigma_loc^2(K, T) = (dC/dT + (r - q) * K * dC/dK + q * C) / (0.5 * K^2 * d2C/dK2). Der Zähler enthält die Zeitsteigung der Preise (sie darf nicht negativ sein, sonst ist eine längere Option billiger als eine kürzere: eine Kalenderarbitrage), der Nenner den Butterfly d2C/dK2, proportional zur impliziten Wahrscheinlichkeitsdichte des Marktes für den Preis, der positiv sein muss. Bei echten Quotes, die verrauscht und spärlich sind, bildet man die Ableitungen durch finite Differenzen auf einer geglätteten Fläche: (f(x+h) - f(x-h)) / (2h) für die erste und (f(x+h) - 2 f(x) + f(x-h)) / h^2 für die zweite Ableitung. Eine verrauschte Fläche liefert wilde lokale Volatilitäten, daher ist es Standard, zuerst die implizite Volatilitätsfläche zu glätten.
# 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 backDie Fläche in einer Simulation nutzen
Zur Simulation tabelliert man sigma_loc auf einem Preisgitter (für jede Zeitscheibe) und schlägt sie bei jedem Schritt zum aktuellen Preis nach, linear zwischen Gitterpunkten interpolierend und außerhalb des Gitters die Randwerte konstant haltend. Ein Schritt ist dann die übliche Log-Euler-Bewegung mit dieser Volatilität, S * exp((mu - sigma^2/2) dt + sigma sqrt(dt) dz). Verglichen mit Heston passt die lokale Volatilität die heutigen Optionspreise konstruktionsbedingt exakt an, doch ihre künftige Smile-Dynamik ist unrealistisch (der Skew flacht mit der Zeit ab), was für Produkte zählt, die auf den künftigen Smile reagieren. Viele Desks nutzen deshalb lokal-stochastische Volatilität, eine Mischung beider.
# 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- Die lokale Volatilität
sigma(S, t)reproduziert die gesamte Call-Preisfläche mit einer Zufallsquelle; Dupires Formel liefert sie direkt aus den Preisen. - Dupire:
(dC/dT + (r - q) K dC/dK + q C) / (0.5 K^2 d2C/dK2); bilden Sie die Ableitungen per finiten Differenzen auf einer geglätteten, arbitragefreien Fläche. - Simulieren Sie, indem Sie bei jedem Schritt
sigma_loczum aktuellen Preis nachschlagen; ein perfekter Fit heute garantiert keine realistische Smile-Dynamik morgen.
Als Nächstes: das SABR-Modell, ein stochastisches Volatilitätsmodell mit Smile in geschlossener Form.
Lab: Was bewertet wird
- Mit
finite_differencenumerisch ableiten25 Pkt. - Dupires Formel in
local_varianceanwenden25 Pkt. - Die Fläche mit
lookup_local_volauslesen25 Pkt. - Mit
local_vol_stepeinen Schritt simulieren25 Pkt.