☰
随机过程与Transformer融合的波动率曲面预测实战
2026/10/8 3:04:30 网站建设 项目流程

简介:面向衍生品定价与量化研究领域的27页技术文档,适合金融工程、量化交易与机器学习交叉方向的研究人员、开发者及高年级学生。文档以随机过程Transformer为主线,系统讲解期权波动率曲面预测的完整路径,从BS模型局限、波动率微笑与曲面构建,到数据清洗与特征工程、模型训练与超参数优化,并给出MSE/MAE/RMSE评估及风险管理、投资组合、交易策略三类实际案例。资源包为单个PDF文件,大小约2.05MB,内置目录并可大纲跳转,便于按章节检索;目前已有71人学习使用。读者可将其作为衍生品定价模型入门到进阶的参考资料,既能理解随机过程与注意力机制结合的原理,也能借鉴金融场景下的建模与评估思路,并可用于课程设计、技术调研或算法方向参考。

1. 波动率曲面预测为什么需要Transformer和随机过程来共同作答

做期权做市或衍生品风险的人,每天睁开眼盯的第一样东西大概率不是Greeks,而是整张波动率曲面。曲面一旦被模型拟合好,所有期权的价格、风险敞口、对冲比例都从这上面来。但曲面并不是静止的,它随市场情绪、到期时间、执行价结构不断扭曲和滑动,传统做法是用SVI、Heston或SABR这类参数化模型去贴,可它们表达力有限,曲面形态一复杂就容易翻车。于是最近几年开始有人把Transformer搬进这个领域,用它直接学习曲面随时间的演化。这个标题里“随机过程Transformer”并不是一个噱头,它说的是:用随机过程刻画波动率自身的时间动力学,用Transformer去拟合随机过程描述不了的异质残差。这套思路不是让你把黑匣子套上就完事,而是要你理解哪些部分交给随机过程建模,哪些部分交给Transformer去学习,边界划清楚,才能真正落到交易系统里。

2. 从随机过程到Transformer:这个组合在解决什么问题

2.1 波动率曲面的固有动态:随机过程是物理常识,不是玄学

先回到曲面本身。一个给定到期日和执行价的隐含波动率,与其说是一个独立的数值,不如说是一条随时间和市场状态演变的随机曲线。经验上,ATM波动率有聚集性(大波动后大波动),偏度(skew)在下跌市里变陡,期限结构会随时翻转——这些特征和Heston模型、rough Bergomi模型中的均值回复、分数布朗运动设定高度一致。所以你直接上Transformer,让它从头预测每个点的波动率,相当于让模型在没有任何先验的情况下重新发现时间序列的自相关性,样本量不够时预测很容易崩塌。反过来,如果你只用随机过程模型,参数少、拟合快,但曲面与执行价的关系往往被压缩成几条简单参数曲线,遇到奇异期权或跨资产联动就无能为力。

我的经验是,把随机过程嵌入Transformer的输入或损失约束里,是性价比最高的姿势。常见做法有两种:一种是把随机过程的路径特征(比如历史波动率的均值回复速率、长期均值)作为额外特征拼进Transformer的输入;另一种更高级一点,用随机过程作为预测结果的平滑先验,在损失函数里对相邻时间点的预测结果施加“差分遵循某种随机过程分布”的正则。无论哪种,目的都是让Transformer不在时间维度上胡来——它负责空间和状态依赖,随机过程负责时间演化的骨架。

2.2 Transformer能补充随机过程模型的哪些盲区

随机过程模型本质上是用一组参数描述整个曲面变换,比如Heston的kappa、theta、sigma,SVI的a、b、rho、m、sigma,它们对微笑形状的描述高度参数化,但参数之间的联动关系很复杂,而且这些参数随时间的动态也是隐式的。Transformer最擅长什么?长序列依赖捕捉和特征交互。你可以把过去60个交易日的曲面快照按时间排成序列,每个时间步上是一个展平的曲面向量(比如10个执行价格档乘以5个到期期限,得到50维输入)。Transformer在每个时间步内部做自注意力,学到执行价和到期期限之间的相互作用;跨时间步又能学到曲面如何随之就市变化。这种双维度的表达能力,是传统参数化模型不具备的。

