第十四章:波动率曲面套利:静态套利条件、动态套利条件、套利检测与修正
做波动率曲面分析,最刺激也最头疼的事情是什么?
我个人觉得,就是套利检测。
你辛辛苦苦从市场上扒下来一堆期权报价,插值拟合出一个漂漂亮亮的曲面。结果一跑套利检测,满屏的红叉。嗯,这时候你就知道,市场不是教科书,报价里全是坑。
今天我们就来聊聊,怎么把这些坑找出来,再想办法填上。
14.1 什么是波动率曲面套利?
说白了,套利就是「无风险赚钱」的机会。
在波动率曲面上,如果两个期权合约的价格关系不合理,理论上你可以通过买卖组合锁定利润。但现实中,这种机会转瞬即逝。我们做套利检测,更多是为了清洗数据、保证模型输入的质量。
套利分两类:
- 静态套利:只看当前时刻的截面数据,不依赖时间变化
- 动态套利:考虑时间维度上的价格演变关系
我在项目中遇到过最典型的情况:某天下午三点,外汇期权市场突然出现一组蝶式价差为负的报价。当时我第一反应是数据错了,但反复确认后发现,确实是市场流动性枯竭导致的畸形报价。这种数据如果不处理,后续的定价模型全得崩。
14.2 静态套利条件
静态套利,你想想看,就是检查同一到期日下,不同行权价的期权价格是否合理。
核心条件有三个:
14.2.1 无蝶式套利条件
这是最基础的。对于三个连续行权价 K₁ < K₂ < K₃,看涨期权的价格必须满足:
C(K₂) ≤ (K₃ - K₂)/(K₃ - K₁) * C(K₁) + (K₂ - K₁)/(K₃ - K₁) * C(K₃)
如果违反了这个条件,就存在蝶式套利机会。
我曾经在整理沪深300ETF期权数据时,发现某天的虚值期权报价居然比平值还贵。这明显不合理——行权价越高,看涨期权应该越便宜才对。后来查出来是交易员手误输错了价格。
14.2.2 无垂直价差套利条件
看涨期权的价格必须随行权价单调递减:
K₁ < K₂ ⇒ C(K₁) ≥ C(K₂)
看跌期权则相反:
K₁ < K₂ ⇒ P(K₁) ≤ P(K₂)
这个条件简单粗暴,但非常有效。我一般会把它作为第一道过滤条件。
14.2.3 凸性条件
波动率曲面在行权价方向上应该是凸的。用数学表达就是二阶导数非负:
∂²σ/∂K² ≥ 0
嗯,这里要注意,实际数据中凸性条件经常被违反,尤其是在深度实值和深度虚值区域。
14.3 动态套利条件
动态套利涉及不同到期日之间的价格关系。说白了,就是检查时间维度上的合理性。
14.3.1 日历价差套利条件
对于相同行权价、不同到期日的看涨期权,到期日越远,价格应该越贵:
T₁ < T₂ ⇒ C(T₁) ≤ C(T₂)
如果违反了这个条件,就可以通过买入远月、卖出近月来套利。
我记得有一次做原油期权分析,发现近月合约因为某个突发事件被炒得比远月还贵。这种时候,日历价差套利条件就被触发了。但要注意,这不一定代表市场错了——可能是市场在定价短期的高波动风险。
14.3.2 无偏预期条件
远期波动率应该等于未来即期波动率的期望值。这个条件在理论上是成立的,但实际中很少严格满足。我一般把它作为参考,而不是硬约束。
14.4 套利检测的实现
讲完理论,我们来看看代码怎么写。
import numpy as np
from scipy.interpolate import CubicSpline
def detect_butterfly_arbitrage(strikes, prices):
"""
检测蝶式套利
strikes: 行权价数组
prices: 对应期权价格数组
"""
violations = []
for i in range(1, len(strikes) - 1):
K1, K2, K3 = strikes[i-1], strikes[i], strikes[i+1]
C1, C2, C3 = prices[i-1], prices[i], prices[i+1]
# 计算理论价格上限
w2 = (K3 - K2) / (K3 - K1)
w1 = (K2 - K1) / (K3 - K1)
upper_bound = w1 * C1 + w2 * C3
if C2 > upper_bound + 1e-8: # 加一个小阈值避免数值误差
violations.append((K2, C2, upper_bound))
return violations
def detect_calendar_arbitrage(maturities, prices):
"""
检测日历价差套利
maturities: 到期时间数组
prices: 对应期权价格数组
"""
violations = []
for i in range(len(maturities) - 1):
if prices[i] > prices[i+1] + 1e-8:
violations.append((maturities[i], maturities[i+1],
prices[i], prices[i+1]))
return violations
这段代码看起来简单,但实际用的时候有几个坑:
- 阈值设置很关键。设太小了全是误报,设太大了漏报严重。我一般用1e-8作为基准,然后根据数据量级调整
- 插值后的曲面更容易出现套利。因为插值方法本身不保证无套利性质
14.5 套利修正方法
检测出套利之后怎么办?不能直接删数据吧?
我常用的修正方法有几种:
| 方法 | 原理 | 适用场景 |
|---|---|---|
| 价格调整法 | 将违规价格调整到边界值 | 轻微违规,数据量少 |
| 局部平滑法 | 用周围数据加权平均替代 | 孤立异常点 |
| 全局优化法 | 在无套利约束下重新拟合曲面 | 大面积违规,需要整体修正 |
我个人最推荐的是全局优化法。虽然计算量大一点,但能保证整个曲面的一致性。
def arbitrage_free_correction(strikes, prices, method='global'):
"""
无套利修正
"""
if method == 'global':
# 使用凸优化方法,在无套利约束下最小化与原始数据的偏差
from scipy.optimize import minimize
def objective(p):
return np.sum((p - prices) ** 2)
# 约束条件:凸性 + 单调性
constraints = []
for i in range(1, len(strikes) - 1):
def con_butterfly(p, i=i):
K1, K2, K3 = strikes[i-1], strikes[i], strikes[i+1]
w2 = (K3 - K2) / (K3 - K1)
w1 = (K2 - K1) / (K3 - K1)
return w1 * p[i-1] + w2 * p[i+1] - p[i]
constraints.append({'type': 'ineq', 'fun': con_butterfly})
# 单调性约束
for i in range(len(strikes) - 1):
def con_mono(p, i=i):
return p[i] - p[i+1]
constraints.append({'type': 'ineq', 'fun': con_mono})
result = minimize(objective, prices, constraints=constraints)
return result.x
elif method == 'local':
# 局部调整法
corrected = prices.copy()
for i in range(1, len(strikes) - 1):
K1, K2, K3 = strikes[i-1], strikes[i], strikes[i+1]
w2 = (K3 - K2) / (K3 - K1)
w1 = (K2 - K1) / (K3 - K1)
upper = w1 * corrected[i-1] + w2 * corrected[i+1]
if corrected[i] > upper:
corrected[i] = upper - 1e-8
return corrected
14.6 知识体系总览
下面这张图,是我自己总结的套利检测与修正的完整流程:
做套利检测这件事,说白了就是跟市场数据「斗智斗勇」。你永远不知道下一个异常点会出现在哪里。但只要你把检测逻辑写扎实了,修正方法备齐了,就能保证你的波动率曲面是干净、可用的。
嗯,今天就聊到这里。记住一句话:无套利是理想,有套利是常态。关键是你怎么处理它。
公众号:蓝海资料掘金营,微信deep3321