14. SABR校准实战:使用市场数据校准SABR参数
好了,终于到了动手环节。
前面几章我们把SABR模型的理论、偏微分方程、近似公式都过了一遍。说实话,理论再漂亮,落不了地也是白搭。今天我们就来点实在的——拿真实的市场数据,把SABR的四个参数(α, β, ρ, ν)给校准出来。
14.1 校准的本质:我们在干什么?
校准,说白了就是找一组参数,让模型算出来的期权价格,跟市场上实际交易的价格尽可能接近。
我刚开始做这个的时候,总觉得校准是个黑盒子。后来发现,它本质上就是一个优化问题:
目标函数:最小化模型价格与市场价格的误差平方和
决策变量:α, β, ρ, ν
约束条件:α > 0, 0 ≤ β ≤ 1, -1 < ρ < 1, ν > 0
嗯,这里要注意:β 参数比较特殊。它通常不参与校准,而是根据市场经验固定下来。我个人习惯把 β 设为 0.5,这是外汇期权市场的常见做法。如果你做的是利率衍生品,β=0.5 或者 β=1 都有人用。
14.2 数据准备:从市场拿到原始报价
先看看我们要处理的数据长什么样。假设我们从彭博或者路透拿到了欧元/美元期权的波动率曲面:
| 期限 | 10D Put | 25D Put | ATM | 25D Call | 10D Call |
|---|---|---|---|---|---|
| 1W | 12.5 | 11.2 | 10.0 | 10.8 | 12.0 |
| 1M | 13.0 | 11.5 | 10.2 | 11.0 | 12.3 |
| 3M | 13.8 | 12.0 | 10.5 | 11.5 | 13.0 |
| 6M | 14.5 | 12.5 | 10.8 | 12.0 | 13.5 |
| 1Y | 15.2 | 13.0 | 11.0 | 12.5 | 14.0 |
这些是隐含波动率,单位是百分比。10D Put 表示 Delta 为 10% 的看跌期权,以此类推。ATM 就是平值期权。
我在项目中遇到过一个问题:不同数据源给的报价格式不一样。有的给 Delta,有的给行权价。所以第一步,先把所有报价统一转换成行权价和波动率的对应关系。
14.3 核心逻辑:校准流程
校准的流程其实不复杂,我画了张图帮你理清思路:
你想想看,这个流程其实跟机器学习里的梯度下降很像。我们不断调整参数,让模型输出跟真实数据之间的差距越来越小。
14.4 代码实现:从零开始写校准器
下面是我写的一个校准函数。代码不长,但该有的都有了:
import numpy as np
from scipy.optimize import minimize
from scipy.stats import norm
def sabr_vol(alpha, beta, rho, nu, F, K, T):
"""
Hagan近似公式计算SABR隐含波动率
"""
if abs(F - K) < 1e-6:
# ATM情况
z = nu / alpha * (F * K) ** ((1 - beta) / 2) * np.log(F / K)
x_z = np.log((np.sqrt(1 - 2 * rho * z + z ** 2) + z - rho) / (1 - rho))
term1 = alpha / (F ** (1 - beta))
term2 = 1 + ((1 - beta) ** 2 / 24 * (np.log(F / K)) ** 2
+ (1 - beta) ** 4 / 1920 * (np.log(F / K)) ** 4)
term3 = 1 + ((1 - beta) ** 2 / 24 * alpha ** 2 / (F * K) ** (1 - beta)
+ rho * beta * nu * alpha / (4 * (F * K) ** ((1 - beta) / 2))
+ (2 - 3 * rho ** 2) / 24 * nu ** 2) * T
return term1 * term2 * term3
else:
z = nu / alpha * (F * K) ** ((1 - beta) / 2) * np.log(F / K)
x_z = np.log((np.sqrt(1 - 2 * rho * z + z ** 2) + z - rho) / (1 - rho))
term1 = alpha / ((F * K) ** ((1 - beta) / 2))
term2 = 1 + ((1 - beta) ** 2 / 24 * (np.log(F / K)) ** 2
+ (1 - beta) ** 4 / 1920 * (np.log(F / K)) ** 4)
term3 = z / x_z
term4 = 1 + ((1 - beta) ** 2 / 24 * alpha ** 2 / (F * K) ** (1 - beta)
+ rho * beta * nu * alpha / (4 * (F * K) ** ((1 - beta) / 2))
+ (2 - 3 * rho ** 2) / 24 * nu ** 2) * T
return term1 * term2 * term3 * term4
def calibrate_sabr(market_strikes, market_vols, F, T, beta=0.5):
"""
校准SABR参数
market_strikes: 行权价数组
market_vols: 市场隐含波动率数组
F: 远期价格
T: 期限(年)
beta: 固定beta参数
"""
def objective(params):
alpha, rho, nu = params
# 约束检查
if alpha <= 0 or abs(rho) >= 1 or nu <= 0:
return 1e10
model_vols = np.array([
sabr_vol(alpha, beta, rho, nu, F, K, T)
for K in market_strikes
])
# 均方误差
mse = np.mean((model_vols - market_vols) ** 2)
return mse
# 初始值设定
# alpha初始值:用ATM波动率估算
atm_idx = np.argmin(np.abs(market_strikes - F))
alpha_init = market_vols[atm_idx] * (F ** (1 - beta))
# 初始猜测
x0 = [alpha_init, 0.0, 0.3]
# 参数边界
bounds = [(1e-6, None), (-0.999, 0.999), (1e-6, None)]
result = minimize(objective, x0, method='L-BFGS-B',
bounds=bounds, options={'maxiter': 1000})
return result.x # [alpha, rho, nu]
# 使用示例
if __name__ == "__main__":
# 假设市场数据
strikes = np.array([1.05, 1.08, 1.10, 1.12, 1.15])
vols = np.array([0.125, 0.112, 0.100, 0.108, 0.120])
F = 1.10 # 远期汇率
T = 0.25 # 3个月
alpha, rho, nu = calibrate_sabr(strikes, vols, F, T)
print(f"校准结果: alpha={alpha:.4f}, rho={rho:.4f}, nu={nu:.4f}")
💡 个人经验:初始值的选择很关键。我踩过坑,初始值设得不好,优化器直接跑到局部最优去了。建议先用ATM波动率反推alpha的初始值,这样收敛会快很多。
14.5 避坑指南:校准中的常见问题
我曾经在一个项目里连续跑了三天校准,结果出来的参数怎么看怎么不对劲。后来发现是数据出了问题。这里总结几个常见坑:
- 数据质量问题:市场报价里偶尔会有异常值。比如某个深度虚值期权的波动率突然跳得很高。我建议先做一次数据清洗,把明显偏离的点剔除掉。
- 参数可识别性问题:有时候ρ和ν会互相"打架"。你想想看,这两个参数都影响波动率曲线的斜率和曲率。如果数据不够丰富,它们可能无法被唯一确定。解决办法是固定其中一个,或者加正则化项。
- 期限结构问题:不同期限的SABR参数应该分开校准。不要试图用一个参数集拟合整个曲面。我一般每个期限单独校准,然后看参数随期限的变化是否平滑。
⚠️ 重要提醒:校准完成后,一定要做回测检验。把校准得到的参数代回模型,重新计算波动率,跟市场数据对比。如果误差超过0.5个波动率点,说明校准可能有问题。
14.6 结果分析:校准得好不好?
校准完了,怎么判断结果好坏?我一般看三个指标:
- 均方根误差(RMSE):一般应该小于0.2个波动率点。如果大于0.5,说明模型可能不适合这个市场。
- 参数合理性:α应该在0.01-0.5之间,ρ通常在-0.5到0.5之间,ν在0.1-1.0之间。如果参数跑出这个范围,要警惕。
- 残差分布:把模型波动率减去市场波动率,画个散点图。残差应该随机分布在零轴附近,不能有明显的系统性偏差。
说实话,校准这件事,七分技术三分经验。代码谁都能写,但参数调得好不好,就看你对市场的理解深不深了。
公众号:蓝海资料掘金营,微信deep3321