PyTorch高频波动率预测:特征工程与LSTM+Attention实战
2026/9/19 15:02:02 网站建设 项目流程

简介:本资源是一份面向量化研究初学者与金融科技从业者的专题文档,聚焦PyTorch在股票价格波动率预测中的特征工程设计,覆盖高频交易背景、模型选型、统计与技术指标提取、特征选择与转换、训练优化及回测实践等完整知识链条。全包仅1个PDF文件,大小2.15MB,共45页,自带目录与章节大纲,便于按需跳转阅读。文档从张量操作、自动求导等PyTorch基础讲起,逐步深入到移动平均线、RSI、布林带等特征构建,并给出MSE、RMSE、MAE等评估指标及模型比较方法,兼顾理论与代码思路。内容组织上从特征工程基本流程到案例复盘逐层递进,适合希望系统搭建量化预测模型、梳理特征工程方法的读者参考学习。目前已有222人浏览学习,兼具入门导览与实操参考价值。

1. 高频交易里最稳的预测目标,不是涨跌而是波动率

一分钟K线的下一次收盘价涨跌,在高频数据里几乎逼近随机游走,预测准确率做到52%都算吃力;但同一份数据的波动率却呈现出极强的自相关和聚集性——大波动后往往跟大波动,这是ARCH效应的经典表现。这句话把四个东西串在一起:PyTorch是建模工具,量化交易是落点,股票价格波动率预测是中间目标,特征工程设计则是决定模型上限的核心环节。所以这篇内容讲的是「不预测价格,而是预测波动率」这条高频策略路径:把原始行情表变成结构化特征,再交给PyTorch模型,最后用预测出的波动率反推仓位和止损位。适合已经会跑LSTM但觉得特征总差一口气的人,也适合传统因子研究员想往深度学习迁移时直接抄一套可落地的框架。

2. 波动率特征工程的分层:从分钟K线到Tick微观结构

2.1 预测目标先定标签:已实现波动率怎么算才不歪

波动率是隐变量,模型训练前得先用一个统计量把它刻画出来。常见做法有三种:已实现波动率(Realized Volatility)、Parkinson高低价波动率、以及GARCH类模型估计。高频场景下我一般首选已实现波动率,因为它只依赖收盘价的平方收益,计算简单且没有参数估计偏差,日内重采样后天然包含高频信息。

import pandas as pd import numpy as np # 假设df是1分钟bar,包含close列,按时间升序 df["log_ret"] = np.log(df["close"]).diff() # 10个1分钟bar滚动平方和,再开方,得到10分钟已实现波动率 df["rv_10"] = np.sqrt((df["log_ret"] ** 2).rolling(10).sum()) # 换成5分钟已实现波动率,窗口调成5 df["rv_5"] = np.sqrt((df["log_ret"] ** 2).rolling(5).sum())

这段代码里最关键的是rolling(10).sum(),它把过去10个bar的平方收益加总,对应10分钟窗口的已实现方差。注意这里刻意没有做年化处理,因为在高频波动率预测里,模型学习的是相对水平和时序模式,年化只是一个常数缩放,不影响特征和标签的排序关系,反而会把数值推到极其接近0的小数区间,让PyTorch里的BatchNorm和损失函数数值都变得难调。

下表是三类波动率标签的对比,选型时可以直接按数据粒度决定:

标签类型计算公式适用粒度优点
已实现波动率sqrt(sum(r^2))1分钟及以上简单、无参数、高频信息足对零成交区间敏感
Parkinsonsqrt((1/(4ln2))*mean(ln(H/L)^2))任意OHLC利用高低价,信息量更大高低价含噪声,tick级失真
GARCH(1,1)极大似然估计条件方差日线或低频有理论基础、可预测性解释强高频拟合慢,收敛不稳

高频策略里我默认用已实现波动率做标签,Parkinson可以当作第二个预测目标做多任务学习的辅助输出,而不是替代主目标。

2.2 高频特征分四层:价格、时间、微观结构与盘口

特征工程不能一把梭把所有列都塞进模型。高频波动率预测的特征可以按来源分四层,每一层解决一类信息缺失问题。第一层是价格衍生特征,包括收益率、动量、相对强弱、距离均线的偏离度;第二层是时间特征,比如星期几、当日第几分钟、距离开盘和收盘的秒数;第三层是微观结构特征,包括买卖价差、每笔成交均额、成交笔数、主动买卖占比;第四层是盘口特征,包括委买委卖挂单量差、盘口深度、大单净流入。

