第10章 曲面插值与外推:时间维度与执行价维度的插值方法,边界处理技巧

波动率曲面这东西,说白了就是个三维的怪物。你手头只有几十个期权报价,却要填满整个执行价-到期日的网格。怎么填?这就是插值与外推的活儿。

我做了这么多年量化,最深的体会是:插值方法选得好,定价就成功了一半。另一半?边界处理。嗯,这里要注意,边界往往是坑最多的地方。

10.1 为什么需要插值与外推?

你想想看,市场上交易的期权合约是有限的。比如50ETF期权,可能只有5个执行价、4个到期月。但你的定价模型需要任意执行价、任意期限的波动率。怎么办?

两个场景:

  • 场内交易:报价稀疏,需要填充中间点
  • 场外交易:客户要的条款市场上根本没有,必须外推

我个人习惯把这个问题拆成两个维度:

  • 执行价维度:同一期限下,不同执行价的波动率插值
  • 时间维度:同一执行价下,不同期限的波动率插值

先处理哪个?我建议先做执行价维度,再做时间维度。为什么?因为执行价维度的数据通常更密集,插值结果更稳定。

10.2 执行价维度的插值方法

10.2.1 线性插值——最简单,但别乱用

线性插值,就是两点之间画直线。公式很简单:

σ(K) = σ(K₁) + (σ(K₂) - σ(K₁)) * (K - K₁) / (K₂ - K₁)

但我在项目中遇到过一个问题:线性插值会导致波动率微笑形状失真。尤其是深度实值和深度虚值区域,线性插值会抹掉微笑的曲率。

避坑指南:我曾经用线性插值做价差套利策略的回测,结果发现插值后的曲面出现了套利机会——其实是插值方法引入的假象。后来改用样条插值才解决。

10.2.2 三次样条插值——我的首选

三次样条插值,说白了就是用分段三次多项式来拟合。它保证了:

  • 曲线连续(C⁰)
  • 一阶导数连续(C¹)
  • 二阶导数连续(C²)

代码实现其实不复杂:

import numpy as np
from scipy.interpolate import CubicSpline

# 已知的执行价和波动率
K_known = np.array([2.5, 2.6, 2.7, 2.8, 2.9, 3.0])
sigma_known = np.array([0.25, 0.23, 0.22, 0.21, 0.22, 0.24])

# 创建三次样条插值器
cs = CubicSpline(K_known, sigma_known, bc_type='natural')

# 在任意执行价上求值
K_new = np.linspace(2.5, 3.0, 50)
sigma_new = cs(K_new)

这里要注意边界条件(bc_type)。我一般用'natural',也就是二阶导数为零。但如果你有更精确的边界信息,也可以用'clamped'指定一阶导数。

10.2.3 带约束的插值——防止套利

插值不是随便插的。波动率曲面必须满足无套利条件:

  • 蝶式价差无套利:波动率关于执行价的二阶导数不能太大
  • 日历价差无套利:总方差关于期限单调递增

我建议用单调保形插值(PCHIP),它能保证插值结果不产生新的极值点:

from scipy.interpolate import PchipInterpolator

pchip = PchipInterpolator(K_known, sigma_known)
sigma_pchip = pchip(K_new)
个人经验:PCHIP虽然不如三次样条光滑,但它不会在数据点之间乱跳。对于波动率曲面这种需要严格单调性的场景,PCHIP往往更安全。

10.3 时间维度的插值方法

10.3.1 总方差插值——比波动率插值更合理

时间维度上,我强烈建议不要直接插值波动率。为什么?因为波动率是年化的,不同期限的波动率不可比。

正确的做法是插值总方差:

总方差 = σ² × T

举个例子:

期限(天) 波动率 总方差
30 0.20 0.20² × 30/365 = 0.00329
60 0.22 0.22² × 60/365 = 0.00796
90 0.24 0.24² × 90/365 = 0.01420

然后对总方差做线性插值,再开方得到波动率:

def interpolate_vol_by_total_variance(T_known, sigma_known, T_target):
    # 计算总方差
    total_var_known = sigma_known**2 * T_known
    
    # 线性插值总方差
    total_var_target = np.interp(T_target, T_known, total_var_known)
    
    # 转回波动率
    sigma_target = np.sqrt(total_var_target / T_target)
    return sigma_target
核心要点:总方差插值保证了日历价差无套利。直接插值波动率可能导致短期波动率高于长期,这在逻辑上是说不通的。

10.3.2 时间维度的样条插值