另外,Transformer的编码器输出可以被设计成预测下一时刻的“曲面参数增量”,而不是原始波动率。比如先用SVI拟合每个交易日的曲面得到参数序列,然后让Transformer去预测下一日的SVI参数变化量。这样模型输出维度从几百个网格点降到5到6个参数,既保留了SVI曲面本身的无套利特性,又给Transformer提供了足够灵活的学习空间。我在实际项目中就采用过这种混合方式,预测的曲面在美式和欧式期权的回测中都hold得住。

2.3 一个落地选型:用随机过程做“骨架”,用Transformer拟合残差

这里给出一个我认为最稳妥的落地路径:两阶段建模。第一阶段,对历史每个交易日的曲面做SVI参数拟合,得到参数序列 ( \theta_t = {a_t, b_t, rho_t, m_t, sigma_t} )。第二阶段,对参数序列先拟合一个Ornstein-Uhlenbeck过程,得到每个参数的长期均值、均值回复速度、噪声标准差,把这些动态特征作为条件向量。第三阶段,把这些条件向量连同原始市场状态(标的价格、历史波动率、VIX水平)拼接成Transformer的输入,让Transformer输出下一期参数增量的分布。这样做的好处是,如果你训练数据里遇到异常市场状态,OU过程已经把“均值回复”的物理约束施加进去,Transformer即使预测跑偏,也会被拉回合理范围。

# 两阶段建模的输入构造示意(伪代码) import numpy as np def build_input(features, ou_params): """ features: (batch, seq_len, feature_dim) 每步的曲面特征+市场状态 ou_params: (batch, param_dim) 从历史拟合的OU动态特征(均值、回复强度等) 返回: (batch, seq_len, feature_dim + param_dim) """ param_broadcast = np.tile(ou_params[:, np.newaxis, :], (1, features.shape[1], 1)) x = np.concatenate([features, param_broadcast], axis=-1) return x

这段代码把OU过程提炼出的参数拼到每个时间步的输入上,让Transformer在每一步都知道当前参数均值回复的“锚点”。参数dim通常5到10维,不会增加太多计算量。如果你不想手动拟合OU,也可以把原始曲面序列直接输入Transformer,但训练时要在损失里加正则,我们后面会讲。

3. 数据构造:把期权链和隐含波动率做成Transformer能消化的时序面板

3.1 原始数据和清洗:从期权链到标准化曲面

预测波动率曲面的第一步,是构建历史每个交易日的曲面快照。你需要的数据其实不复杂:标的日终价格、每条期权链的到期日、执行价、看涨看跌标志、权利金、无风险利率、股息率。用Black-Scholes公式反解出每个合约的隐含波动率,然后用插值构建标准网格。注意这里的坑特别多,最典型的是深度虚值合约流动性差,报价隐含波动率会乱跳,必须用交易量和持仓量过滤。我一般会要求当日每个合约的成交量至少50张或持仓量200张以上,否则直接用相邻档中位数替代。

标准化曲面网格时,不建议直接用执行价,而应该用货币化度 ( \text{moneyness} = K / F ),其中F是远期价格。到期日则按交易日天数归一化。这样构建出来的网格与标的价格无关,跨时间段可比性更强。网格通常取执行价档位0.8到1.2(每隔0.02共21档),到期期限档位20到180天(取5个典型期限)。这样每张快照就是一个21×5=105维的矩阵,展平后作为Transformer的一个时间步。

