تقلب لكل سعر تنفيذ ولكل زمن
يسعّر بلاك-شولز خيار الشراء بتقلب واحد، بينما يعلن السوق تقلبًا ضمنيًا مختلفًا لكل سعر تنفيذ وكل استحقاق. ويعيد نموذج التقلب المحلي إنتاج كل تلك الأسعار دفعة واحدة بجعل التقلب يعتمد على مستوى السعر والزمن sigma(S, t) مع مصدر عشوائية واحد. وبيّن برونو دوبير عام 1994 أن هذه الدالة ليست تخمينًا: بل تحددها سطح أسعار الخيارات C(K, T) بصيغة، فتتحول أسعار الخيارات في السوق مباشرة إلى نموذج لتسعير الخيارات الغريبة بانسجام معها.
# 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 pricesصيغة دوبير
تعبّر صيغة دوبير عن التباين المحلي بثلاث مشتقات لسطح أسعار الخيارات: sigma_loc^2(K, T) = (dC/dT + (r - q) * K * dC/dK + q * C) / (0.5 * K^2 * d2C/dK2). يحتوي البسط على ميل الزمن للأسعار (يجب ألا يكون سالبًا وإلا كان الخيار الأطول أرخص من الأقصر: مراجحة تقويمية) ويحتوي المقام على الفراشة d2C/dK2 المتناسبة مع كثافة احتمال السعر الضمنية في السوق وهي موجبة. ومع الأسعار الحقيقية المشوشة والمتفرقة تُحسب المشتقات بـالفروق المنتهية على سطح مُمهَّد: (f(x+h) - f(x-h)) / (2h) للمشتقة الأولى و(f(x+h) - 2 f(x) + f(x-h)) / h^2 للثانية. والسطح المشوش يعطي تقلبات محلية جامحة، فتمهيد سطح التقلب الضمني أولًا ممارسة معيارية.
# 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 backاستعمال السطح في المحاكاة
للمحاكاة يُجدوَل sigma_loc على شبكة أسعار (لكل شريحة زمنية) ثم يُبحث عنه عند السعر الحالي في كل خطوة بالاستيفاء الخطي بين نقاط الشبكة وإبقاء قيم الطرفين ثابتة خارجها. والخطوة حينئذ هي حركة لوغاريتم-أويلر المعتادة بذلك التقلب S * exp((mu - sigma^2/2) dt + sigma sqrt(dt) dz). وبالمقارنة مع هستون يطابق التقلب المحلي أسعار خيارات اليوم تمامًا بالبناء لكن ديناميكية ابتسامته المستقبلية غير واقعية (يتسطح الانحياز مع الزمن) وهذا مهم للمنتجات الحساسة للابتسامة المستقبلية. ولذلك تستعمل مكاتب كثيرة التقلب المحلي-العشوائي، مزيجًا منهما.
# 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- يعيد التقلب المحلي
sigma(S, t)إنتاج سطح أسعار الخيارات كله بمصدر عشوائية واحد؛ وتعطيه صيغة دوبير مباشرة من الأسعار. - دوبير:
(dC/dT + (r - q) K dC/dK + q C) / (0.5 K^2 d2C/dK2)؛ احسب المشتقات بالفروق المنتهية على سطح مُمهَّد خالٍ من المراجحة. - حاكِ بالبحث عن
sigma_locعند السعر الحالي في كل خطوة؛ والمطابقة التامة اليوم لا تضمن ديناميكية ابتسامة واقعية غدًا.
التالي: نموذج SABR، نموذج تقلب عشوائي له ابتسامة بصيغة مغلقة.
المختبر: ما الذي يُقيَّم
- اشتق عدديًا باستخدام
finite_difference25 نقطة - طبّق صيغة دوبير في
local_variance25 نقطة - اقرأ السطح باستخدام
lookup_local_vol25 نقطة - حاكِ خطوة باستخدام
local_vol_step25 نقطة