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 可视化流程总结

说了这么多,咱们捋一下。我一般按这个顺序来:

  1. 先画3D曲面图——看整体形态,找明显异常
  2. 再画热力图——看局部细节,检查微笑对称性
  3. 接着画等高线图——看期限结构,验证模型一致性
  4. 最后做交互式图表——深入分析,定点排查

这四步走完,你对这个曲面的了解,比看一百行数字都深刻。

最后提醒一句:可视化不是终点,是手段。画完图发现问题,一定要回去检查数据源和拟合参数。别为了图好看,把异常点硬生生平滑掉了。那叫自欺欺人。

曲面可视化四步检查流程 3D曲面图 看整体形态 热力图 看局部细节 等高线图 看期限结构 交互式图表 深入分析 发现异常 → 检查数据源 → 调整拟合参数 → 重新可视化 可视化是手段,不是终点。别为了图好看而掩盖问题。

公众号:蓝海资料掘金营,微信deep3321