Python量化进阶:期权定价Black-Scholes模型

期权是量化交易中最有意思的品种之一,定价模型是理解期权的基础。这篇从Black-Scholes公式出发,用Python实现定价和Greeks计算。

Black-Scholes公式

BS模型的基本假设:标的价格服从几何布朗运动,无摩擦市场,无套利,欧式期权。

看涨期权(Call)定价公式:

C=SN(d1)KerTN(d2)C = S \cdot N(d_1) - K \cdot e^{-rT} \cdot N(d_2)

看跌期权(Put)定价公式:

P=KerTN(d2)SN(d1)P = K \cdot e^{-rT} \cdot N(-d_2) - S \cdot N(-d_1)

其中:

d1=ln(S/K)+(r+σ2/2)TσTd_1 = \frac{\ln(S/K) + (r + \sigma^2/2)T}{\sigma\sqrt{T}}

d2=d1σTd_2 = d_1 - \sigma\sqrt{T}

各参数含义:

  • 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交易,每个指标背后都是一种交易策略。把这些搞清楚,才算真正入门期权。