26. 极端行情处理:跳跃、负波动率、流动性枯竭时的特殊处理
做波动率曲面这么多年,我最怕的不是模型不收敛,而是市场突然发疯。2020年3月、2015年8月、还有瑞郎黑天鹅事件——这些日子我都在交易台前盯着屏幕,亲眼看着波动率曲面变成一堆废纸。
今天聊的,就是这些极端情况怎么处理。说白了,就是当教科书上的方法全失效时,我们还能靠什么活下去。
26.1 跳跃行情:曲面瞬间撕裂
先说说跳跃。市场突然跳空,波动率曲面会怎样?
我举个例子。假设标的价格从100跳到95,跌幅5%。这时候平值期权变成了深度实值,原来的虚值期权突然变成了平值。整个曲面的结构在一秒钟内完全变了。
常规的SVI、SSVI这些参数化模型,在这种跳跃面前基本是废的。为什么?因为它们假设市场是连续的,参数变化是平滑的。跳跃一来,参数直接飞出可行域。
我的经验法则:跳跃发生后,先别急着拟合曲面。等15-30分钟,让市场消化一下,再用滚动窗口的方式重新校准参数。
具体做法是这样的:
import numpy as np
from scipy.optimize import minimize
def robust_svi_fit(strikes, ivs, k0, sigma, rho, eta, gamma):
"""
带跳跃检测的SVI拟合
如果检测到跳跃,使用L1正则化约束参数变化
"""
def detect_jump(ivs_new, ivs_old, threshold=0.15):
# 检测隐含波动率是否发生跳跃
jump_idx = np.abs(ivs_new - ivs_old) > threshold
return jump_idx
def objective(params, strikes, ivs, ivs_old, lambda_reg=0.1):
k0, sigma, rho, eta, gamma = params
# SVI公式
x = np.log(strikes / k0)
svi_iv = sigma + eta * (rho * x + np.sqrt(x**2 + gamma**2))
# 如果检测到跳跃,加入正则化项
jump_idx = detect_jump(ivs, ivs_old)
if np.any(jump_idx):
reg_term = lambda_reg * np.sum((params - params_old)**2)
else:
reg_term = 0
return np.mean((svi_iv - ivs)**2) + reg_term
# 保存上一期参数用于正则化
params_old = np.array([k0, sigma, rho, eta, gamma])
result = minimize(objective, params_old,
args=(strikes, ivs, ivs_old),
method='L-BFGS-B',
bounds=[(0.8, 1.2), (0.01, 0.5), (-1, 1),
(0.01, 0.5), (0.01, 0.5)])
return result.x
避坑指南:我曾经在2015年8月24日那天,用常规SVI拟合A股波动率曲面,结果参数rho直接跑到-1.2去了。后来加了跳跃检测和正则化,才把曲面稳住。记住,极端行情下,参数约束比拟合精度更重要。
26.2 负波动率:不可能的事情发生了
负波动率?听起来像天方夜谭。但2020年4月WTI原油期货跌到负值时,波动率曲面确实出现了负值区域。
为什么会这样?因为当标的价格为负时,对数收益率没法算了。ln(-1)?数学上不存在。这时候传统的Black-Scholes框架直接崩塌。
我的处理方式分三步:
- 检测负值区域:扫描整个曲面,找出所有隐含波动率为负或无法计算的网格点
- 局部替换模型:在负值区域改用Bachelier模型(正态模型)
- 边界平滑:在正负区域交界处做三次样条插值,保证连续性
def handle_negative_vol(strikes, ivs, spot, method='bachelier'):
"""
处理负波动率区域
使用Bachelier模型替代Black-Scholes
"""
# 检测负值或无效区域
valid_mask = (ivs > 0) & (strikes > 0)
if np.all(valid_mask):
return ivs # 没有负波动率,直接返回
# 对无效区域使用Bachelier模型
ivs_corrected = ivs.copy()
invalid_idx = ~valid_mask
for idx in np.where(invalid_idx)[0]:
k = strikes[idx]
# Bachelier模型:波动率 = 价格 / (sqrt(T) * dN(d))
# 这里简化处理,实际需要根据期权价格反推
ivs_corrected[idx] = bachelier_implied_vol(k, spot, 0.3)
# 边界平滑
from scipy.interpolate import CubicSpline
valid_strikes = strikes[valid_mask]
valid_ivs = ivs_corrected[valid_mask]
spline = CubicSpline(valid_strikes, valid_ivs,
bc_type='natural')
ivs_corrected[invalid_idx] = spline(strikes[invalid_idx])
return ivs_corrected
注意:负波动率不是真的波动率为负,而是模型失效导致的。千万别直接拿负值去算VaR或者定价,会出大问题。我见过有人把负波动率直接代入公式,结果算出来期权价格是负的——这明显不合理。
26.3 流动性枯竭:没有报价怎么办
流动性枯竭比跳跃更可怕。跳跃至少还有数据,流动性枯竭时,整个曲面都是空的。
我记得2020年3月,某些外汇期权在亚洲时段完全没成交,报价点差从0.5个vol直接跳到5个vol。这时候你拿什么去插值?
我的策略是:
- 时间维度借用:用前一天的曲面形态,加上当天的市场波动率指数调整
- 产品维度借用:用流动性好的产品(比如平值期权)推导流动性差的产品
- 相关性借用:用相关标的的波动率曲面做映射
def liquidity_crisis_interpolation(sparse_quotes, prev_surface,
vix_change, method='time_decay'):
"""
流动性枯竭时的曲面重建
"""
if method == 'time_decay':
# 用前一天曲面 + VIX变化调整
adjusted_surface = prev_surface * (1 + vix_change * 0.5)
# 用稀疏报价做局部修正
for strike, iv in sparse_quotes.items():
idx = np.argmin(np.abs(strikes - strike))
adjusted_surface[idx] = iv * 0.7 + adjusted_surface[idx] * 0.3
return adjusted_surface
elif method == 'cross_asset':
# 用相关标的曲面做映射
# 比如用SPX曲面映射到NDX曲面
beta = calculate_beta(spx_surface, ndx_surface, window=60)
return spx_surface * beta
我的习惯:流动性枯竭时,我一般会同时用三种方法算一遍,取中位数。如果三种方法结果差异超过20%,说明市场真的没法做了——这时候我建议直接暂停交易,等流动性恢复再说。
26.4 知识体系总览
下面这张图,是我这些年处理极端行情的经验总结。你可以把它当成一个决策树,遇到问题按图索骥就行。
26.5 实战中的避坑清单
最后,分享一个我这些年踩坑总结出来的清单。每次遇到极端行情,我都会过一遍:
| 场景 | 常见错误 | 正确做法 |
|---|---|---|
| 跳跃行情 | 立即用新数据拟合 | 等待15-30分钟,加正则化 |
| 负波动率 | 直接代入Black-Scholes | 改用Bachelier模型 |
| 流动性枯竭 | 用宽点差数据强行插值 | 借用时间/产品/相关性数据 |
| 参数飞出可行域 | 不加约束直接优化 | 设置硬边界,加正则化 |
| 模型不收敛 | 增加迭代次数硬算 | 换初始化参数或换模型 |
最后一句忠告:极端行情下,模型只是辅助工具。我见过太多人迷信模型,结果亏得血本无归。记住,当市场疯了的时候,你的判断比任何模型都重要。如果感觉不对,就停下来。活着,比什么都强。
公众号:蓝海资料掘金营,微信deep3321