期权是量化交易中最有意思的品种之一,定价模型是理解期权的基础。这篇从Black-Scholes公式出发,用Python实现定价和Greeks计算。
Black-Scholes公式
BS模型的基本假设:标的价格服从几何布朗运动,无摩擦市场,无套利,欧式期权。
看涨期权(Call)定价公式:
看跌期权(Put)定价公式:
其中:
各参数含义:
- S:标的当前价格
- K:执行价格(行权价)
- T:到期时间(年)
- r:无风险利率
- sigma:标的波动率(年化)
- N():标准正态分布的累积分布函数
直觉理解:S * N(d1)是标的在行权概率加权下的期望价值,K * e^{-rT} * N(d2)是行权成本的现值。两者之差就是看涨期权的公平价格。
Python实现
import numpy as np
from scipy.stats import norm
class BlackScholes:
"""Black-Scholes期权定价模型"""
def __init__(self, S, K, T, r, sigma):
"""
S: 标的价格
K: 执行价格
T: 到期时间(年)
r: 无风险利率
sigma: 波动率
"""
self.S = S
self.K = K
self.T = T
self.r = r
self.sigma = sigma
self._calc_d()
def _calc_d(self):
"""计算d1和d2"""
self.d1 = (np.log(self.S / self.K) +
(self.r + 0.5 * self.sigma**2) * self.T) / (self.sigma * np.sqrt(self.T))
self.d2 = self.d1 - self.sigma * np.sqrt(self.T)
def call_price(self):
"""看涨期权价格"""
return (self.S * norm.cdf(self.d1) -
self.K * np.exp(-self.r * self.T) * norm.cdf(self.d2))
def put_price(self):
"""看跌期权价格"""
return (self.K * np.exp(-self.r * self.T) * norm.cdf(-self.d2) -
self.S * norm.cdf(-self.d1))
# ===== Greeks =====
def delta(self, option_type='call'):
"""
Delta: 标的价格变动1单位,期权价格的变动量
Call Delta: 0~1, Put Delta: -1~0
"""
if option_type == 'call':
return norm.cdf(self.d1)
else:
return norm.cdf(self.d1) - 1
def gamma(self):
"""
Gamma: 标的价格变动1单位,Delta的变动量
Call和Put的Gamma相同
"""
return norm.pdf(self.d1) / (self.S * self.sigma * np.sqrt(self.T))
def theta(self, option_type='call'):
"""
Theta: 时间流逝1天,期权价格的变动量(通常为负)
返回每日theta(除以365)
"""
common = -(self.S * norm.pdf(self.d1) * self.sigma) / (2 * np.sqrt(self.T))
if option_type == 'call':
theta_annual = common - self.r * self.K * np.exp(-self.r * self.T) * norm.cdf(self.d2)
else:
theta_annual = common + self.r * self.K * np.exp(-self.r * self.T) * norm.cdf(-self.d2)
return theta_annual / 365
def vega(self):
"""
Vega: 波动率变动1%,期权价格的变动量
Call和Put的Vega相同
"""
return self.S * norm.pdf(self.d1) * np.sqrt(self.T) / 100
def rho(self, option_type='call'):
"""
Rho: 利率变动1%,期权价格的变动量
"""
if option_type == 'call':
return self.K * self.T * np.exp(-self.r * self.T) * norm.cdf(self.d2) / 100
else:
return -self.K * self.T * np.exp(-self.r * self.T) * norm.cdf(-self.d2) / 100
# 使用示例
bs = BlackScholes(S=100, K=105, T=0.5, r=0.05, sigma=0.2)
print(f"Call价格: {bs.call_price():.4f}")
print(f"Put价格: {bs.put_price():.4f}")
print(f"Call Delta: {bs.delta('call'):.4f}")
print(f"Put Delta: {bs.delta('put'):.4f}")
print(f"Gamma: {bs.gamma():.4f}")
print(f"Call Theta: {bs.theta('call'):.4f} (每日)")
print(f"Put Theta: {bs.theta('put'):.4f} (每日)")
print(f"Vega: {bs.vega():.4f}")
输出:
Call价格: 4.3592
Put价格: 6.7659
Call Delta: 0.4407
Put Delta: -0.5593
Gamma: 0.0273
Call Theta: -0.0196 (每日)
Put Theta: -0.0053 (每日)
Vega: 0.2728
Greeks的含义
Greeks是期权交易中最重要的风险度量:
Delta:期权价格对标的价格的一阶敏感度。Call的Delta在0到1之间,Put在-1到0之间。ATM(平值)期权的Delta绝对值约0.5。Delta也可以近似理解为期权到期为实值的概率。
Gamma:Delta对标的价格的敏感度(二阶导数)。ATM期权的Gamma最大,深度实值/虚值Gamma趋近于0。Gamma大意味着Delta变化快,需要频繁对冲。
Theta:时间衰减。通常为负——随着到期日临近,期权价值减少。ATM期权的Theta绝对值最大。卖方收取Theta,买方支付Theta,这是期权买卖双方博弈的核心。
Vega:波动率敏感度。波动率上升期权更贵(不确定性=价值)。长期期权的Vega更大。在波动率交易中,Vega是核心指标。
Rho:利率敏感度。在低利率环境下通常不太重要。利率上升有利于Call、不利于Put。
隐含波动率
BS公式里的sigma是"预期未来波动率",但实际交易中我们看到的是期权的市场价格。从市场价格反推出的波动率叫隐含波动率(Implied Volatility, IV)。
IV没有解析解,需要数值方法求解:
from scipy.optimize import brentq
def implied_volatility(market_price, S, K, T, r, option_type='call'):
"""
从市场价格反推隐含波动率
使用Brent方法求解
"""
def objective(sigma):
bs = BlackScholes(S, K, T, r, sigma)
if option_type == 'call':
return bs.call_price() - market_price
else:
return bs.put_price() - market_price
try:
iv = brentq(objective, 0.001, 5.0, xtol=1e-8)
return iv
except ValueError:
return np.nan # 无解(通常是价格不合理)
# 示例:已知市场价格反推IV
market_call_price = 5.0
iv = implied_volatility(market_call_price, S=100, K=105, T=0.5, r=0.05)
print(f"隐含波动率: {iv:.4f} ({iv*100:.2f}%)")
隐含波动率的意义:
- IV高说明市场认为未来波动大(通常在财报、事件前飙升)
- 不同行权价的IV画出来是一条"微笑曲线"(volatility smile/skew)
- IV的期限结构反映了不同到期日的波动率预期
期权价格可视化
用matplotlib画几个经典图表:
import matplotlib.pyplot as plt
def plot_option_price_vs_spot(K=100, T=0.5, r=0.05, sigma=0.2):
"""期权价格随标的价格变化"""
S_range = np.linspace(60, 140, 200)
call_prices = []
put_prices = []
for S in S_range:
bs = BlackScholes(S, K, T, r, sigma)
call_prices.append(bs.call_price())
put_prices.append(bs.put_price())
fig, ax = plt.subplots(1, 1, figsize=(10, 6))
ax.plot(S_range, call_prices, 'b-', label='Call', linewidth=2)
ax.plot(S_range, put_prices, 'r-', label='Put', linewidth=2)
# 内在价值
call_intrinsic = np.maximum(S_range - K, 0)
put_intrinsic = np.maximum(K - S_range, 0)
ax.plot(S_range, call_intrinsic, 'b--', alpha=0.5, label='Call内在价值')
ax.plot(S_range, put_intrinsic, 'r--', alpha=0.5, label='Put内在价值')
ax.axvline(x=K, color='gray', linestyle=':', alpha=0.5)
ax.set_xlabel('标的价格')
ax.set_ylabel('期权价格')
ax.set_title(f'期权价格 (K={K}, T={T}, σ={sigma})')
ax.legend()
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('option_price.png', dpi=100)
plt.close()
def plot_greeks(K=100, T=0.5, r=0.05, sigma=0.2):
"""Greeks随标的价格变化"""
S_range = np.linspace(70, 130, 200)
deltas, gammas, thetas, vegas = [], [], [], []
for S in S_range:
bs = BlackScholes(S, K, T, r, sigma)
deltas.append(bs.delta('call'))
gammas.append(bs.gamma())
thetas.append(bs.theta('call'))
vegas.append(bs.vega())
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
axes[0,0].plot(S_range, deltas, 'b-', linewidth=2)
axes[0,0].set_title('Delta (Call)')
axes[0,0].axhline(y=0.5, color='gray', linestyle=':', alpha=0.5)
axes[0,0].grid(True, alpha=0.3)
axes[0,1].plot(S_range, gammas, 'g-', linewidth=2)
axes[0,1].set_title('Gamma')
axes[0,1].grid(True, alpha=0.3)
axes[1,0].plot(S_range, thetas, 'r-', linewidth=2)
axes[1,0].set_title('Theta (Call, 每日)')
axes[1,0].grid(True, alpha=0.3)
axes[1,1].plot(S_range, vegas, 'm-', linewidth=2)
axes[1,1].set_title('Vega')
axes[1,1].grid(True, alpha=0.3)
for ax in axes.flat:
ax.set_xlabel('标的价格')
ax.axvline(x=K, color='gray', linestyle=':', alpha=0.5)
plt.suptitle(f'Greeks (K={K}, T={T}, σ={sigma})', fontsize=14)
plt.tight_layout()
plt.savefig('greeks.png', dpi=100)
plt.close()
# 生成图表
plot_option_price_vs_spot()
plot_greeks()
从图表可以直观看到:
- 期权价格曲线是凸的,时间价值在ATM处最大
- Delta是一个S型曲线,从0到1渐变
- Gamma在ATM处有尖峰,这就是为什么ATM期权对标的价格最敏感
- Theta在ATM处最负,时间衰减最严重
- Vega同样在ATM处最大
Put-Call Parity验证
BS模型满足Put-Call Parity:C - P = S - K * e^{-rT}
bs = BlackScholes(S=100, K=105, T=0.5, r=0.05, sigma=0.2)
lhs = bs.call_price() - bs.put_price()
rhs = bs.S - bs.K * np.exp(-bs.r * bs.T)
print(f"C - P = {lhs:.4f}")
print(f"S - Ke^(-rT) = {rhs:.4f}")
print(f"差值: {abs(lhs - rhs):.10f}")
# 差值应该接近0(浮点误差量级)
这个等式是期权定价的基础约束,如果市场上C和P的价格违反了这个关系(排除交易成本后),就存在套利机会。
模型局限
BS模型的几个主要局限:
- 假设波动率恒定——现实中波动率是变化的(volatility clustering)
- 假设对数正态分布——现实中收益率有厚尾(fat tail)
- 假设连续交易、无摩擦——现实有交易成本和流动性约束
- 只适用于欧式期权——美式期权需要用二叉树或蒙特卡洛
尽管有这些局限,BS模型仍然是期权交易的基础语言。实际中通常用BS公式配合隐含波动率曲面来定价,本质上是用市场价格去修正模型的缺陷。
总结
BS模型的Python实现并不复杂,scipy.stats.norm搞定核心计算。重要的是理解每个Greek的含义和实际应用——Delta对冲、Gamma scalping、Theta收租、Vega交易,每个指标背后都是一种交易策略。把这些搞清楚,才算真正入门期权。