把公式反过来用
布莱克-斯科尔斯公式把波动率变成价格。市场上正相反:价格是报出来的,波动率是未知的。隐含波动率就是使公式复现市场价格的 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 单调上升:vega 为正,所以误差只会穿过零一次。因此答案唯一,并且告诉你该往哪边调:模型太贵就降低 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))价格和波动率是同一个报价做市商报出波动率,买方用约定的公式把它换算成价格;这种换算是一种约定,并不代表相信该模型。对于给定的行权价、日期和利率,这个对应是一一对应的,所以任一方向都不会丢失信息。真正重要的是输入:隐含波动率的质量取决于你给它的利率、股息和期限。
- 隐含波动率是使
bs_call等于市场价格的sigma:没有反函数,只能搜索求得。 - 价格随
sigma上升,所以答案唯一,误差的符号告诉你调整方向;超出范围则无解。 - 区间减半收敛很快(约 30 步得到 8 位);微笑曲线就是隐含波动率对行权价的曲线。
下一节:二分法与牛顿法求根,这种搜索背后的通用方法,以及一种快得多的方法。
实验:评分内容
- 用
call_price_error比较模型与市场25 分 - 用
implied_vol_scan在波动率网格中搜索25 分 - 用
implied_vol反解公式25 分 - 用
implied_vol_smile读出整个微笑曲线25 分