第21章 多资产曲面:相关性矩阵在异常检测中的应用
说实话,做波动率曲面异常检测做到一定程度,你会发现一个有意思的现象——单看一个曲面的异常点,有时候真真假假分不清。但如果你把多个资产的曲面放在一起看,很多问题就一目了然了。这就是我今天想聊的:相关性矩阵在多资产曲面异常检测中的妙用。
为什么需要多资产视角?
我刚开始做量化那会儿,习惯盯着单个资产的波动率曲面看。有一次发现某个股指的曲面突然出现了一个很陡的skew,我第一反应是市场要出大事了。结果呢?后来发现是数据源的一个bug。从那以后我就养成了一个习惯:看异常点之前,先看看其他相关资产的表现。
你想想看,如果真的是市场层面的冲击,比如地缘政治事件、央行加息,那受影响的不可能只有一个资产。相关性矩阵在这里就像一面照妖镜——
- 系统性异常:多个资产同时出现类似形态的曲面扭曲
- 个体性异常:只有某个资产曲面异常,其他都正常
- 相关性断裂:原本高度相关的两个资产,突然相关性骤降
核心观点:单资产异常检测看的是「点」,多资产相关性看的是「面」。两者结合,才能把误报率降下来。
相关性矩阵的构建
我们先从最基础的开始。假设你有N个资产,每个资产我们提取一个关键特征——比如ATM波动率、skew斜率、或者整个曲面的主成分得分。然后计算两两之间的相关系数。
import numpy as np
import pandas as pd
from scipy.stats import spearmanr
# 假设我们有5个资产,每个资产提取了曲面特征向量
# 这里用PCA的第一主成分得分作为示例
assets = ['SPX', 'NDX', 'RUT', 'VIX', 'TNX']
n_assets = len(assets)
n_days = 252 # 一年交易日
# 模拟数据:每个资产每天一个曲面特征值
np.random.seed(42)
surface_features = np.random.randn(n_days, n_assets)
# 构建相关性矩阵
corr_matrix = np.corrcoef(surface_features.T)
# 转成DataFrame方便查看
corr_df = pd.DataFrame(corr_matrix, index=assets, columns=assets)
print(corr_df.round(3))
嗯,这里要注意一点:我建议用Spearman秩相关系数而不是Pearson。为什么?因为曲面特征往往不是正态分布的,而且Spearman对异常值更稳健。我在处理期权数据时吃过Pearson的亏——一个极端异常点就能把相关系数拉到0.8以上,看着吓人。
异常检测的三种模式
基于相关性矩阵,我个人习惯把异常分成三类来排查:
模式一:相关性结构突变
正常情况下,资产间的相关性是相对稳定的。如果某一天相关性矩阵发生了剧烈变化,那大概率是市场出了问题。
def detect_correlation_break(corr_window, corr_historical, threshold=2.0):
"""
检测相关性矩阵是否发生结构性突变
corr_window: 当前窗口的相关性矩阵 (n_assets x n_assets)
corr_historical: 历史平均相关性矩阵
"""
# 计算矩阵差异的Frobenius范数
diff = np.linalg.norm(corr_window - corr_historical, 'fro')
# 计算历史差异的均值和标准差
# 这里简化处理,实际应该用滚动窗口
threshold_value = threshold * np.std(historical_diffs)
return diff > threshold_value, diff
实战技巧:我一般用60天的滚动窗口计算相关性矩阵,然后和过去一年的平均矩阵做对比。如果差异超过3个标准差,基本可以确认有异常事件发生。
模式二:资产对相关性断裂
有些资产对是出了名的「铁关系」,比如SPX和NDX,正常情况下相关性在0.8以上。如果某天突然掉到0.3以下,那肯定有问题。
def detect_pair_correlation_break(asset1_series, asset2_series,
window=20, threshold=0.5):
"""
检测特定资产对的相关性断裂
"""
rolling_corr = asset1_series.rolling(window).corr(asset2_series)
# 标记断裂点:相关性低于历史均值的threshold倍标准差
historical_mean = rolling_corr.mean()
historical_std = rolling_corr.std()
break_points = rolling_corr < (historical_mean - threshold * historical_std)
return break_points
我曾经用这个方法抓到过一次数据错误:某天VIX和SPX的相关性从-0.7变成了+0.2。一开始以为是市场异象,后来发现是VIX的结算价数据被搞错了。你看,相关性矩阵有时候比人工检查靠谱多了。
模式三:多资产异常传播路径
这个稍微高级一点。当某个资产出现异常时,它会通过相关性网络传播到其他资产。我们可以用图论的方法来追踪传播路径。
import networkx as nx
def build_correlation_graph(corr_matrix, threshold=0.7):
"""
构建相关性网络图
只保留相关性高于threshold的边
"""
G = nx.Graph()
n = corr_matrix.shape[0]
for i in range(n):
for j in range(i+1, n):
if abs(corr_matrix[i, j]) > threshold:
G.add_edge(i, j, weight=corr_matrix[i, j])
return G
# 计算节点中心性,找出异常传播的关键节点
G = build_correlation_graph(corr_matrix, threshold=0.6)
centrality = nx.betweenness_centrality(G)
print("关键传播节点:", centrality)
可视化:相关性矩阵热力图
光看数字不够直观。我习惯把相关性矩阵画成热力图,异常点一目了然。
import matplotlib.pyplot as plt
import seaborn as sns
def plot_corr_heatmap(corr_matrix, title="相关性矩阵",
annot=True, cmap='RdBu_r'):
"""
绘制相关性矩阵热力图
"""
plt.figure(figsize=(10, 8))
mask = np.triu(np.ones_like(corr_matrix, dtype=bool))
sns.heatmap(corr_matrix, mask=mask, annot=annot,
cmap=cmap, center=0, vmin=-1, vmax=1,
square=True, linewidths=0.5,
cbar_kws={"shrink": 0.8})
plt.title(title, fontsize=14)
plt.tight_layout()
return plt.gcf()
注意:相关性矩阵热力图有个坑——如果资产数量超过20个,视觉上就很难分辨了。我一般只展示关键资产对,或者用聚类方法把相关性高的资产归为一组再画。
实战案例:2020年3月的市场崩盘
讲个真实案例。2020年3月,新冠疫情引发全球市场暴跌。当时我负责监控一组跨资产波动率曲面,包括美股股指、国债、原油、黄金。
3月9日那天,我注意到几个异常信号:
- SPX和原油的波动率曲面相关性从0.3跳升到0.7
- 黄金和国债的相关性从-0.2变成了+0.5(避险资产集体失效)
- VIX和所有资产的相关性都趋近于-1(恐慌情绪全面蔓延)
这些信号叠加在一起,基本可以确认是系统性危机,而不是个别资产的数据问题。我当时做的决策是:全面降低期权卖方仓位,增加尾部风险对冲。后来的走势证明这个判断是对的。
相关性矩阵的SVG结构图
下面这张图展示了多资产曲面异常检测的完整流程:
实际应用中的注意事项
最后分享几个我在实战中踩过的坑:
- 窗口大小选择:窗口太短(比如5天)噪声太大,太长(比如252天)又反应太慢。我一般用20-60天的滚动窗口,具体看资产类别。
- 非同步交易问题:不同资产交易时间不同,比如美股和日股。直接算相关性会有偏差。我习惯先把数据对齐到同一时间戳。
- 相关性不等于因果:两个资产相关性突然升高,不一定代表它们之间有直接联系。可能是共同受到第三个因素的影响。
我的建议:把相关性矩阵异常检测作为第一道防线,而不是唯一防线。它帮你快速定位问题,但最终决策还是要结合基本面分析。
好了,多资产相关性矩阵在异常检测中的应用就聊到这里。这套方法我用了好几年,确实帮我在数据清洗和风险预警上省了不少力气。你可以在自己的数据集上试试,看看能不能发现一些有意思的模式。