تشغيل الصيغة بالعكس
تحوّل صيغة بلاك-شولز التقلب إلى سعر. وفي السوق يحدث العكس: السعر معلن والتقلب مجهول. التقلب الضمني هو قيمة sigma التي تعيد بها الصيغة سعر السوق. ولا يوجد معكوس جبري، فنبحث: نحسب السعر لقيمة sigma تجريبية، وننظر إلى خطأ السعر (النموذج ناقص السوق)، ونحرك sigma حتى ينعدم الخطأ. وأبسط بحث يجرب شبكة من القيم ويبقي الأفضل.
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))لماذا توجد إجابة واحدة فقط
يرتفع سعر الكول باطراد مع sigma: فيغا موجبة، فيقطع الخطأ الصفر مرة واحدة فقط. وهذا يجعل الجواب وحيدًا ويدل على اتجاه الحركة: إن كان النموذج غاليًا خفّض sigma. ولا يوجد حل إلا إذا وقع سعر السوق بين *الحد الأدنى الجوهري* max(S - K * exp(-rT), 0) (تقلب صفر) والسعر الفوري S (تقلب لانهائي) بدقة. وخارج هذين الحدين يناقض السعر انعدام المراجحة، فالإجابة الصحيحة خطأ لا رقم.
# 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 spotتنصيف المجال
الشبكة خشنة: الجواب لا يكون أدق من الشبكة. الأفضل استخدام إشارة الخطأ. نبدأ بـ sigma بين قيمة منخفضة جدًا وأخرى مرتفعة جدًا، ونأخذ المنتصف وننظر إلى الخطأ عنده: إن كان موجبًا فالمنتصف مرتفع فنُبقي النصف الأدنى؛ وإن كان سالبًا نُبقي الأعلى. كل خطوة تنصّف المجال، فنحو ثلاثين خطوة تعطي ثمانية أرقام. الموضوع التالي يسمّي هذا ويعمّمه؛ وهنا هو الأداة التي تقرأ السوق.
# 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.0الابتسامة
لو كانت بلاك-شولز صحيحة تمامًا لأوحت كل الخيارات على السهم نفسه والتاريخ نفسه بالتقلب نفسه. لكن الواقع غير ذلك: فأسعار التنفيذ المنخفضة توحي عادة بتقلب أعلى من المرتفعة (*الانحراف*)، أو يكون الجناحان أعلى من الوسط (*الابتسامة*). ويُظهر النمط ما يفوت النموذج (القفزات والذيول السميكة والطلب على الحماية)، ولهذا تُعلن الخيارات بالتقلب الضمني: رقم واحد قابل للمقارنة عبر أسعار التنفيذ والآجال، ومنه يُشتق السعر.
# 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))- التقلب الضمني هو
sigmaالتي تجعلbs_callتساوي سعر السوق: يُوجد بالبحث لعدم وجود صيغة معكوسة. - يرتفع السعر مع
sigma: فهناك جواب واحد وإشارة الخطأ تدل على الاتجاه؛ وخارج الحدين لا جواب. - تنصيف المجال يتقارب بسرعة (نحو 30 خطوة لثمانية أرقام)؛ والابتسامة هي التقلب الضمني مقابل سعر التنفيذ.
التالي: إيجاد الجذور بالتنصيف ونيوتن، الطريقة العامة وراء هذا البحث وطريقة أسرع بكثير.
المختبر: ما الذي يُقيَّم
- قارن النموذج بالسوق باستخدام
call_price_error25 نقطة - ابحث في شبكة من التقلبات باستخدام
implied_vol_scan25 نقطة - اعكس الصيغة باستخدام
implied_vol25 نقطة - اقرأ ابتسامة كاملة باستخدام
implied_vol_smile25 نقطة