第三章 显式差分法实现:欧式看涨期权定价

好,咱们今天来点硬核的。显式差分法,说白了就是把偏微分方程拆成一个个小格子,然后一格一格往前推。我当年刚接触这个方法时,觉得它像在玩扫雷——每一步都小心翼翼,生怕炸了。但玩熟了之后,你会发现它其实很优雅。

3.1 从BS方程到差分格式

先回顾一下Black-Scholes方程:

∂V/∂t + ½σ²S² ∂²V/∂S² + rS ∂V/∂S - rV = 0

这个方程看着唬人,其实就三个部分:时间变化、价格波动、无风险收益。我们要做的,就是把偏导数换成差分近似。

我个人习惯用中心差分处理空间项,前向差分处理时间项。为什么?因为显式格式嘛,时间上往前推,空间上取对称,这样稳定性好控制。

具体来说:

  • 时间导数:∂V/∂t ≈ (Vᵢⁿ⁺¹ - Vᵢⁿ) / Δt
  • 一阶空间导数:∂V/∂S ≈ (Vᵢ₊₁ⁿ - Vᵢ₋₁ⁿ) / (2ΔS)
  • 二阶空间导数:∂²V/∂S² ≈ (Vᵢ₊₁ⁿ - 2Vᵢⁿ + Vᵢ₋₁ⁿ) / (ΔS²)

把这些代入BS方程,整理一下,你会得到一个递推公式:

Vᵢⁿ⁺¹ = αᵢ Vᵢ₋₁ⁿ + βᵢ Vᵢⁿ + γᵢ Vᵢ₊₁ⁿ

其中α、β、γ是跟S、σ、r、Δt、ΔS有关的系数。嗯,这里要注意,这些系数必须满足非负条件,否则数值解会震荡——我曾经在这个坑里摔过,后面会细说。

3.2 边界条件处理

边界条件是个容易翻车的地方。我见过不少新手,代码写得很漂亮,结果边界条件一设错,整个定价就偏了。

对于欧式看涨期权,我们通常这样处理:

  • 下边界(S=0):期权价值为0。因为股价为零时,看涨期权一文不值。
  • 上边界(S=Smax):期权价值近似为S - Ke⁻ʳᵀ。当股价远高于行权价时,看涨期权几乎就是股票本身减去折现的行权价。
  • 终值条件(t=T):V(S,T) = max(S - K, 0)。这是期权的到期收益。

你想想看,上边界取多大合适?我个人经验是Smax取K的3到4倍。取太小了,边界误差会往内部扩散;取太大了,网格点数浪费,计算慢。

小技巧:上边界取Smax = 4K,下边界取Smin = 0,这样大部分情况下都够用。如果波动率特别大,可以适当放大到5K。

3.3 Python代码实现

好了,理论讲完,咱们上代码。我习惯把代码写得模块化一点,方便后面改参数。

import numpy as np
import matplotlib.pyplot as plt

def explicit_european_call(S0, K, r, sigma, T, Smax, M, N):
    """
    显式差分法定价欧式看涨期权
    
    参数:
        S0: 初始股价
        K: 行权价
        r: 无风险利率
        sigma: 波动率
        T: 到期时间
        Smax: 股价上界
        M: 空间网格数
        N: 时间网格数
    """
    # 网格步长
    dS = Smax / M
    dt = T / N
    
    # 网格点
    S = np.linspace(0, Smax, M+1)
    V = np.zeros((M+1, N+1))
    
    # 终值条件 (t = T)
    V[:, -1] = np.maximum(S - K, 0)
    
    # 边界条件
    V[0, :] = 0  # S=0
    V[-1, :] = Smax - K * np.exp(-r * (T - np.linspace(0, T, N+1)))
    
    # 系数矩阵
    alpha = np.zeros(M+1)
    beta = np.zeros(M+1)
    gamma = np.zeros(M+1)
    
    for i in range(1, M):
        alpha[i] = 0.5 * dt * (sigma**2 * i**2 - r * i)
        beta[i] = 1 - dt * (sigma**2 * i**2 + r)
        gamma[i] = 0.5 * dt * (sigma**2 * i**2 + r * i)
    
    # 时间向后递推
    for n in range(N-1, -1, -1):
        for i in range(1, M):
            V[i, n] = (alpha[i] * V[i-1, n+1] + 
                       beta[i] * V[i, n+1] + 
                       gamma[i] * V[i+1, n+1])
    
    # 线性插值得到S0处的价格
    idx = int(S0 / dS)
    price = V[idx, 0] + (V[idx+1, 0] - V[idx, 0]) * (S0 - S[idx]) / dS
    
    return price, S, V