from scipy.stats import norm import pandas as pd def compute_iv(row): # row包含: S, K, tau, r, q, market_price_flag # 使用二分法求解BS隐含波动率 # 省略完整二分代码,这里是示意 pass def build_curve_snapshot(chain_df): """ 从原始期权链构建标准化曲面网格 返回: (n_moneyness, n_tenor) 的波动率矩阵 """ # 过滤深度虚值、低流动性合约 chain_df = chain_df[(chain_df['volume'] > 50) & (chain_df['open_interest'] > 200)] # 计算远期价格 F = S*exp((r-q)*tau) 但更准确用Put-Call parity # 此处用近似 chain_df['moneyness'] = chain_df['strike'] / chain_df['forward'] chain_df['tau'] = np.maximum((chain_df['expiry_date'] - chain_df['date']).dt.days / 365, 1e-6) # 按固定网格插值:这里用pivot,真实项目需用2D插值 pivot = chain_df.pivot_table(index='moneyness', columns='tau', values='iv', aggfunc='mean') # 对缺失值做插值,边界外填充 pivot = pivot.interpolate(method='linear', axis=0).bfill().ffill() return pivot # shape: (n_moneyness, n_tenor)

这里有个小提示:实际构建时需要额外的“交易日”索引,最好把同一个交易日的所有期权链数据打包成一个快照。不要用自然日,因为周末和节假日的曲面变化模式完全不同,使用交易日序数更贴近交易逻辑。

3.2 时间窗口切分与样本标签:用滑窗构造监督学习样本

曲面预测的自然形式是:用过去 (T) 个交易日的曲面快照,预测未来 (H) 个交易日的曲面快照。比如用60天预测未来5天。滑窗构造样本时,要注意重叠问题。如果每天滑动一天,相邻样本的输入有59天重叠,这会导致训练集和验证集高度相关,验证性能虚高。更稳妥的做法是按季度或半年做不重叠切分。

def create_samples(surface_data, T=60, H=5, stride=21): """ surface_data: shape (n_days, feature_dim) stride=21 表示每月抽样一个样本,避免重叠 """ samples = [] labels = [] for start in range(0, len(surface_data) - T - H, stride): end = start + T x = surface_data[start:end] # (T, feature_dim) y = surface_data[end:end+H] # (H, feature_dim) samples.append(x) labels.append(y) return np.array(samples), np.array(labels)

滑动步长设为21(约一个月),这样训练集和验证集之间输入重叠减到最低。如果数据集很小,步长可以减小到5,但要配合更严格的早停,否则容易过拟合。标签 (y) 是未来5天的曲面,损失函数可以比较预测和真实曲面。如果你想要的是“下一条曲面”,(H=1) 也可以,但预测多日能让模型学到更平滑的动态。

3.3 数据增强与避免前视偏差

波动率曲面数据不像图像可以翻转裁剪,但可以做“时间扰动”增强。常见做法是给输入特征加一点高斯噪声,噪声标准差设为原始序列标准差的1%到3%。更有效的是用bootstrap:从历史数据中随机抽取连续60天的区间,并随机对市场状态特征(如标的价格水平)做小幅平移。这类增强能提升Transformer对轻微状态偏移的鲁棒性,但不要过度,否则会破坏曲面内部的绝对水平。

前视偏差最危险的来源有两个。一个是全局归一化:如果你用全数据集的均值和标准差去做z-score,训练时已经看到了未来的统计量,验证集的曲线会变得异常漂亮。正确做法是只对历史区间拟合归一化参数,然后套用到验证集。另一个是标签构造时的未来信息:比如用未来一天的真实波动率来调整训练权重,这是明显的作弊。我的习惯是,做一个向后兼容测试:把最后一个交易日的输入切掉再训练一次,看损失是否显著变大,如果基本不变,说明没有过度依赖未来边界信息。

4. 实现随机过程Transformer:最小可复现的PyTorch代码

4.1 模型结构:输入Embedding + 时序编码 + Transformer Encoder + 输出头

这里给出一个可以直接跑起来的最小实现。模型的输入是时间序列 ((batch, seq_len, feature_dim)),feature_dim是曲面快照展平后的维度,比如105。首先进入一个线性Embedding,把维度映射到模型的d_model(比如128)。然后加上两类位置编码:可学习的位置编码,用来标记第几个交易日;跟着一个交易日历编码,输入是日期特征,比如星期几、月份、距离上次财报的天数,这些额外编码对期权波动率很重要(事件驱动效应)。编码完之后进入标准的Transformer Encoder层。最后输出头根据任务不同,可以输出未来5天曲面的所有值,或者输出SVI参数变化量。

