Die Formel rückwärts laufen lassen
Die Black-Scholes-Formel macht aus einer Volatilität einen Preis. Am Markt ist es umgekehrt: Der Preis wird gestellt, die Volatilität ist unbekannt. Die implizite Volatilität ist das sigma, mit dem die Formel den Marktpreis reproduziert. Es gibt keine algebraische Umkehrung, also sucht man: Preis für ein Probe-sigma berechnen, den Preisfehler (Modell minus Markt) ansehen und sigma verschieben, bis er null ist. Die einfachste Suche probiert ein Raster von Werten und behält den besten.
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))Warum es genau eine Antwort gibt
Der Call-Preis steigt stetig mit sigma: Vega ist positiv, der Fehler überquert null also nur einmal. Die Antwort ist eindeutig, und man weiß, in welche Richtung man gehen muss: Ist das Modell zu teuer, sigma senken. Eine Lösung gibt es nur, wenn der Marktpreis streng zwischen dem *inneren Wert* max(S - K * exp(-rT), 0) (Volatilität null) und dem Kassakurs S (Volatilität unendlich) liegt. Außerhalb widerspricht die Notierung der Arbitragefreiheit, und die richtige Antwort ist ein Fehler, keine Zahl.
# 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 spotDas Intervall halbieren
Ein Raster ist grob: Die Antwort ist nur so fein wie das Raster. Besser ist das Vorzeichen des Fehlers. Man startet mit sigma zwischen einem sehr niedrigen und einem sehr hohen Wert, nimmt die Mitte und prüft den Fehler: positiv heißt, die Mitte ist zu hoch, man behält die untere Hälfte; negativ die obere. Jeder Schritt halbiert das Intervall, etwa dreißig Schritte ergeben acht Stellen. Das nächste Thema benennt und verallgemeinert das; hier ist es das Werkzeug, das den Markt liest.
# 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.0Das Smile
Wäre Black-Scholes exakt richtig, würden alle Optionen auf dieselbe Aktie und denselben Tag dieselbe Volatilität implizieren. Tun sie nicht: Niedrige Strikes implizieren meist mehr als hohe (der *Skew*), oder beide Flügel mehr als die Mitte (das *Smile*). Das Muster zeigt, was das Modell übersieht (Sprünge, Fat Tails, Nachfrage nach Absicherung); darum werden Optionen in impliziter Volatilität notiert: eine einzige, über Strikes und Laufzeiten vergleichbare Zahl, aus der der Preis folgt.
# 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))- Die implizite Volatilität ist das
sigma, dasbs_calldem Marktpreis gleichsetzt: durch Suche gefunden, da es keine Umkehrformel gibt. - Der Preis steigt mit
sigma: Es gibt eine Antwort, das Fehlervorzeichen zeigt die Richtung; außerhalb der Grenzen gibt es keine. - Intervallhalbierung konvergiert schnell (etwa 30 Schritte für 8 Stellen); das Smile ist die implizite Volatilität über dem Strike.
Als Nächstes: Nullstellensuche mit Bisektion und Newton, die allgemeine Methode hinter dieser Suche und eine viel schnellere.
Lab: Was bewertet wird
- Mit
call_price_errorModell und Markt vergleichen25 Pkt. - Mit
implied_vol_scanein Raster von Volatilitäten durchsuchen25 Pkt. - Die Formel mit
implied_volumkehren25 Pkt. - Mit
implied_vol_smileein ganzes Smile lesen25 Pkt.