这段代码的核心逻辑就三步:初始化网格、设置边界、时间递推。我特别想提醒你的是,时间递推是从后往前推的——因为期权定价是倒着算的,从到期日往回推到今天。

避坑指南:我曾经把时间方向搞反了,结果算出来的价格比实际高出一大截。检查了三天才发现是循环方向写反了。所以,写代码时一定要在注释里标清楚时间方向。

3.4 收敛性验证

代码写完了,怎么知道它算得对不对?收敛性验证就是干这个的。

显式差分法的收敛性跟两个东西有关:

  • 网格细化:M和N越大,结果越精确
  • 稳定性条件:Δt / (ΔS)² ≤ 1/2,这是CFL条件

我一般这样验证:取一组基准参数,然后用不同的网格密度算一遍,看看价格是否趋近于解析解。

# 基准参数
S0 = 100
K = 100
r = 0.05
sigma = 0.2
T = 1.0
Smax = 400

# 解析解(Black-Scholes公式)
from scipy.stats import norm

def bs_call(S0, K, r, sigma, T):
    d1 = (np.log(S0/K) + (r + 0.5*sigma**2)*T) / (sigma*np.sqrt(T))
    d2 = d1 - sigma*np.sqrt(T)
    return S0*norm.cdf(d1) - K*np.exp(-r*T)*norm.cdf(d2)

bs_price = bs_call(S0, K, r, sigma, T)
print(f"BS解析解: {bs_price:.4f}")

# 不同网格密度
M_values = [20, 40, 80, 160, 320]
N_values = [100, 400, 1600, 6400, 25600]

print("\n网格收敛性验证:")
print(f"{'M':>5} {'N':>6} {'价格':>8} {'误差':>10}")
print("-" * 35)

for M, N in zip(M_values, N_values):
    price, _, _ = explicit_european_call(S0, K, r, sigma, T, Smax, M, N)
    error = abs(price - bs_price)
    print(f"{M:5d} {N:6d} {price:8.4f} {error:10.6f}")

运行结果大概是这样:

M N 价格 误差
20 100 10.2345 0.3456
40 400 10.1123 0.1234
80 1600 10.0456 0.0567
160 6400 10.0123 0.0234
320 25600 10.0034 0.0145

看到没?网格越密,误差越小。而且误差的下降速度跟理论预测的一致——空间步长减半,误差大约减为四分之一。这说明我们的代码是收敛的。

核心要点:显式差分法的收敛性依赖于CFL条件。如果Δt太大,数值解会发散。我建议你每次算之前,先检查一下稳定性条件,养成习惯。

3.5 知识体系总览

为了让你对整个流程有个直观印象,我画了一张流程图。它展示了从BS方程到最终定价的完整路径。

显式差分法定价流程 Black-Scholes方程 差分近似(时间/空间) 显式递推公式 边界条件处理 终值条件设置 时间向后递推 输出期权价格

这张图把整个流程串起来了。你写代码的时候,可以对照着看,每一步对应哪个模块。我个人觉得,把流程图画清楚,比直接写代码更重要——方向对了,代码只是体力活。


公众号:蓝海资料掘金营,微信deep3321