如果期限点比较多(比如有1周、2周、1月、2月、3月、6月、1年),我建议用样条插值总方差:

from scipy.interpolate import UnivariateSpline

# 期限(年)和对应的总方差
T_years = np.array([1/12, 2/12, 3/12, 6/12, 1.0])
total_var = np.array([0.0033, 0.0080, 0.0142, 0.0300, 0.0625])

# 样条插值,s控制平滑程度
spline = UnivariateSpline(T_years, total_var, s=0.001)

# 求任意期限的总方差
T_target = 0.25  # 3个月
total_var_target = spline(T_target)
sigma_target = np.sqrt(total_var_target / T_target)

s参数很关键。s越大越平滑,但可能偏离数据点。我一般设s=0.001,既保证平滑又不失真。

10.4 边界处理技巧——最容易翻车的地方

10.4.1 执行价维度的外推

市场报价通常只覆盖平值附近。深度实值和深度虚值怎么办?

我常用的三种方法:

  • 常数外推:直接用最远端的波动率。简单但粗糙。
  • 线性外推:用最后两个点的斜率延伸。小心斜率太陡。
  • 模型外推:用SVI或SSVI模型拟合后外推。最专业,但计算量大。
我曾经踩过的坑:用线性外推时,斜率是负的,结果外推到深度虚值时波动率变成了负数。嗯,波动率不能为负,这是常识。从那以后我对外推结果都加了边界检查。

我建议的边界处理策略:

def safe_extrapolate(K, sigma, K_min, K_max):
    """
    安全的外推函数
    K_min, K_max: 允许的最小和最大执行价
    """
    # 内部用样条插值
    cs = CubicSpline(K, sigma, bc_type='natural')
    
    # 外推时用常数
    sigma_left = sigma[0]   # 左侧常数
    sigma_right = sigma[-1] # 右侧常数
    
    # 组合结果
    def vol_func(k):
        if k < K[0]:
            return sigma_left
        elif k > K[-1]:
            return sigma_right
        else:
            return cs(k)
    
    return vol_func

10.4.2 时间维度的外推

时间维度的外推更棘手。因为波动率有期限结构,短期和长期的行为完全不同。

我的做法:

  • 短期外推(小于最短期限):用最短期限的波动率,但加一个衰减因子。因为越短期波动率越不稳定。
  • 长期外推(大于最长期限):用最长期限的波动率,或者向历史均值回归。

代码示例:

def time_extrapolate(T, T_known, sigma_known, long_term_vol=0.20):
    """
    时间维度外推
    T: 目标期限
    long_term_vol: 长期波动率假设
    """
    T_min = T_known[0]
    T_max = T_known[-1]
    
    if T < T_min:
        # 短期外推:向T=0衰减
        # 用平方根时间法则
        sigma_short = sigma_known[0] * np.sqrt(T / T_min)
        return max(sigma_short, 0.05)  # 不低于5%
    
    elif T > T_max:
        # 长期外推:向长期均值回归
        weight = np.exp(-(T - T_max) / 2)  # 衰减权重
        sigma_long = weight * sigma_known[-1] + (1 - weight) * long_term_vol
        return sigma_long
    
    else:
        # 内部用总方差插值
        return interpolate_vol_by_total_variance(T_known, sigma_known, T)

10.5 完整的曲面构建流程

好了,把上面所有东西串起来。这是我个人习惯的完整流程:

  1. 数据清洗:去除明显异常的报价(比如波动率超过100%或低于5%)
  2. 执行价维度插值:对每个期限,用PCHIP插值得到标准执行价网格上的波动率
  3. 时间维度插值:对每个执行价,用总方差样条插值得到任意期限的波动率
  4. 边界处理:执行价边界用常数外推,时间边界用回归外推
  5. 无套利检查:检查蝶式价差和日历价差是否无套利
核心原则:插值方法的选择取决于你的应用场景。做高频交易,线性插值就够了;做结构化产品定价,必须用样条加约束;做风险管理,边界处理比插值本身更重要。

10.6 本章知识体系

下面这张图总结了本章的核心逻辑:

曲面插值与外推知识体系 离散报价数据 执行价维度插值 时间维度插值 线性插值 三次样条 PCHIP 总方差插值 样条插值 回归外推 边界处理 + 无套利检查 完整波动率曲面

嗯,到这里插值与外推的核心内容就讲完了。记住:插值是艺术,边界处理是科学。多在实践中积累经验,慢慢就能找到最适合自己场景的方法。


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