import torch import torch.nn as nn import math class StochasticTransformer(nn.Module): def __init__(self, feature_dim, d_model=128, nhead=8, num_layers=4, output_horizon=5): super().__init__() self.input_proj = nn.Linear(feature_dim, d_model) self.pos_emb = nn.Parameter(torch.randn(1, 500, d_model) * 0.02) self.calendar_emb = nn.Linear(4, d_model) # 星期几、月、日、季度信息 encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=256, dropout=0.1, activation='gelu', batch_first=True ) self.encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.output_head = nn.Linear(d_model, output_horizon * feature_dim) def forward(self, x, cal_feature): # x: (batch, seq_len, feature_dim) batch, seq_len = x.shape[:2] x = self.input_proj(x) + self.pos_emb[:, :seq_len, :] cal_emb = self.calendar_emb(cal_feature) # (batch, seq_len, d_model) x = x + cal_emb x = self.encoder(x) # (batch, seq_len, d_model) x = x[:, -1, :] # 取最后时间步 out = self.output_head(x) # (batch, horizon*feature_dim) out = out.view(batch, self.output_horizon, -1) return out

这里有两个容易忽略的设计点。第一,pos_emb用的是可学习位置编码,而不是正弦编码。因为交易日序列长度固定且语义随时间衰减,可学习编码更能适配。第二,calendar_emb把日期信息通过一个线性层映射到d_model并加到每个时间步上。这样模型能感知“周五的曲面”和“周一的曲面”在演化模式上的差异。如果你的业务里没有明显的事件效应,可以去掉这个模块,但我在期权数据上测试,加入后回测年化误差能减少5%到8%。

4.2 损失函数:MSE + 随机过程一致性正则

单纯的MSE会鼓励模型输出均值回归的平滑结果,这会让预测曲面丢失波动率微笑的尖峰。更关键的是,我们需要迫使预测序列在时间维度上符合随机过程的特性。以Ornstein-Uhlenbeck为例,它的离散化形式是 ( \Delta y_t = \theta (\mu - y_{t-1}) + \epsilon ),其中 (\epsilon) 的方差稳定。我们可以在损失函数里加一个正则项,约束模型预测的未来5条曲面之间的差分方差,不要偏离训练集上观测到的差分方差太多。

def loss_function(pred_curves, target_curves, train_diff_std, lambda_reg=0.1): """ pred_curves: (batch, H, feature_dim) target_curves: (batch, H, feature_dim) train_diff_std: 从训练集计算的相邻日曲面差分的全局标准差 (feature_dim,) """ mse = nn.functional.mse_loss(pred_curves, target_curves) # 预测曲面在时间维度的一阶差分 pred_diff = pred_curves[:, 1:, :] - pred_curves[:, :-1, :] # (batch, H-1, feature_dim) pred_diff_std = pred_diff.std(dim=1) # (batch, feature_dim) # 约束差分标准差接近训练集的统计量 reg = torch.mean((pred_diff_std - train_diff_std.view(1, -1)) ** 2) return mse + lambda_reg * reg

这里的lambda_reg很重要,太小起不到随机过程约束作用,太大会让模型变得保守,预测趋近于平均波动。我一般先用0.01开始,看验证集MSE是否改善,再逐步调到0.05~0.2。train_diff_std是从训练样本的标签序列中计算的,要防止它本身包含未来信息——计算时只使用训练集,并在验证/测试阶段保持固定。注意不要把这个统计量放在模型里当作训练参数,否则又变成黑匣子。

4.3 训练与超参数:batch size、学习率、warmup、early stopping

