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里跑一跑。记住,动手才是最好的学习方式。