实际从tick数据合成分钟bar时,微观结构特征往往比价格特征更有信息量。量化交易平台的数据接口里,tick数据通常包含bid_price,ask_price,bid_volume,ask_volume,last_price,volume这些字段,合成特征时可以这样处理:

# 从tick聚合成1分钟bar,并生成微观结构特征 tick["mid"] = (tick["bid_price"] + tick["ask_price"]) / 2 tick["spread"] = tick["ask_price"] - tick["bid_price"] tick["spread_pct"] = tick["spread"] / tick["mid"] bar = tick.set_index("time").resample("1min").agg({ "mid": "last", "spread_pct": "mean", "bid_volume": "sum", "ask_volume": "sum", "volume": "sum", "last_price": "last" }) bar["volume_imbalance"] = (bar["bid_volume"] - bar["ask_volume"]) / (bar["bid_volume"] + bar["ask_volume"] + 1e-8)

spread_pct是相对买卖价差,波动率高的时候做市商普遍扩大价差,它本身就是波动率的领先信号;volume_imbalance衡量买卖力量的失衡程度,大值往往伴随后续剧烈波动。这两个特征计算成本极低,但对PyTorch模型的边际提升通常非常明显。值得注意:合成bar时用resample("1min")默认右闭合,标签列也必须用同一套对齐逻辑,否则特征和标签错位一个周期,等于把整个模型变成了对未来信息的偷看。

2.3 特征计算里三个隐蔽的高频陷阱

高频数据上的特征工程,第一个坑是前视偏差。比如用rolling(10).mean()算均线时,pandas默认窗口包含当前bar,而标签是未来10分钟的波动率,模型看到的是「当前bar的均值」预测未来,这没问题;但如果你在t时刻用shift(-1)把下一根bar的数据挪进来,泄漏就发生了。PyTorch模型训练时loss会异常低,实盘却一塌糊涂。

第二个坑是停牌和零成交区间。A股涨跌停或临时停牌时,价格不变,已实现波动率变成0,这个0不是真实波动率,而是没有交易产生的伪信号。处理方式是在特征表里加一列「成交笔数」,并把零收益的bar标记出来,让模型自己去学「零波动+零成交」和「零波动+正常成交」的区别。

第三个坑是归一化用了全样本统计量。StandardScaler用全部训练数据拟合,等于让模型在训练时“看到”了验证集的均值和方差。正确做法是用滚动窗口拟合归一化参数,或者至少用TimeSeriesSplit折内fit。后面第四章会专门讲这个问题,因为它是特征工程做完后最容易被忽视、又最能毁掉模型的一步。

3. 用PyTorch搭建波动率预测模型:LSTM加注意力是起步配置

3.1 特征张量的构造:滑动窗口与实例归一化

特征工程产出的是逐行特征表,交给PyTorch前要切成[batch, seq_len, feature_dim]的三维张量。seq_len代表用过去多少个时间步做上下文,我一般取30分钟或60分钟。这个超参数和交易品种的波动率衰减速度有关,沪深300指数期货的波动率半衰期约20分钟,取60略保守但稳妥。

import torch from torch.utils.data import Dataset class VolatilityDataset(Dataset): def __init__(self, features, labels, seq_len=30): self.features = torch.tensor(features, dtype=torch.float32) self.labels = torch.tensor(labels, dtype=torch.float32) self.seq_len = seq_len def __len__(self): return len(self.features) - self.seq_len def __getitem__(self, idx): x = self.features[idx: idx + self.seq_len] y = self.labels[idx + self.seq_len] return x, y

这里__getitem__返回idxidx + seq_len的特征窗口,标签取窗口结束后的下一条。注意窗口内是历史,标签是未来,边界划分必须用这种「左闭右开」的切法。InstanceNorm是一个在高频场景里很好用的小技巧:对每个样本窗口单独做减均值除标准差,能消除不同时间段波动水平整体漂移的影响,等价于让模型专注学习波动形态而不是绝对数值。

3.2 LSTM加Attention的PyTorch实现

高频波动率预测模型不需要多复杂,LSTM加时间维度的注意力在绝大多数品种上都能稳定超过纯LSTM和纯Transformer的基线。注意力的作用是让模型自己决定过去60分钟里哪几个关键时刻对预测未来波动率最有用,而不是机械地把最后一个时间步当作总结。