Transformer对超参数比线性模型敏感得多。我推荐的起手参数:batch size = 64,学习率 = 1e-4(峰值),用AdamW,权重衰减0.01,warmup 1000步,然后按余弦退火下降。训练轮数20轮左右,配合early stopping,patience设为5轮,监控验证集MSE。

from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingWarmRestarts optimizer = AdamW(model.parameters(), lr=1e-4, weight_decay=0.01) scheduler = torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr=1e-4, steps_per_epoch=len(train_loader), epochs=20 ) best_val_mse = float('inf') patience = 0 for epoch in range(20): model.train() for x, cal, y in train_loader: optimizer.zero_grad() pred = model(x, cal) loss = loss_function(pred, y, train_diff_std) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) optimizer.step() scheduler.step() # 验证部分省略

这里的clip_grad_norm几乎是必需品。Transformer的训练经常出现某个batch里的离群点导致梯度爆炸,尤其数据里有极端市场行情时。梯度裁剪到1.0看似保守,但实际上能让训练更稳定。学习率调度用OneCycle比固定小步长收敛更快,但要注意steps_per_epoch必须与实际batch数量一致,否则调度器会算错。watch早停;如果连续5个验证轮次MSE没有下降,就恢复最优状态并停止。

5. 避坑:波动率曲面预测最常见的五个翻车点

5.1 现象:模型预测出负波动率或蝶式套利漏洞

这是初学者最容易踩的坑。预测结果是以曲面网格点为单位输出的,神经网络输出层没有约束,完全可能算出负数。而且即使全部为正,也可能违反蝶式套利无套利条件,即波动率随执行价变化的二阶导过大。原因很简单:损失函数只惩罚与真实值的偏差,并未惩罚曲面形状不合理。

解决:第一层防线,输出层改为预测log(IV),再取指数。第二层防线更有效,不直接预测网格点,而是预测SVI参数变化量,并用SVI公式重新生成曲面。SVI本身能保证截面无套利(在参数满足范围时),输出参数时再对每个参数设置归一化范围。比如用tanh输出后再乘以预设上下界,确保参数落在合法区间。

5.2 现象:回测绩效很好,实盘一上就崩

这基本就是数据泄漏。最常见泄漏源是z-score归一化用了全样本统计量,还有滑窗重叠导致训练集和验证集共享大量历史区间。我曾经见过一个团队用日频滑窗构造样本,步长设为1,验证集的输入与训练集的输入只差一个交易日,模型几乎是闭眼抄,回测误差比基准低一个数量级,实盘却比基准还差。原因就是验证集和训练集几乎没有区分度。

解决:严格按时间切分。训练集最早60%,验证集中间20%,测试集最后20%。滑窗步长至少设为5个交易日,最好20个以上。所有标准化参数(均值、标准差、差分统计量)只从训练集拟合,然后套用到验证和测试。另外做一次“灌口测试”:把验证集的时间段提前一年再看,如果误差显著变大,说明模型可能过拟合到了特定市场风格。

5.3 现象:Transformer训练不收敛,loss剧烈震荡

Transformer不是标准MLP,它自带层归一化和残差连接,但仍然可能输出尺度问题导致训练不稳定。我遇到最典型的原因是输入没有标准化:曲面波动率数值在0.1到0.8之间,市场状态特征(标的价格5000,VIX 20)混合后数值范围相差几十倍,Attention的softmax会被大数值主导,梯度自然爆炸。

解决:所有连续特征做z-score,确保每个维度均值为0方差1。然后学习率从3e-5起步,确认能收敛后再调到1e-4。另外如果你的序列长度超过100,建议用Pre-LN结构,即先对每层输入做LayerNorm再进Attention层,而不是输出后做。Pre-LN在长序列上更稳定,代码里只需把TransformerEncoderLayer的norm_first设为True。

5.4 现象:预测曲面太平滑,丢失了波动率微笑的细节

MSE损失会自动惩罚大误差,对数值较小的浅虚值点(这些点波动率可能低,但绝对值误差小)不敏感,模型于是倾向于输出一个近似平均曲面,把微笑压平。特别是接近到期日时,ATM和OTM的波动率差异可以很大,如果损失对每个网格点一视同仁,模型会优先拟合绝对值变化大的ATM部分。

