第12章 曲面外推与内插:边界处理、极端行权价与期限的预测

做波动率曲面的人,迟早要面对一个问题:边界怎么处理?

我刚开始做期权定价系统时,觉得曲面构建嘛,把市场上的期权价格倒推成隐含波动率,再插值一下就行了。结果第一次遇到深度虚值期权——行权价离当前价格老远,市场上根本没有报价——系统直接崩了。嗯,从那以后我才意识到,曲面外推和内插才是真正考验功力的地方。

为什么需要外推与内插?

说白了,市场数据永远是不完整的。你打开交易终端,能看到几十个行权价、几个期限的报价。但实际定价时,你需要的是任意行权价、任意期限的波动率。比如一个奇异期权,行权价是标的价格的150%,期限是18个月——市场上大概率没有这个点。

这时候怎么办?

  • 内插:在已知数据点之间填充。比如已知1个月和3个月的波动率,要算2个月的。
  • 外推:在已知数据范围之外预测。比如行权价超出市场报价范围,或者期限超过最长期限。

内插相对简单,外推才是真正的挑战。我在项目中遇到过好几次,外推方法选错了,整个曲面在边界处出现奇怪的扭曲,导致定价结果完全不可用。

内插方法:从简单到实用

1. 线性内插

最直接的方法。给定两个点,中间的点按比例取值。

def linear_interp(x, x1, x2, y1, y2):
    return y1 + (y2 - y1) * (x - x1) / (x2 - x1)

但说实话,线性内插在波动率曲面上效果一般。为什么?因为波动率曲面通常不是线性的,尤其是期限结构——短期波动率变化快,长期波动率变化慢。线性内插会忽略这种曲率。

2. 样条内插

我个人习惯用三次样条。它能在保证平滑的同时,捕捉曲面的局部特征。

from scipy.interpolate import CubicSpline
import numpy as np

# 假设已知期限和对应的波动率
tenors = np.array([0.25, 0.5, 1.0, 2.0, 3.0])
vols = np.array([0.25, 0.22, 0.20, 0.19, 0.185])

# 构建三次样条
cs = CubicSpline(tenors, vols, bc_type='natural')

# 内插任意期限
target_tenor = 1.5
interp_vol = cs(target_tenor)
print(f"1.5年期限的波动率: {interp_vol:.4f}")

这里要注意边界条件。我一般用natural(自然边界),也就是二阶导数为零。这样在边界处不会出现过度振荡。

3. 双线性/双三次内插(二维)

当你要同时处理行权价和期限两个维度时,就需要二维内插了。双线性内插是最简单的二维方法:

def bilinear_interp(x, y, x_grid, y_grid, z_grid):
    # x: 行权价, y: 期限
    # x_grid, y_grid: 网格坐标, z_grid: 波动率矩阵
    from scipy.interpolate import RectBivariateSpline
    interp_func = RectBivariateSpline(x_grid, y_grid, z_grid, kx=1, ky=1)
    return interp_func(x, y)[0][0]

但说实话,双线性内插在曲面上的表现不够平滑。如果你追求更光滑的结果,试试双三次样条(kx=3, ky=3)。

我的经验:内插时,尽量在对数行权价对数期限空间上进行。因为波动率曲面在这些变换后的空间中更接近线性,内插效果更好。

外推方法:边界处的艺术

外推比内插难得多。你想想看,数据点之外的地方,没有任何信息约束,模型可以随便跑偏。我曾经见过一个团队用多项式外推,结果在深度虚值区域波动率变成了负数——这显然不合理。

1. 常数外推

最简单也最保守的方法:边界之外的波动率等于边界值。

def constant_extrap(vol_surface, strike, tenor):
    # 如果行权价超出范围,取最近边界的值
    if strike < min_strike:
        return vol_surface[min_strike, tenor]
    elif strike > max_strike:
        return vol_surface[max_strike, tenor]
    # 期限同理

这个方法的好处是稳定,不会出现离谱的值。但缺点也很明显——它假设波动率在边界外不再变化,这通常不符合市场行为。

2. 线性外推

用边界处的斜率继续延伸。比如在期限维度上,用最后两个已知点的斜率外推:

def linear_extrap(tenor, tenors, vols):
    # 用最后两个点计算斜率
    slope = (vols[-1] - vols[-2]) / (tenors[-1] - tenors[-2])
    return vols[-1] + slope * (tenor - tenors[-1])

但这里有个坑:如果斜率是负的(长期波动率下降),外推太久波动率可能变成负值。我一般会加一个下限保护,比如波动率不低于5%。

3. SVI参数化外推

这是我最推荐的方法。SVI(Stochastic Volatility Inspired)模型用几个参数来描述波动率微笑的形状,然后外推时保持这些参数不变。

def svi_extrap(strike, forward, params):
    # params: [a, b, rho, m, sigma]
    a, b, rho, m, sigma = params
    k = np.log(strike / forward)
    # SVI公式
    w = a + b * (rho * (k - m) + np.sqrt((k - m)**2 + sigma**2))
    return np.sqrt(w / max(tenor, 0.01))  # 转换为波动率

SVI的好处是:它天然保证了波动率在极端行权价处是线性的(在方差空间上),而且不会出现负值。我在做外汇期权系统时,深度虚值期权的定价全靠SVI外推撑着。

注意:SVI参数拟合需要小心。我曾经遇到过参数不稳定的情况——稍微调整一下输入数据,参数就大变样。建议用加权最小二乘,给流动性好的点更高权重。

极端行权价的预测

极端行权价,说白了就是深度实值和深度虚值。这些地方市场报价很少,但奇异期权经常需要。

