28. 曲面可视化技术:3D曲面图、热力图、动态曲面动画的Python实现
波动率曲面这东西,光看数字矩阵,说实话,脑子容易炸。你想想看,几十个行权价乘以十几个到期日,几百个数字堆在一起,谁能一眼看出哪里贵了、哪里便宜了?
所以,可视化不是花架子,是刚需。我做了这么多年期权定价,曲面可视化帮我抓出过不少数据异常。有一次,我发现曲面尾部突然翘起来,一查,原来是某个深度虚值期权流动性差,报价被一个散户挂单带偏了。没有图,这种问题根本发现不了。
今天我们就来聊聊,怎么用Python把波动率曲面画得漂漂亮亮的。三种主流方式:3D曲面图、热力图、动态动画。一个一个来。
28.1 准备数据:先造一个曲面出来
画图之前,得有数据。我们模拟一个典型的波动率微笑加期限结构的曲面。代码很简单,但背后逻辑你得懂:行权价方向体现微笑,到期日方向体现期限结构。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import seaborn as sns
# 设定参数
strikes = np.linspace(0.8, 1.2, 15) # 行权价,标准化为ATM=1.0
maturities = np.array([0.1, 0.2, 0.3, 0.5, 0.75, 1.0, 1.5, 2.0]) # 年化到期时间
# 构造曲面:微笑 + 期限结构
X, Y = np.meshgrid(strikes, maturities)
# 微笑效应:越偏离ATM,波动率越高
smile = 0.05 * (X - 1.0) ** 2
# 期限结构:短期波动率低,长期波动率高
term_structure = 0.02 * np.sqrt(Y)
# 基础波动率
base_vol = 0.25
Z = base_vol + smile + term_structure + np.random.normal(0, 0.005, X.shape)
print("数据形状:", Z.shape)
print("波动率范围:{:.3f} ~ {:.3f}".format(Z.min(), Z.max()))
这里我加了一点随机噪声,模拟真实市场数据的不完美。嗯,真实数据从来不会这么光滑,你懂的。
28.2 3D曲面图:最直观的立体呈现
3D曲面图是我个人最常用的方式。它能把整个曲面的形态立体地展示出来,哪里凸起、哪里凹陷,一目了然。
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
# 绘制曲面
surf = ax.plot_surface(X, Y, Z, cmap='viridis',
alpha=0.9, linewidth=0, antialiased=True)
# 设置标签
ax.set_xlabel('行权价 (Strike)', fontsize=12, labelpad=10)
ax.set_ylabel('到期时间 (Maturity)', fontsize=12, labelpad=10)
ax.set_zlabel('隐含波动率', fontsize=12, labelpad=10)
ax.set_title('波动率曲面 - 3D视图', fontsize=14, fontweight='bold')
# 添加颜色条
cbar = fig.colorbar(surf, ax=ax, shrink=0.5, aspect=10)
cbar.set_label('波动率水平', fontsize=10)
plt.tight_layout()
plt.show()
画出来之后,你会看到曲面像一张被风吹起的毯子。中间低、两边高,那是微笑效应。沿着到期时间方向慢慢抬升,那是期限结构。我在项目中遇到过,有些同事只看表格,死活没发现某个期限的波动率异常偏高,3D图一拉出来,那个尖峰简直扎眼。
28.3 热力图:二维视角下的细节挖掘
3D图好看,但说实话,精确读取数值不太方便。这时候热力图就派上用场了。它把曲面压平到二维平面,用颜色深浅表示波动率高低。
plt.figure(figsize=(12, 6))
# 绘制热力图
ax = sns.heatmap(Z, xticklabels=np.round(strikes, 2),
yticklabels=np.round(maturities, 2),
cmap='YlOrRd', annot=True, fmt='.3f',
linewidths=0.5, linecolor='white',
cbar_kws={'label': '隐含波动率'})
ax.set_xlabel('行权价', fontsize=12)
ax.set_ylabel('到期时间 (年)', fontsize=12)
ax.set_title('波动率曲面 - 热力图', fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
热力图有个好处:你可以直接读出每个格子的具体数值。我建议把 annot=True 打开,虽然数字多了看着有点挤,但做报告的时候,领导就喜欢这种「有数据支撑」的感觉。
28.4 动态曲面动画:让时间流动起来
静态图只能看一个时间点。但波动率曲面是随时间变化的。今天和明天的曲面可能完全不同。动态动画能让你看到曲面如何随着市场情绪演变。
这里我们用 matplotlib.animation 来实现。模拟一个曲面随时间波动的效果:
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
# 生成多期曲面数据
n_frames = 50
Z_series = np.zeros((n_frames, *Z.shape))
for t in range(n_frames):
# 模拟曲面随时间波动:微笑加深、期限结构变化
time_factor = 1 + 0.1 * np.sin(2 * np.pi * t / n_frames)
smile_dynamic = 0.05 * (X - 1.0) ** 2 * time_factor
term_dynamic = 0.02 * np.sqrt(Y) * (1 + 0.05 * np.cos(2 * np.pi * t / 20))
Z_series[t] = base_vol + smile_dynamic + term_dynamic + np.random.normal(0, 0.003, X.shape)
# 创建动画
fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(111, projection='3d')
def update(frame):
ax.clear()
surf = ax.plot_surface(X, Y, Z_series[frame], cmap='viridis',
alpha=0.9, linewidth=0, antialiased=True)
ax.set_xlabel('行权价', fontsize=10)
ax.set_ylabel('到期时间', fontsize=10)
ax.set_zlabel('隐含波动率', fontsize=10)
ax.set_title(f'波动率曲面动态演化 - 第{frame+1}帧', fontsize=12)
ax.set_zlim(0.2, 0.35)
return surf,
ani = FuncAnimation(fig, update, frames=n_frames, interval=200, blit=False)
# 在Jupyter中显示
# HTML(ani.to_jshtml())
# 保存为GIF(需要安装pillow)
# ani.save('vol_surface_animation.gif', writer='pillow', fps=5)
plt.show()
28.5 三种可视化方式的对比与选择
| 可视化方式 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 3D曲面图 | 立体感强,整体形态一目了然 | 精确读取数值困难,视角遮挡 | 汇报展示、趋势判断、异常检测 |
| 热力图 | 数值精确,细节丰富 | 缺乏立体感,大趋势不直观 | 数据核查、报告附注、定量分析 |
| 动态动画 | 展示时间维度变化,发现异常波动 | 文件体积大,不适合打印 | 实时监控、数据质量检查、教学演示 |
我个人习惯是:做研究时先用3D图扫一眼整体,再用热力图抠细节。如果发现异常,就拉出动态动画看它是不是持续性的。三个工具配合使用,基本不会漏掉问题。
28.6 知识体系总览
下面这张SVG图,把本章的核心逻辑串起来了。从原始数据到三种可视化方式,再到各自的应用场景,一条线走通。
说白了,可视化就是给数据装上眼睛。你盯着数字看半天可能没感觉,图一出来,问题自己就跳出来了。我建议你把这段代码跑一遍,换自己的数据试试。第一次跑通之后,你会爱上这种「一眼看穿曲面」的感觉。
公众号:蓝海资料掘金营,微信deep3321