12. SVI拟合实战:使用Python拟合SVI参数,构建平滑曲面

好了,终于到了动手环节。前面我们聊了那么多SVI的理论,什么总方差、参数含义、旋转拉伸……说实话,光看公式确实有点枯燥。但今天不一样,我们要真刀真枪地干一场——用Python把SVI参数拟合出来,再构建一个漂亮的波动率曲面。

我个人习惯是,先跑通一个最小可行案例,再逐步优化。所以这一章,咱们就从最基础的拟合开始,一步步来。

12.1 准备工作:数据与工具

首先,你得有数据。真实交易中,我们通常从期权市场拿到不同行权价、不同到期日的隐含波动率。但为了演示,我准备用一组模拟数据——这样你能清楚看到拟合效果。

核心思路:SVI模型拟合的是“总方差” vs “对数行权价”的关系。总方差 = 隐含波动率² × 到期时间。

嗯,这里要注意:千万别直接用波动率去拟合,那是错的。我刚开始做的时候也犯过这个错误,拟合出来的参数乱七八糟,后来才发现是单位没统一。

先看看我们要用的Python库:

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.optimize import minimize
from scipy.interpolate import griddata

核心工具就是scipy.optimize.minimize。说白了,SVI拟合就是一个非线性优化问题。你给它一个初始猜测,它帮你找到让误差最小的参数组合。

12.2 SVI模型的数学形式

再复习一下SVI的原始形式——别担心,就一行公式:

w(k) = a + b * (rho * (k - m) + sqrt((k - m)² + sigma²))

其中:

  • w(k):在log行权价k处的总方差
  • a:整体水平(截距)
  • b:倾斜程度(斜率)
  • rho:偏度(-1到1之间)
  • m:中心位置
  • sigma:平滑度(控制微笑的弯曲程度)

你想想看,这5个参数就决定了整个波动率微笑的形状。我在项目中遇到过最头疼的问题就是参数初始值怎么给——给不好,优化器直接跑飞。

我的经验:sigma初始值设为0.1左右,rho设为0,m设为0,a设为ATM总方差,b设为0.1。这样基本不会翻车。

12.3 构建目标函数

拟合的本质是“最小化模型值与市场值之间的误差”。我们通常用均方误差(MSE)作为损失函数。

def svi_function(k, a, b, rho, m, sigma):
    return a + b * (rho * (k - m) + np.sqrt((k - m)**2 + sigma**2))

def svi_loss(params, k, w_market):
    a, b, rho, m, sigma = params
    # 约束:b > 0, sigma > 0, |rho| < 1
    if b <= 0 or sigma <= 0 or abs(rho) >= 1:
        return 1e10
    w_model = svi_function(k, a, b, rho, m, sigma)
    return np.mean((w_model - w_market) ** 2)

看到那个if判断了吗?这是我自己加的“硬约束”。优化器有时候会乱跑,把b搞成负数,或者sigma变成0,那模型就废了。直接给它一个超大惩罚值,让它死心。

12.4 拟合单个到期日

我们先拟合一个到期日的数据。假设我们有10个行权价,对应的隐含波动率已经算好了。

# 模拟数据:10个log行权价和对应的总方差
np.random.seed(42)
k = np.linspace(-0.3, 0.3, 10)
w_true = 0.05 + 0.2 * (0.1 * (k - 0.02) + np.sqrt((k - 0.02)**2 + 0.08**2))
w_market = w_true + np.random.normal(0, 0.005, size=len(k))  # 加一点噪声

# 初始猜测
initial_guess = [0.05, 0.2, 0.0, 0.0, 0.1]

# 优化
result = minimize(svi_loss, initial_guess, args=(k, w_market),
                  method='Nelder-Mead', options={'maxiter': 5000, 'xatol': 1e-8, 'fatol': 1e-8})

a_opt, b_opt, rho_opt, m_opt, sigma_opt = result.x
print(f"拟合参数: a={a_opt:.4f}, b={b_opt:.4f}, rho={rho_opt:.4f}, m={m_opt:.4f}, sigma={sigma_opt:.4f}")

