14. 曲面平滑技术:核回归平滑、局部多项式平滑、平滑参数选择
好了,咱们接着聊。上一章我们把波动率曲面的骨架搭起来了——原始数据、插值、网格化。但说实话,那个曲面看起来还像个刺猬。为什么?因为市场报价本身就有噪音,买卖价差、交易摩擦、做市商的情绪波动,都会让曲面坑坑洼洼。
平滑,就是把这些毛刺去掉,让曲面回归到它该有的样子。我个人习惯把平滑看作是“给曲面做美容”——不是整容,是微调。今天咱们聊三种主流手法:核回归、局部多项式,以及那个让人又爱又恨的参数选择问题。
14.1 为什么需要平滑?
先看一个实际场景。我在做某个股指期权曲面时,发现ATM附近的隐含波动率突然跳了0.5个vol。查了半天,原来是某个大单成交导致的瞬时冲击。这种点,你如果不处理,后续的定价和风控全都会偏。
平滑要解决的核心问题有三个:
- 去噪:剔除市场微观结构带来的随机波动
- 保持形状:不能把真实的偏斜(skew)和期限结构给抹平了
- 稳定性:今天和明天的曲面不能因为一个报价的变动就剧烈抖动
核心原则:平滑不是越光滑越好,而是要在“拟合度”和“光滑度”之间找到平衡。过拟合会保留噪音,过平滑会丢失结构。
14.2 核回归平滑(Nadaraya-Watson Estimator)
核回归,说白了就是加权平均。每个点的值,由它周围点的加权平均来估计。权重由核函数决定——距离越近,权重越大。
公式长这样:
σ̂(K, T) = Σ wᵢ · σᵢ / Σ wᵢ
其中 wᵢ = K( (K - Kᵢ)/h_K ) · K( (T - Tᵢ)/h_T )
这里K(·)是核函数,h是带宽参数。我常用的核函数有:
- 高斯核:最常用,平滑效果好,但计算稍慢
- Epanechnikov核:理论上最优,计算快,我比较偏爱
- 均匀核:简单粗暴,但边界处容易有跳跃
代码实现其实不复杂:
import numpy as np
from scipy.spatial.distance import cdist
def kernel_regression(X, y, X_pred, h=0.1, kernel='gaussian'):
"""
X: (n, 2) 形状,列是 [K, T]
y: (n,) 隐含波动率
X_pred: (m, 2) 待预测点
h: 带宽
"""
dists = cdist(X_pred, X, metric='euclidean')
if kernel == 'gaussian':
weights = np.exp(-0.5 * (dists / h)**2)
elif kernel == 'epanechnikov':
weights = np.maximum(0, 1 - (dists / h)**2)
# 加权平均
y_pred = np.sum(weights * y, axis=1) / np.sum(weights, axis=1)
return y_pred
我的经验:核回归在数据密集区表现很好,但在边界处(比如深度虚值或远月合约)容易出问题。因为边界点的邻居不对称,估计会有偏。我曾经在远月合约上吃过这个亏,后来加了边界校正才解决。
14.3 局部多项式平滑(Local Polynomial Regression)
核回归有个问题——它本质上是用常数来拟合局部区域。如果曲面曲率比较大,常数拟合就不够用了。局部多项式就是升级版:在每个局部,用一个低阶多项式来拟合。
我个人最常用的是局部线性回归(局部多项式的一阶形式)。它在每个预测点附近,求解一个加权最小二乘问题:
min Σ wᵢ · (σᵢ - (β₀ + β₁·(K-K₀) + β₂·(T-T₀)))²
代码实现:
def local_linear_regression(X, y, X_pred, h=0.1):
y_pred = np.zeros(len(X_pred))
for i, x0 in enumerate(X_pred):
# 计算权重
dists = np.linalg.norm(X - x0, axis=1)
weights = np.exp(-0.5 * (dists / h)**2)
W = np.diag(weights)
# 构建设计矩阵(包含截距项和线性项)
X_design = np.c_[np.ones(len(X)), X - x0]
# 加权最小二乘
beta = np.linalg.inv(X_design.T @ W @ X_design) @ (X_design.T @ W @ y)
y_pred[i] = beta[0] # 只取截距项
return y_pred
局部多项式比核回归好在哪?
- 边界表现更好:线性项能自动校正边界偏差
- 导数估计:β₁和β₂直接给出了曲面的偏导数,这对greeks计算很有用
- 适应性更强:能处理曲率变化大的区域
注意:局部多项式的阶数不是越高越好。二阶以上容易过拟合,而且计算量会暴增。我一般只用一阶(局部线性)或二阶(局部二次),再高就不推荐了。
14.4 平滑参数选择——那个让人头疼的问题
不管是核回归还是局部多项式,都绕不开一个参数:带宽h。h选大了,曲面太平滑,细节全丢;h选小了,曲面还是毛刺一堆。
我常用的方法有几种:
| 方法 | 原理 | 优缺点 |
|---|---|---|
| 交叉验证(CV) | 留一法或k折,最小化预测误差 | 客观,但计算量大 |
| 广义交叉验证(GCV) | CV的近似,计算更快 | 适合大样本,但可能过平滑 |
| 经验法则(Rule of Thumb) | h = c · σ · n^(-1/5) | 简单快速,但不够精确 |
| 目视检查 | 画几个不同h的曲面,凭经验选 | 主观,但实战中很有效 |
交叉验证的代码:
from sklearn.model_selection import KFold
def cv_bandwidth_selection(X, y, h_candidates):
kf = KFold(n_splits=5, shuffle=True)
cv_errors = []
for h in h_candidates:
errors = []
for train_idx, val_idx in kf.split(X):
X_train, y_train = X[train_idx], y[train_idx]
X_val, y_val = X[val_idx], y[val_idx]
y_pred = kernel_regression(X_train, y_train, X_val, h=h)
errors.append(np.mean((y_val - y_pred)**2))
cv_errors.append(np.mean(errors))
best_h = h_candidates[np.argmin(cv_errors)]
return best_h, cv_errors
我的习惯:先用经验法则算一个初始值,然后在它周围画一个网格做交叉验证。这样既不会盲目搜索,也不会完全依赖经验。另外,我建议对K和T维度分别设置带宽——因为它们的量纲不同,一个带宽很难同时适配两个维度。
14.5 实战中的避坑指南
这些年做曲面平滑,踩过的坑不少。挑几个典型的说说:
- 带宽对数据密度敏感:ATM附近数据密集,带宽可以小一点;深度虚值数据稀疏,带宽要大一些。我曾经用全局带宽吃过亏,后来改成了自适应带宽。
- 不要忽视异常值:平滑前最好先做一次异常值检测。我一般用3σ原则或MAD(中位数绝对偏差)来剔除明显的错误报价。
- 平滑后的曲面要满足无套利约束:平滑完一定要检查——有没有出现波动率为负的情况?有没有蝶式套利机会?这些在平滑过程中很容易被忽略。
- 计算效率:如果你要做实时曲面更新,核回归和局部多项式都要考虑计算效率。我一般用KD树来加速近邻搜索,能快一个数量级。
14.6 本章知识体系
下面这张图总结了曲面平滑的核心流程和选择逻辑:
嗯,曲面平滑这块内容不少,但核心就三件事:选方法、调参数、验结果。核回归适合快速原型,局部多项式适合生产环境。参数选择没有银弹,交叉验证加经验判断是我目前觉得最靠谱的组合。
记住一点:平滑是为了让曲面更真实,而不是更漂亮。如果平滑后的曲面出现了新的套利机会,那说明你平滑过头了。
本章小结:核回归平滑适合数据密集区域,局部多项式平滑在边界和曲率变化大的区域表现更好。平滑参数选择推荐交叉验证结合经验法则。实战中要注意自适应带宽、异常值处理和无套利约束检查。
公众号:蓝海资料掘金营,微信deep3321