每个行权价和时间对应一个波动率
Black-Scholes 用一个波动率给看涨期权定价,而市场对每个行权价和到期日都报出不同的隐含波动率。局部波动率模型让波动率依赖于价格水平和时间 sigma(S, t),同时只保留一个随机源,从而一次性复现所有这些价格。Bruno Dupire 在 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 pricesDupire 公式
Dupire 公式用看涨期权价格曲面的三个导数来表达局部方差: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)。与 Heston 相比,局部波动率在构造上恰好拟合今天的期权价格,但其未来微笑的动态不现实(偏斜会随时间变平),这对依赖未来微笑的产品很重要。因此许多交易台使用两者混合的局部-随机波动率。
# 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)用一个随机源复现整张看涨期权价格曲面;Dupire 公式直接由价格给出它。 - Dupire:
(dC/dT + (r - q) K dC/dK + q C) / (0.5 K^2 d2C/dK2);在平滑且无套利的曲面上用有限差分求导。 - 每一步按当前价格查
sigma_loc来模拟;今天的完美拟合并不保证明天微笑动态的真实性。
下一节:SABR 模型,带闭式微笑的随机波动率模型。
实验:评分内容
- 用
finite_difference做数值求导25 分 - 在
local_variance中应用 Dupire 公式25 分 - 用
lookup_local_vol读取曲面25 分 - 用
local_vol_step模拟一步25 分