为什么用Nelder-Mead?因为它是无梯度优化方法,对这类非线性问题比较稳健。我曾经试过用L-BFGS-B,结果经常卡在局部最优。Nelder-Mead虽然慢一点,但胜在稳定。

12.5 构建整个曲面

单个到期日拟合完了,接下来我们要对每个到期日都做一遍。然后,把不同到期日的参数连起来,就构成了一个曲面。

但这里有个坑:不同到期日的参数是独立的,它们之间可能不连续。你想想看,如果3个月到期的参数和6个月到期的参数差别很大,那曲面就会出现“断层”。

我的做法是:先对每个到期日单独拟合,然后对参数进行“二次平滑”。说白了,就是把a、b、rho、m、sigma分别对到期时间T做插值,保证参数随T变化是光滑的。

# 假设我们有5个到期日,每个到期日拟合出一组参数
T = np.array([0.1, 0.25, 0.5, 1.0, 2.0])
params_array = np.array([
    [0.04, 0.18, -0.1, 0.01, 0.08],  # T=0.1
    [0.05, 0.20, -0.05, 0.02, 0.10], # T=0.25
    [0.06, 0.22, 0.0, 0.0, 0.12],    # T=0.5
    [0.07, 0.25, 0.05, -0.01, 0.15], # T=1.0
    [0.08, 0.28, 0.10, -0.02, 0.18]  # T=2.0
])

# 对每个参数做样条插值
from scipy.interpolate import interp1d
a_interp = interp1d(T, params_array[:, 0], kind='cubic')
b_interp = interp1d(T, params_array[:, 1], kind='cubic')
# ... 以此类推

注意:rho参数的范围是[-1, 1],插值后可能会越界。我建议插值后做一次clip,或者用logit变换把rho映射到实数域再插值。

12.6 可视化曲面

拟合完了,不画出来看看怎么行?我习惯用3D曲面图来检查效果。

# 生成网格
k_grid = np.linspace(-0.5, 0.5, 50)
T_grid = np.linspace(0.1, 2.0, 30)
K_mesh, T_mesh = np.meshgrid(k_grid, T_grid)

# 计算每个网格点的总方差
w_surface = np.zeros_like(K_mesh)
for i in range(len(T_grid)):
    a_t = a_interp(T_grid[i])
    b_t = b_interp(T_grid[i])
    rho_t = rho_interp(T_grid[i])
    m_t = m_interp(T_grid[i])
    sigma_t = sigma_interp(T_grid[i])
    w_surface[i, :] = svi_function(k_grid, a_t, b_t, rho_t, m_t, sigma_t)

# 转换为隐含波动率
iv_surface = np.sqrt(w_surface / T_mesh)

# 绘图
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(111, projection='3d')
ax.plot_surface(K_mesh, T_mesh, iv_surface, cmap='viridis', alpha=0.8)
ax.set_xlabel('Log Strike')
ax.set_ylabel('Time to Maturity')
ax.set_zlabel('Implied Volatility')
plt.title('SVI Fitted Volatility Surface')
plt.show()

看到那个平滑的曲面了吗?这就是SVI的魅力——用5个参数就能描述整个微笑形状,而且参数本身还有经济含义。rho代表偏度,b代表斜率,sigma代表微笑的弯曲程度……

12.7 避坑指南

最后,分享几个我踩过的坑:

  • 初始值敏感:SVI优化对初始值非常敏感。我建议先用粗网格搜索,找到大致区域,再精细优化。
  • 参数冗余:有时候a和b会互相补偿,导致参数不唯一。可以加正则化项,或者固定某个参数。
  • 极端行权价:深度虚值期权的数据质量很差,拟合时可以考虑加权,给中间行权价更高的权重。
  • 到期日太短:T小于7天的数据,总方差太小,拟合容易出问题。我一般会剔除这些数据点。

总结一下:SVI拟合不是一锤子买卖。你需要反复调整初始值、优化方法、约束条件,才能得到稳定的结果。但一旦你掌握了这个流程,构建波动率曲面就是分分钟的事。

好了,这一章的内容就到这里。代码都在上面了,你可以直接复制到Jupyter Notebook里跑一跑。记住,动手才是最好的学习方式。


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