جعل النموذج يتفق مع السوق
لنموذج التسعير معاملات، وقيمها الصحيحة لا توجد في كتاب بل في سوق اليوم. والمعايرة تختار المعاملات لتكون أسعار النموذج أقرب ما يمكن إلى الأسعار المرصودة، عادةً أسعار الخيارات المعلنة لليوم. وللوصفة ثلاثة أجزاء. هدف يقيس عدم المطابقة، وهو دائمًا تقريبًا مجموع مربعات الفروق، وقد يُرجَّح لتُحسب الأسعار السائلة أكثر. وبحث يحرك المعاملات لتقليله. وفحوص لمعقولية النتيجة: مستقرة من يوم لآخر وضمن حدود مقبولة وجيدة على أسعار لم تُستعمل في المطابقة.
# 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 muchأداتا بحث: إيجاد جذر وإيجاد حد أدنى
تتكرر مهمتان باستمرار. الأولى التقلب الضمني: بمعلومية سعر السوق أوجد التقلب الذي يعطيه عنده النموذج، أي جذر model(sigma) - price = 0. ولأن السعر يرتفع باطراد مع التقلب تنجح التنصيف (bisection): ابدأ بمجال يغيّر فيه الفرق إشارته وقسّمه نصفين واحتفظ بالنصف الذي ما زالت الإشارة تتغير فيه وكرّر؛ تتضاعف الدقة في كل خطوة ولا يمكن أن يفشل. والثانية الحد الأدنى: لدالة بوادٍ واحد يقلص بحث القطع الذهبي المجال بالعامل 0.618 في كل خطوة مع إعادة استعمال إحدى قيمتَي الدالة، فيحتاج تقييمًا واحدًا في الخطوة ولا يحتاج مشتقات. والمكتبات (scipy.optimize) أسرع وتعالج معاملات كثيرة لكن هاتين الأداتين تُظهران الأفكار.
# 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-8معايرة تقلب والقيام بذلك بمسؤولية
بجمع القطع تعني معايرة تقلب واحد على عدة أسعار خيارات تقليل مجموع مربعات الأخطاء بين price_fn(strike, sigma) وأسعار السوق على sigma مع تمرير price_fn وسيطًا ليعمل الرمز نفسه لأي نموذج. ولنموذج متعدد المعاملات (لهستون خمسة ولـ SABR ثلاثة بعد تثبيت beta) يكون البحث متعدد الأبعاد وقد يقع في حد أدنى محلي: ابدأ من عدة تقديرات أولية وقيّد المعاملات وفضّل الأخطاء النسبية أو بالتقلب الضمني على أخطاء الأسعار الخام كي لا تُهمل الخيارات الرخيصة خارج السعر. ثم تحقق من استقرار المعاملات المعايَرة من يوم لآخر ومن تسعير النموذج الجيد لخيارات لم يُعايَر عليها. والمطابقة التامة غير المستقرة فرط تعلم (overfitting).
# 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- تختار المعايرة معاملات النموذج التي تقلل مجموعًا (مرجّحًا) لمربعات أخطاء الأسعار؛ تحقق من الاستقرار عبر الأيام ومن الدقة على أسعار لم تُستعمل في المطابقة.
- يجد التنصيف جذرًا (تقلبًا ضمنيًا) بتنصيف مجال فيه تغير إشارة؛ ويجد القطع الذهبي حدًّا أدنى لدالة ذات وادٍ واحد بتقييم واحد في الخطوة.
- مرّر دالة التسعير وسيطًا وقيّد المعاملات وابدأ من عدة تقديرات في النماذج متعددة المعاملات وارفض المطابقات العبثية.
التالي: الشبكات العصبية للتنبؤ، تعلّم تنبؤ من البيانات بدل كتابة الصيغة.
المختبر: ما الذي يُقيَّم
- قِس عدم المطابقة باستخدام
sum_squared_error25 نقطة - حُلّ f(x) = 0 باستخدام
bisect_root25 نقطة - اعثر على أدنى نقطة باستخدام
golden_section_minimize25 نقطة - طابق السوق باستخدام
calibrate_sigma25 نقطة