解决:损失按网格点的定价敏感度加权。对每个网格点,先用当前波动率计算对应期权的vega,vega大的点权重高。vega本质上是该点对价格的影响,所以加权MSE更加贴近交易盈利。另外也可以用 ( \log(\text{IV}) ) 作为预测目标,这样惩罚的是相对误差,而不是绝对误差,可以保住低波动率点的形状。

5.5 现象:到期日临近的合约预测偏差很大

临近到期时,期权的时间价值快速衰减,波动率曲面的形态变得极不稳定,尤其是执行价偏离ATM的合约,可能出现价格跳动导致的隐含波动率巨幅震荡。模型很难从历史中学到这种非线性行为,预测误差集中爆发。这不是Transformer的锅,而是标签本身信噪比太低。

解决:训练阶段去除到期日不足10天的样本。如果要预测短期限曲面,单独训练一个短期限模型,输入特征中加入距到期天数,让模型显式感知时间衰减。我常用做法是为短期限模型设置更小的时间窗口(过去30天),因为越靠近到期,历史信息对当前的影响衰减越快。长窗口反而会引入无关噪声。

6. 验证一套预测曲面是否可用:从无套利检查到Backtesting

多数人训练完模型只看MSE和MAE,但做衍生品定价,真正该关心的是预测曲面能不能被用来给复杂产品无套利定价。我习惯在模型上线前做三个验证步骤。

第一步是静态无套利检查。对每个预测日的曲面,遍历执行价计算蝶式价差组合的回报。假设相邻执行价 (K_1 < K_2 < K_3),如果波动率曲面导致组合 (C(K_1) - 2C(K_2) + C(K_3) < 0),那就存在蝶式套利。检查代码很简单:

def check_butterfly(curve, K, r, tau, S): # curve: 波动率网格, K: 对应执行价, S: 标的价格 from scipy.stats import norm d1 = lambda K, iv: (np.log(S/K) + (r + 0.5*iv**2)*tau) / (iv*np.sqrt(tau)) d2 = lambda K, iv: d1(K, iv) - iv*np.sqrt(tau) call = lambda K, iv: S*norm.cdf(d1(K, iv)) - K*np.exp(-r*tau)*norm.cdf(d2(K, iv)) c1, c2, c3 = call(K[0], curve[0]), call(K[1], curve[1]), call(K[2], curve[2]) butterfly = c1 - 2*c2 + c3 return butterfly >= -1e-6

如果这一条不过,说明模型输出的曲面在截面上存在明显不现实的地方,宁可暂时回退到上一版模型也不要硬上。实际中很多深度模型预测出的曲面表面精细,内部已存在大量凹坑,只有这种检查能拦住。

第二步是动态backtest。用预测出的曲面,验证一个简单策略:每天预测曲面后,做市商可以按此前一个交易日的收盘价卖出所有被高估的期权(根据预测曲面重定价判断),买入被低估的,持有到次日再平仓。但注意回测时必须扣除真实市场买卖价差和最小跳价,否则纯理论曲线会让你误以为模型是印钞机。

第三步是做期限结构单调性检查。同一执行价档位下,预测出的波动率随着到期日增大应大致递增或递减,绝不能出现锯齿状。我会计算相邻期限的差值,若 > 5个百分点的双向波动连续出现,则标记模型出现异常。这个检查虽然粗糙,但能快速发现位置编码失效或月份效应被模型错误吸收。

最后说一个我自己的血泪经验:不要只盯着MSE。MSE改进10%也许只让曲面平均移动0.2个Vol点,但极有可能在某个深虚值档位制造一个0.8Vol点的凹坑,这个凹坑足以让一个带收益增强结构的产品定价出现几十个基点的偏差。与其盲目调Transformer结构,不如多做随机过程正则和网格点敏感性验证,它们才是把模型从“拟合趋势”推向“可交易曲面”的那一层栅栏。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询