import torch.nn as nn class VolatilityLSTM(nn.Module): def __init__(self, input_size, hidden_size, num_layers=2, dropout=0.3): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout) self.attn = nn.Linear(hidden_size, 1) self.head = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Linear(32, 1) ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) attn_w = torch.softmax(self.attn(out).squeeze(-1), dim=1) # attn_w: (batch, seq_len) context = torch.bmm(attn_w.unsqueeze(1), out).squeeze(1) # context: (batch, hidden_size) return self.head(context).squeeze(-1)

attn_w是用一个线性层把每个时间步的隐向量压成标量,再在时间维度做softmax,得到的是归一化的注意力权重。torch.bmm是批矩阵乘法,attn_w.unsqueeze(1)变成[batch, 1, seq_len],与out相乘得到[batch, 1, hidden_size]的加权和,也就是用注意力权重把所有时间步的隐状态做加权平均。hidden_size我习惯设64到128之间,超过128对分钟级数据提升微乎其微,反而增加过拟合风险。num_layers=2够捕获两层时间依赖,再深就不好训了。

3.3 训练循环与损失函数选型

训练高频波动率模型,损失函数建议用分位数损失而不是纯MSE。波动率预测在交易里的真实用途是仓位管理,低估波动率会带来超预期的亏损,高估则减少收益机会。分位数损失能分别惩罚高估和低估,让模型偏向保守。

def quantile_loss(pred, target, tau=0.7): err = target - pred loss = torch.where(err >= 0, tau * err, (tau - 1) * err) return loss.mean() model = VolatilityLSTM(input_size=X.shape[2], hidden_size=64) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): model.train() for x, y in train_loader: optimizer.zero_grad() pred = model(x) loss = quantile_loss(pred, y, tau=0.7) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() scheduler.step()

tau=0.7意味着高估误差的惩罚更轻、低估误差惩罚更重,模型会倾向于给出稍微偏高的波动率预测,这对做风险管理是安全的。weight_decay=1e-5对LSTM的权重施加轻微L2正则,能有效抑制高频噪声过拟合。clip_grad_norm_设5.0是为了防止个别极端bar产生的大梯度把整个序列学习打乱。如果发现训练loss震荡很剧烈,优先降低学习率到3e-4,而不是增大batch size去硬扛。关于PyTorch环境搭建,直接用Anaconda建一个Python 3.10的虚拟环境,CPU版本跑分钟级数据完全够用,只有数据量到Tick级且需要大量调参时才值得配GPU版本。

4. 高频量化模型的过拟合与特征穿越排查

4.1 特征穿越的四种隐蔽形态

回测曲线漂亮到不真实时,我首先怀疑的不是模型强,而是特征穿越。第一种形态是归一化泄漏:整个数据集一起fitStandardScaler,验证集的均值方差被偷看。第二种形态是标签窗口重叠:预测未来10分钟波动率时,相邻样本的标签窗口重叠了9/10,训练集和验证集之间信息高度重叠,验证误差严重虚低。第三种形态是特征使用了未来聚合值,比如把当日全天的成交量当成特征,回测时数据完整所以没问题,实盘时下午两点根本不知道全天的值。第四种形态是价格特征的shift方向搞反,把当前bar的收盘价当成下一bar的已知信息。

试想下面这段代码,它就是最经典的归一化穿越:

from sklearn.preprocessing import StandardScaler # 错误做法:全数据集上拟合 scaler = StandardScaler() X_scaled = scaler.fit_transform(X)

这个fit用的是包括验证集在内的全部数据。回测时模型的输入分布与真实分布完全一致,实盘线上数据却会因为行情水平漂移而偏离训练分布,表现断崖式下跌。正确做法是把归一化参数放进训练集拟合,并在验证集上只做transform

4.2 用手写的walk-forward验证替代KFold

时序数据不能用KFold随机打乱,这一点大家都知道,但很多人不知道标签窗口重叠的问题依然存在于按时间切分的验证里。假设预测未来10分钟波动率,验证集从第1000分钟开始,训练集到第990分钟结束,两者之间其实只隔了10分钟,训练集最后一个样本的标签窗口已经伸进了验证集区域,相当于模型回测时提前见过了一部分“未来”。