我个人习惯用尾部行为假设来预测:

  • 深度虚值看涨:波动率应该趋于一个常数(或者缓慢上升)。因为极端虚值期权反映的是尾部风险,市场通常要求更高的波动率补偿。
  • 深度虚值看跌:同理,但要注意负偏斜(skew)的影响。在股票市场,深度虚值看跌的波动率通常高于深度虚值看涨。
  • 深度实值:波动率应该趋于平价波动率。因为深度实值期权几乎等于标的资产本身,波动率的影响很小。

一个实用的方法是:用线性外推+上限/下限。比如:

def extreme_strike_vol(strike, forward, atm_vol, skew, max_vol=1.0, min_vol=0.05):
    moneyness = strike / forward
    # 基础波动率 = ATM波动率 + skew * (moneyness - 1)
    base_vol = atm_vol + skew * (moneyness - 1)
    # 限制在合理范围内
    return max(min_vol, min(max_vol, base_vol))

嗯,这个方法虽然简单,但在大多数情况下够用了。如果你需要更精确的预测,可以考虑加入随机波动率模型(比如Heston模型)来生成理论上的极端行权价波动率。

极端期限的预测

期限外推比行权价外推更棘手。为什么?因为期限结构有明确的物理意义——短期波动率反映的是即时不确定性,长期波动率反映的是均值回归水平。

我一般遵循以下原则:

  • 短期外推(小于1个月):波动率应该上升。因为越临近到期,不确定性越大(事件风险)。但要注意,不要外推到0期限——那会变成无穷大。
  • 长期外推(大于5年):波动率应该趋于一个常数。这是均值回归的结果。我通常用5-10年的数据来估计这个长期水平。

一个实用的模型是指数衰减

def tenor_extrap(tenor, short_vol, long_vol, decay_rate):
    # 短期波动率 -> 长期波动率,指数衰减
    return long_vol + (short_vol - long_vol) * np.exp(-decay_rate * tenor)

这个模型的好处是:参数少(就三个),而且保证了波动率在长期趋于稳定。我在做利率期权时,经常用这个模型来外推10年以上的期限。

核心要点:外推不是瞎猜,而是基于市场微观结构的合理假设。深度虚值期权反映尾部风险,长期期权反映均值回归。理解这些,你才能做出靠谱的预测。

实战:构建完整的波动率曲面

好了,理论说完了,咱们来点实际的。下面是一个完整的曲面构建流程:

import numpy as np
from scipy.interpolate import RectBivariateSpline

class VolSurface:
    def __init__(self, strikes, tenors, vols, forward):
        self.strikes = np.array(strikes)
        self.tenors = np.array(tenors)
        self.vols = np.array(vols)  # 二维矩阵
        self.forward = forward
        
        # 在log-moneyness空间构建样条
        log_moneyness = np.log(self.strikes / self.forward)
        self.interp = RectBivariateSpline(
            log_moneyness, self.tenors, self.vols,
            kx=3, ky=3, s=0
        )
    
    def get_vol(self, strike, tenor):
        # 内插
        log_m = np.log(strike / self.forward)
        if (self.strikes.min() <= strike <= self.strikes.max() and
            self.tenors.min() <= tenor <= self.tenors.max()):
            return self.interp(log_m, tenor)[0][0]
        
        # 外推:用SVI风格
        # 这里简化处理,实际项目中需要更复杂的逻辑
        return self._extrapolate(strike, tenor)
    
    def _extrapolate(self, strike, tenor):
        # 边界处理
        if tenor < self.tenors.min():
            # 短期外推:波动率上升
            short_vol = self.interp(log_m, self.tenors.min())[0][0]
            return short_vol * 1.1  # 简单加10%
        elif tenor > self.tenors.max():
            # 长期外推:趋于常数
            long_vol = self.interp(log_m, self.tenors.max())[0][0]
            return long_vol * 0.95  # 简单减5%
        # 行权价外推类似...
        return self.interp(log_m, tenor)[0][0]

这个类虽然简单,但已经包含了内插和外推的核心逻辑。实际生产环境中,你还需要加入:

  • 波动率上下限保护
  • 不同资产类别的特殊处理(比如外汇的波动率微笑更对称)
  • 日历套利和蝶式套利的检查

避坑指南

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

  • 不要用多项式外推:高次多项式在边界外会剧烈振荡,我曾经用5次多项式外推,结果波动率在深度虚值区域变成了负数。
  • 注意套利条件:外推后的曲面必须满足无套利条件。比如,不能出现日历套利(短期波动率高于长期太多)或蝶式套利(波动率微笑出现凹陷)。
  • 定期验证:我每个月都会用市场数据回测外推效果。如果发现系统性的偏差,及时调整参数。
一个小技巧:在做外推时,可以同时用多个方法(常数、线性、SVI),然后取加权平均。权重根据距离边界的远近动态调整——离边界越近,常数外推的权重越高;离边界越远,SVI的权重越高。这样既保证了稳定性,又保留了灵活性。

好了,关于曲面外推与内插,就聊这么多。记住一句话:内插是技术,外推是艺术。技术可以学,艺术需要练。多动手、多回测,你也能做出漂亮的波动率曲面。

波动率曲面外推与内插知识体系 波动率曲面构建 内插方法 线性内插 三次样条内插 双线性/双三次内插 外推方法 常数外推 线性外推 SVI参数化外推 应用场景 极端行权价预测 极端期限预测 奇异期权定价 注意事项:无套利条件检查 | 波动率上下限保护 | 定期回测验证

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