18. 曲面可视化:3D曲面图、热力图、等高线图、动态交互式图表
数据构建完了,模型也拟合好了。接下来干嘛?
你得把那个抽象的隐含波动率曲面,变成肉眼能看懂的东西。说白了,就是让数字说话。
我个人习惯,在完成曲面构建后,第一件事不是去算定价误差,而是先画图。看一眼,心里就有底了。哪里平滑,哪里突兀,哪个期限出了异常值,一目了然。
18.1 为什么可视化这么重要?
你想想看,一个曲面背后可能有几百个数据点。光看数字,你很难发现局部的问题。比如某个行权价附近,波动率突然跳升了2个点——这在表格里可能就是一行数字,但在3D图上,那就是一个尖锐的“刺”。
我在项目中遇到过,有一次用SVI拟合完,看均方根误差挺小的,以为万事大吉。结果画了个热力图一看,远月虚值期权那块颜色深得离谱。后来排查发现,是那个期限的报价数据混入了几个错误值。嗯,要是没画图,这个坑就踩进去了。
核心原则:可视化不是为了好看,是为了快速发现异常、验证模型合理性。
18.2 四种图,各有各的用处
我一般会画四张图,每张图看一个侧面。咱们一个一个说。
18.2.1 3D曲面图
这是最直观的。X轴是行权价(或者Delta),Y轴是剩余期限,Z轴是隐含波动率。整个曲面就像一块起伏的地形。
好的曲面,应该是光滑的、没有明显褶皱的。如果你看到某个地方突然塌陷或者凸起,那就要警惕了。
import plotly.graph_objects as go
import numpy as np
# 假设我们已经有了网格数据
# strike_grid: 行权价网格 (50x50)
# time_grid: 期限网格 (50x50)
# iv_surface: 拟合后的波动率曲面 (50x50)
fig = go.Figure(data=[
go.Surface(
z=iv_surface,
x=strike_grid,
y=time_grid,
colorscale='Viridis',
opacity=0.85
)
])
fig.update_layout(
title='隐含波动率曲面 (3D)',
scene=dict(
xaxis_title='行权价',
yaxis_title='剩余期限 (年)',
zaxis_title='隐含波动率 (%)'
),
width=800,
height=600
)
fig.show()
小技巧:我习惯把opacity设成0.85左右,稍微透明一点,能看到背后的结构。完全实心的曲面,有时候会遮挡后面的异常点。
18.2.2 热力图
热力图是3D曲面的俯视图。把Z轴的值用颜色来表示。红色高,蓝色低。
这个图的好处是,你能一眼看出波动率微笑的形态。比如近月合约,虚值看涨和虚值看跌的颜色是不是对称?如果不对称,说明市场有偏斜(skew)。
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10, 6))
sns.heatmap(
iv_surface,
xticklabels=np.round(strike_grid[0, :], 1),
yticklabels=np.round(time_grid[:, 0], 2),
cmap='RdYlBu_r',
annot=False,
cbar_kws={'label': '隐含波动率 (%)'}
)
plt.title('隐含波动率曲面热力图')
plt.xlabel('行权价')
plt.ylabel('剩余期限 (年)')
plt.show()
注意:热力图的颜色映射要选对。我个人推荐RdYlBu_r(红-黄-蓝反转),红色代表高波动率,蓝色代表低波动率,符合直觉。别用那种彩虹色,容易误导。
18.2.3 等高线图
等高线图,说白了就是把曲面上相同波动率的点连成线。就像地图上的海拔线一样。
这个图在分析波动率期限结构时特别好用。你可以看看,是不是所有期限的等高线都大致平行?如果某条线突然拐弯,那可能就是那个区域的数据有问题。
plt.figure(figsize=(10, 6))
contour = plt.contour(
strike_grid[0, :],
time_grid[:, 0],
iv_surface,
levels=15, # 画15条等高线
cmap='viridis'
)
plt.clabel(contour, inline=True, fontsize=8, fmt='%.1f')
plt.title('隐含波动率等高线图')
plt.xlabel('行权价')
plt.ylabel('剩余期限 (年)')
plt.colorbar(label='隐含波动率 (%)')
plt.show()
避坑指南:我曾经在等高线图上吃过亏。当时用了默认的levels参数,结果画出来的线稀稀拉拉,根本看不出结构。后来我改成levels=15,线条密度才合适。记住,等高线条数要根据数据范围来调,一般10-20条比较合适。
18.2.4 动态交互式图表
静态图有个问题:你没法旋转、缩放、查看具体数值。所以,我强烈建议你做一个交互式图表。
用Plotly或者Bokeh都可以。我个人更偏爱Plotly,因为它生成的HTML文件可以直接在浏览器里打开,发给同事也方便。
import plotly.graph_objects as go
import numpy as np
# 构建一个带滑块的交互式图表
# 按期限切片,每个切片是一个波动率微笑
time_points = time_grid[:, 0] # 所有期限
strike_slice = strike_grid[0, :] # 行权价
fig = go.Figure()
# 先添加所有期限的曲线(默认只显示第一条)
for i, t in enumerate(time_points):
fig.add_trace(
go.Scatter(
x=strike_slice,
y=iv_surface[i, :],
mode='lines+markers',
name=f'T={t:.2f}',
visible=(i == 0) # 默认只显示第一条
)
)
# 创建滑块
steps = []
for i in range(len(time_points)):
step = dict(
method='update',
args=[{'visible': [False] * len(time_points)}],
label=f'{time_points[i]:.2f}年'
)
step['args'][0]['visible'][i] = True
steps.append(step)
sliders = [dict(
active=0,
steps=steps,
currentvalue={'prefix': '剩余期限: '}
)]
fig.update_layout(
sliders=sliders,
title='波动率微笑动态切片',
xaxis_title='行权价',
yaxis_title='隐含波动率 (%)'
)
fig.show()
我的习惯:交互式图表里,我还会加一个hover显示功能。鼠标悬停时,显示具体的行权价、期限和波动率数值。这样在排查异常点时,不用去猜坐标,直接看数值就行。
18.3 可视化流程总结
说了这么多,咱们捋一下。我一般按这个顺序来:
- 先画3D曲面图——看整体形态,找明显异常
- 再画热力图——看局部细节,检查微笑对称性
- 接着画等高线图——看期限结构,验证模型一致性
- 最后做交互式图表——深入分析,定点排查
这四步走完,你对这个曲面的了解,比看一百行数字都深刻。
最后提醒一句:可视化不是终点,是手段。画完图发现问题,一定要回去检查数据源和拟合参数。别为了图好看,把异常点硬生生平滑掉了。那叫自欺欺人。