解决方法是给训练集和验证集之间加一段缓冲带,也就是embargo机制。下面这个walk-forward切分函数把缓冲显式写出来:

def walk_forward_split(n, train_len=3000, val_len=500, embargo=30): folds = [] start = 0 while start + train_len + val_len + embargo <= n: train_idx = list(range(start, start + train_len)) val_idx = list(range(start + train_len + embargo, start + train_len + embargo + val_len)) folds.append((train_idx, val_idx)) start += val_len return folds

embargo=30代表训练集末端与验证集起点之间空出30个bar,用来消化标签窗口的重叠影响。这个值必须大于等于预测窗口长度才有意义。高频数据里我通常把embargo设为预测窗口的1.5倍,比如做10分钟波动率预测时设15。验证集的评价指标要用每折独立计算后取中位数,而不是把多折的预测结果拼在一起算,否则折与折之间的重叠会再次引入数据泄漏。

4.3 按列置换做特征重要性验证

PyTorch模型不像树模型有现成的feature_importances_,但可以用置换法验证每个特征是否真正起作用,原理是:把验证集上某一列特征的时间顺序打乱,如果模型预测误差显著变差,说明模型依赖了这个特征;如果误差几乎不变,说明这列特征在模型里是废的,甚至是噪声来源。

model.eval() baseline = 0.0 with torch.no_grad(): for x, y in val_loader: baseline += quantile_loss(model(x), y).item() * len(x) baseline /= len(val_dataset) scale = torch.std(features, dim=0) importance = {} for col in range(features.shape[1]): perm = features.clone() perm[:, col] = perm[torch.randperm(perm.shape[0]), col] x_perm = build_windows(perm) loss_perm = 0.0 with torch.no_grad(): for i in range(0, len(x_perm), 128): loss_perm += quantile_loss(model(x_perm[i:i+128]), labels[i:i+128]).item() importance[col] = (loss_perm / len(x_perm)) - baseline

这段代码对特征矩阵的第col列做整列行置换,破坏特征与标签的对应关系,同时保持其他特征不变。置换发生在样本维度而不是时间维度,这是一个容易搞错的细节:如果沿时间维度整体平移,会连带破坏其他特征的时间结构,导致重要性估计失真。判断标准是相对值,重要性得分超过baseline的10%以上才算有效特征,低于5%的特征可以考虑直接删掉,减少模型输入维度能同时降低过拟合和推理延迟。

5. 波动率预测结果如何落成高频交易信号

5.1 把预测波动率映射到目标仓位与止损宽度

模型输出的预测波动率本身不是交易信号,它要通过风险预算的换算变成仓位。固定总风险预算的框架里,目标仓位与预测波动率成反比:预测波动率越高,仓位越低,这是波动率目标策略的核心思想。

target_vol = 0.10 # 年化目标波动率,按个人风险偏好设 annual_factor = np.sqrt(252 * 240) # 1分钟线一年约240个交易日 pred_vol = model.predict(last_window) # 模型输出,分钟级 position = target_vol * annual_factor / (pred_vol * np.sqrt(10)) stop_loss_pct = 3 * pred_vol

这里position是名义仓位倍率,stop_loss_pct用3倍预测波动率作为止损宽度,相当于某种动态ATR止损。预测波动率忽然放大时,仓位自动缩小,止损带宽自动放大,避免了固定止损在高波动行情里被噪声反复扫掉的问题。本质上波动率预测模型是在做风险调节器,而不是方向预测器,它的稳定收益来源于「避开大波动、吃透小波动」的复利结构。

5.2 从预测到执行的延迟预算

高频落地时,模型推理延迟直接影响交易质量。LSTM加注意力在CPU上的单条推理一般是1到3毫秒,叠加特征计算和行情推送,只要控制在一个K线周期内就不会影响策略有效性。用分钟级bar做决策时,完全不需要GPU;如果真的升级到了tick级逐笔模型,才需要考虑TensorRT或ONNX导出,把推理压缩到微秒级。验证模型是否退化的方式有两种:一是每根bar计算预测波动率和当时已实现波动率的Spearman秩相关,这个指标相对MSE对极端值更鲁棒,能更快暴露分布漂移;二是监控注意力权重的分布,如果权重从集中在某个时段变成均匀分布,说明模型已经失去了对关键波动时刻的识别能力,这时候重新训练通常比调参有效。

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

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

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

立即咨询