实战案例:商品期货期权曲面异常检测
各位做量化交易的朋友,今天咱们来聊一个实战性很强的话题——商品期货期权曲面的异常检测。
说实话,我刚入行那会儿,对波动率曲面这玩意儿是又爱又恨。爱的是它确实能反映市场情绪,恨的是它经常莫名其妙地出现一些"鬼影"。有一次我在做玉米期权的套利策略,突然发现曲面上一块区域的价格明显偏离,差点让我亏掉一个月的利润。从那以后,我就把异常检测当成了每天的必修课。
为什么要检测曲面异常?
你想想看,商品期货期权市场和股票期权不太一样。商品市场受季节性、天气、政策等因素影响更大,曲面更容易出现"毛刺"。这些异常点如果不处理,会带来三个问题:
- 定价失真:异常点会让你的定价模型跑偏,该买的卖贵了,该卖的买便宜了
- 套利陷阱:看起来是套利机会,其实是数据错误,一进去就被套
- 风控失效:VaR计算依赖曲面数据,异常点会让风险指标完全失真
核心观点:异常检测不是可选项,而是期权交易的"安全带"。不系安全带也能开车,但出了事就晚了。
异常检测的三种方法
我个人习惯把异常检测分成三个层次,从简单到复杂,逐步排查。
1. 统计阈值法
这是最基础的方法。说白了,就是看某个点的隐含波动率偏离均值多少个标准差。
import numpy as np
import pandas as pd
def detect_outliers_by_zscore(surface_data, threshold=3):
"""
基于Z-score的异常检测
surface_data: DataFrame, 行是到期日,列是执行价
"""
mean = np.nanmean(surface_data.values)
std = np.nanstd(surface_data.values)
z_scores = (surface_data - mean) / std
outliers = np.abs(z_scores) > threshold
return outliers, z_scores
我在项目中遇到过一个问题:直接用全局均值做Z-score,结果把深度虚值期权全标成了异常。为什么?因为深度虚值期权的波动率天然就高,这不是异常,是市场特性。所以后来我改成了按行(同一到期日)或按列(同一执行价)分别计算。
2. 局部异常因子(LOF)法
这个方法更聪明一些。它不看全局,而是看每个点周围邻居的密度。如果一个点周围的密度明显低于邻居,那它就很可疑。
from sklearn.neighbors import LocalOutlierFactor
def detect_outliers_lof(surface_data, n_neighbors=20):
"""
使用LOF检测曲面异常点
"""
# 将曲面展平为二维数组
X = surface_data.values.reshape(-1, 1)
# 创建LOF模型
lof = LocalOutlierFactor(n_neighbors=n_neighbors, contamination=0.05)
y_pred = lof.fit_predict(X)
# 重塑回曲面形状
outlier_mask = (y_pred == -1).reshape(surface_data.shape)
return outlier_mask, lof.negative_outlier_factor_
小技巧:n_neighbors参数别设太小。我一般设20-30,太小了容易把正常点误判为异常,太大了又可能漏掉真正的异常。
3. 模型残差法
这是我最喜欢的方法。先用一个平滑模型拟合曲面,然后看每个点的残差。残差大的就是异常点。
from scipy.interpolate import SmoothBivariateSpline
import numpy as np
def detect_outliers_by_residual(moneyness, time_to_expiry, implied_vol, smoothing=0.1):
"""
基于模型残差的异常检测
"""
# 用平滑样条拟合曲面
spline = SmoothBivariateSpline(
moneyness, time_to_expiry, implied_vol,
s=smoothing * len(moneyness)
)
# 计算拟合值
fitted_vol = spline.ev(moneyness, time_to_expiry)
# 计算残差
residuals = implied_vol - fitted_vol
# 残差超过3倍标准差的视为异常
threshold = 3 * np.std(residuals)
outliers = np.abs(residuals) > threshold
return outliers, residuals, fitted_vol
嗯,这里要注意:smoothing参数很关键。设太大,曲面太平滑,真实的市场结构都被抹掉了;设太小,又过度拟合,异常点反而被"解释"掉了。我一般先用交叉验证选一个合适的值。
实战案例:豆粕期权曲面
咱们拿豆粕期权来练练手。豆粕这个品种很有意思,受大豆进口、猪周期、饲料需求等多重因素影响,曲面经常出现"怪相"。
我记得有一次,2022年3月,豆粕期权曲面突然出现了一个"尖刺"——某个执行价的隐含波动率比周围高了将近10个百分点。当时很多人以为是套利机会,但我用模型残差法一查,发现这个点明显偏离了平滑曲面。后来一查,原来是某个大机构在大量买入那个执行价的看涨期权,造成了暂时的流动性失衡。
下面是我当时用的检测流程:
- 数据清洗:剔除交易量过小的合约(日交易量<100手)
- 曲面构建:用SVI模型拟合原始数据
- 异常检测:三种方法同时跑,取交集
- 人工复核:对标记的异常点,查看当时的市场新闻
警告:千万别完全依赖算法。有一次算法把真实的市场冲击误判为异常,我差点错过了一个重要的交易信号。算法是工具,不是上帝。
异常处理策略
检测出异常之后怎么办?我一般分三种情况处理:
| 异常类型 | 处理方式 | 适用场景 |
|---|---|---|
| 数据错误 | 直接剔除,用插值填补 | 明显的数据录入错误、报价异常 |
| 流动性异常 | 标记但不剔除,降低权重 | 大单冲击、流动性枯竭 |
| 市场结构变化 | 保留并分析原因 | 政策变化、供需突变 |
我曾经犯过一个错误:把所有异常点都直接剔除。结果曲面变得过于平滑,完全失去了市场的真实波动信息。后来我学乖了,异常点也要分门别类处理。
可视化检测结果
光看数字不够直观,我习惯把检测结果画出来。下面是一个简单的可视化代码:
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
def plot_surface_with_outliers(moneyness, tte, iv, outlier_mask):
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
# 正常点用蓝色
ax.scatter(moneyness[~outlier_mask], tte[~outlier_mask],
iv[~outlier_mask], c='blue', alpha=0.6, label='Normal')
# 异常点用红色
ax.scatter(moneyness[outlier_mask], tte[outlier_mask],
iv[outlier_mask], c='red', s=100, marker='x', label='Outlier')
ax.set_xlabel('Moneyness')
ax.set_ylabel('Time to Expiry')
ax.set_zlabel('Implied Vol')
ax.legend()
plt.show()
说实话,三维图看起来挺酷的,但实际工作中我更喜欢用热力图。热力图能更清楚地显示异常点的位置和程度。
知识体系总览
下面这张图是我自己总结的异常检测知识体系,你可以参考一下:
这张图把整个知识体系串起来了。从三种检测方法到处理策略,再到核心原则,一目了然。
避坑指南
最后,分享几个我踩过的坑:
- 别只看一个指标:我曾经只靠Z-score检测,结果把正常的波动率微笑当成了异常。现在我会三种方法交叉验证。
- 注意到期日效应:临近到期的期权,波动率会剧烈波动,这不一定是异常。我一般会剔除到期前3天的数据。
- 别忽略交易量:交易量小的合约,报价本身就不靠谱。我习惯先按交易量过滤,再做异常检测。
我的习惯:每天开盘前跑一遍异常检测,把结果存下来。时间长了,你就能摸清市场的"脾气",哪些异常是噪音,哪些是信号,心里就有数了。
好了,关于商品期货期权曲面的异常检测,今天就聊到这儿。记住一句话:异常检测不是目的,目的是让你更清楚地看到市场的真实面貌。工具再好,也得靠人来判断。