8. 波动率曲面平滑:去噪与平滑技术(核平滑、局部回归)
各位同学,咱们今天聊点实在的。波动率曲面这东西,你从市场上拿到的原始数据,说白了就是一堆带毛刺的散点。直接拿去做交易?我劝你别这么干。我见过太多人,拿着带噪声的曲面去套利,结果被市场狠狠教育了一顿。
为什么要平滑?因为市场微观结构里藏着各种噪声——买卖价差、数据延迟、大单冲击。这些噪声会让曲面变得坑坑洼洼。你想想看,一个坑洼的曲面,你去做套利,那不是找坑跳吗?
今天我就把两种最实用的平滑方法掰开揉碎了讲给你听:核平滑和局部回归。这两种方法,我在实盘里用了好几年,踩过的坑比你们吃过的盐还多。
8.1 噪声的来源:为什么曲面需要去噪?
先说说噪声从哪来。我总结了三类:
- 报价噪声:买卖价差导致的报价波动。比如某期权最后一笔成交在2.10,下一笔直接跳到2.15,中间那0.05就是噪声。
- 流动性噪声:深度虚值或深度实值期权,成交稀疏,报价基本靠做市商瞎猜。
- 事件噪声:财报、宏观数据发布瞬间,曲面会剧烈抖动。
嗯,这里要注意:不是所有波动都是噪声。真正的市场信号和噪声混在一起,你得学会分辨。我个人习惯是,先看数据的时间戳,把那些明显异常的点标记出来,再做平滑。
我曾经犯过一个错误:把到期日附近的曲面也做了强平滑。结果呢?到期日附近的波动率结构被完全抹平了,套利信号全没了。后来我学乖了——到期日附近要保留更多细节。
8.2 核平滑:简单粗暴但有效
核平滑,说白了就是加权平均。你想想看,一个点周围的邻居,离得越近,权重越大。这就是核函数干的事。
常用的核函数有几种:
| 核函数 | 公式 | 特点 |
|---|---|---|
| 高斯核 | K(u) = (1/√(2π)) * exp(-u²/2) | 平滑效果好,但计算稍慢 |
| Epanechnikov核 | K(u) = 0.75*(1-u²) for |u|≤1 | 计算快,边界表现好 |
| 均匀核 | K(u) = 0.5 for |u|≤1 | 最简单,但平滑效果一般 |
我个人习惯用高斯核。为什么?因为它的权重衰减是连续的,不会出现边界处突然截断的情况。但如果你追求速度,Epanechnikov核是个不错的选择。
来看看代码怎么实现:
import numpy as np
from scipy.stats import norm
def gaussian_kernel_smooth(x, y, x_new, bandwidth=0.1):
"""
高斯核平滑
x: 原始数据点(比如行权价)
y: 原始波动率值
x_new: 需要预测的新点
bandwidth: 带宽,控制平滑程度
"""
y_smooth = np.zeros_like(x_new)
for i, xi in enumerate(x_new):
# 计算每个原始点到新点的距离
distances = np.abs(x - xi)
# 高斯核权重
weights = norm.pdf(distances / bandwidth)
# 加权平均
y_smooth[i] = np.sum(weights * y) / np.sum(weights)
return y_smooth
# 示例:平滑一条波动率微笑曲线
strikes = np.array([90, 95, 100, 105, 110])
vols = np.array([0.25, 0.22, 0.20, 0.23, 0.28])
smooth_strikes = np.linspace(90, 110, 50)
smooth_vols = gaussian_kernel_smooth(strikes, vols, smooth_strikes, bandwidth=5)
带宽的选择很关键。带宽太小,平滑效果不够;带宽太大,会把真实结构也抹掉。我一般用交叉验证来选带宽,或者直接用Silverman's rule of thumb作为起点。
8.3 局部回归:更聪明的平滑方式
核平滑有个问题:它在边界处表现不好。你想想看,曲面边缘的数据点少,加权平均的结果容易偏。这时候,局部回归就派上用场了。
局部回归(LOESS)的思路是:在每个点附近,用局部多项式去拟合。说白了,就是分段做回归。这样做的好处是:
- 边界处表现更好
- 能捕捉局部曲率变化
- 对异常点更鲁棒
我在项目中遇到过这样的情况:用核平滑处理深度虚值期权时,曲面尾部总是翘得太高。换成局部回归后,尾部自然多了。
from sklearn.linear_model import Ridge
import numpy as np
def local_regression_smooth(x, y, x_new, bandwidth=0.3, degree=2):
"""
局部回归平滑
x: 原始数据点
y: 原始波动率值
x_new: 需要预测的新点
bandwidth: 带宽比例(占数据范围的百分比)
degree: 多项式次数
"""
y_smooth = np.zeros_like(x_new)
x_range = x.max() - x.min()
for i, xi in enumerate(x_new):
# 计算距离
distances = np.abs(x - xi)
# 使用三立方核(tricube kernel)
u = distances / (bandwidth * x_range)
weights = np.where(u <= 1, (1 - u**3)**3, 0)
# 只选择权重大于0的点
mask = weights > 0
if np.sum(mask) < degree + 1:
# 数据点太少,用全局平均
y_smooth[i] = np.mean(y)
continue
# 构建局部多项式特征
X_local = np.vander(x[mask] - xi, degree + 1, increasing=True)
# 加权最小二乘
W = np.diag(weights[mask])
beta = np.linalg.inv(X_local.T @ W @ X_local) @ (X_local.T @ W @ y[mask])
y_smooth[i] = beta[0] # 常数项就是预测值
return y_smooth
局部回归的带宽和多项式次数需要配合。次数越高,对局部细节的捕捉能力越强,但也更容易过拟合。我一般用二次多项式,带宽选0.2-0.4之间。
8.4 两种方法的对比与选择
说了这么多,到底该用哪个?我直接给你个决策指南:
| 场景 | 推荐方法 | 理由 |
|---|---|---|
| 数据量大,追求速度 | 核平滑 | 计算简单,O(n)复杂度 |
| 边界处需要准确 | 局部回归 | 边界表现更好 |
| 曲面有尖峰结构 | 局部回归(低带宽) | 能保留局部特征 |
| 快速原型验证 | 核平滑 | 实现简单,调参容易 |
说白了,没有绝对的好坏。我自己的做法是:先用核平滑快速看看整体形状,再用局部回归做精细调整。两个方法配合着用,效果最好。
8.5 实战中的注意事项
最后,我把自己踩过的坑总结一下:
- 带宽不是越小越好:我见过有人把带宽设到0.01,结果平滑后的曲面比原始数据还毛糙。记住,平滑的目的是去噪,不是拟合。
- 注意到期日效应:不同到期日的波动率结构差异很大。我建议按到期日分组,分别做平滑,再插值到整个曲面。
- 异常点要预处理:有些报价明显是错的(比如波动率为负),这些点要先剔除,再做平滑。否则一个异常点能带偏一片。
- 验证平滑效果:平滑后一定要回测。把平滑后的曲面代入定价模型,看看套利信号是否合理。如果信号变多了,说明平滑过度了。
平滑不是万能的。如果原始数据质量太差(比如大量缺失值),再好的平滑方法也救不了。我建议先做数据清洗,再做平滑。顺序不能乱。
好了,关于核平滑和局部回归,我就讲这么多。这两种方法,你回去一定要动手试试。光看代码没用,得自己调参数、看效果,才能真正理解。记住,平滑的目的是让曲面更干净,而不是更复杂。