简介:这是一份系统讲解量化交易策略开发的53页PDF文档,面向希望借助深度学习开展股票价格预测与策略回测的开发者,完整覆盖基于PyTorch的LSTM模型构建与实战流程。压缩包内包含1个PDF文件,总大小2.33MB,文档目录支持章节跳转,也可在阅读器左侧大纲中快速定位内容。正文从量化交易与LSTM基础原理入手,逐步展开数据获取与预处理、特征工程、PyTorch模型搭建与训练优化,并延伸到股票价格预测、回测框架设计与策略评估改进,各环节均有清晰模块划分,文字图表完整可读。通过该文档可系统掌握LSTM在金融时间序列中的落地方法,也能借鉴回测框架的模块化设计思路。目前已有196人参与学习,适合量化交易入门者及有一定基础的PyTorch开发者参阅。
1. 量化交易里的LSTM:这个框架到底帮你解决了什么
股票价格预测在量化交易里一直是个充满争议的方向——用LSTM这类循环神经网络去做价格预测,很多人第一反应是“这不就是玄学吗”。但现实是,如果你把目标从“预测明天收盘价精确到小数点”换成“预测未来N根K线的涨跌方向并据此构造交易信号”,LSTM在特定行情结构下确实能挖出一些线性模型看不到的规律。这个标题里的PDF,讲的就不是一个孤立的预测模型,而是从数据清洗、序列构造、PyTorch模型训练到信号生成、回测验证的完整闭环。
这套东西适合谁?一类是已经会写Python、想从传统因子模型切到深度学习路线的量化从业者;另一类是正在做LSTM时序预测、想把它落到真实交易场景的研究生或独立开发者。它解决的核心问题有三个:数据怎么喂给LSTM、预测结果怎么变成可执行的交易信号、回测怎么设计才能不自己骗自己。下面按我自己做过的类似方案,把整条链路拆开讲。
2. 数据准备与训练环境:从K线到监督学习的三个关键转换
2.1 原始行情数据里,哪些字段真正值得喂给LSTM
很多人第一次做LSTM股票预测,直接把开盘价、收盘价、最高价、最低价、成交量五列原始数据丢进nn.LSTM,结果训练出来的模型loss低得漂亮,但一拿到样本外就崩。问题通常不在模型,而在数据没有做“可预测性”转换。
我一般会保留以下字段,但每个都先做一步加工:
close:用于计算收益率序列,而不是直接用绝对价格。绝对价格不平稳,LSTM学到的可能是“价格在某个区间”这种伪规律。high/low:用于构造波动率特征,比如(high - low) / close,反映日内振幅。volume:取对数后再做差分,或者计算5日均量/20日均量的比值,消除量纲和趋势。- 衍生特征:比如过去5日收益率、过去20日收益率、RSI的简化版、收盘价相对20日均线的偏离度。
一个常见的做法是把原始K线转换成“收益率的滚动窗口矩阵”。因为LSTM本质上学习的是序列的条件分布,输入序列的每个时间步代表一天的观测,输出是未来N天的累计收益或者涨跌方向。
2.2 归一化、序列化与数据集切分:三个容易被忽略的细节
数据处理好坏,直接决定LSTM训练是正常收敛还是loss曲线像心电图。这里三个关键步骤,每个都有坑。
第一步是归一化。LSTM内部用的是tanh和sigmoid激活函数,输入数值如果落在[-1, 1]之外太大,梯度很容易饱和。我常用的做法是sklearn.preprocessing.MinMaxScaler,但注意必须只用训练集的数据拟合scaler,验证集和测试集用同一套scaler变换。很多人图省事对整个数据集fit,这会把未来信息泄露进训练过程,回测甲结果必然虚高。
from sklearn.preprocessing import MinMaxScaler # 假设 df 包含收盘价和成交量,这里只用训练集拟合 scaler scaler = MinMaxScaler(feature_range=(0, 1)) train_data = df.iloc[:8000][['close', 'volume']].values scaler.fit(train_data) # 训练集、验证集、测试集都使用同一个 scaler 做变换 df_scaled = scaler.transform(df[['close', 'volume']].values)逻辑说明:feature_range=(0, 1)把数据压到LSTM友好的输入范围。scaler.fit只在训练集上调用,随后所有数据集共用同一组最小值和最大值。很多翻车案例的根源就在这一步——对全量数据fit后,测试集的信息已经通过scaler的统计量进入了训练阶段。
第二步是序列化,也就是把二维矩阵转成(样本数, 时间步长, 特征数)的三维张量。时间步长lookback是核心超参数,代表用过去多少天的数据预测未来。短线交易一般取5到10,中线取20到30。我自己做A股日线策略,通常设20,对应一个自然月的交易日数量。
import numpy as np import torch def create_sequences(data, lookback=20, horizon=5): X, y = [], [] for i in range(len(data) - lookback - horizon + 1): X.append(data[i:i + lookback]) # 预测未来 horizon 天的累计收益率 future_ret = (data[i + lookback + horizon - 1, 0] - data[i + lookback - 1, 0]) / data[i + lookback - 1, 0] y.append(1 if future_ret > 0 else 0) return np.array(X), np.array(y) X, y = create_sequences(df_scaled, lookback=20, horizon=5)逻辑说明:这里把预测目标定义成未来5天累计收益率是否为正的二分类问题,而不是直接回归价格。原因很简单——回归MSE再低也不代表方向判断对,而交易盈亏只取决于方向。X的形状是(样本数, 20, 2),y是(样本数,)的0/1标签。
第三步是数据集切分。时序数据不能随机打乱,否则训练集里混进未来数据,等于作弊。我常用的比例是训练集70%、验证集15%、测试集15%,严格按时间顺序切。验证集用来做早停和超参数选择,测试集只在最终评估时碰一次。
2.3 PyTorch环境搭建:从conda到GPU验证的五个命令
标题既然点名了PyTorch,环境这关躲不过。很多新手卡在环境上,不是因为难,而是因为不知道装完怎么确认装对了。下面是我在Linux服务器上从零到能训练的一套最小命令,Windows的WSL环境同样试用。
# 创建独立环境,避免把系统Python弄乱 conda create -n quant python=3.10 -y conda activate quant # 安装PyTorch,这里用CPU版起步,跑通逻辑后再上GPU pip install torch --index-url https://download.pytorch.org/whl/cpu # 验证安装与CUDA可用性 python -c "import torch; print(torch.__version__); print(torch.cuda.is_available())"逻辑说明:conda create建一个干净的Python 3.10环境,避免依赖冲突。pip install torch安装CPU版PyTorch,适合先验证代码逻辑,因为LSTM在CPU上跑小数据集完全够用。最后一行验证代码会输出版本号和False(CPU环境)或True(GPU环境)。
如果你的机器有NVIDIA显卡,想用GPU训练,建议先跑nvidia-smi确认驱动版本,再去PyTorch官网选对应CUDA版本的安装命令。需要注意,PyTorch的CUDA版本和驱动自带CUDA版本是两个东西,不必追求一致,只要驱动版本够新就能跑。
3. 基于PyTorch的LSTM预测模型:网络结构、训练循环与参数调优
3.1 LSTM为什么适合股票序列:门控机制与序列建模的关系
股票价格序列有两个显著特点:一是长程依赖,今天的走势可能受前几周甚至前几个月的市场情绪影响;二是噪声极大,单日涨跌里大部分是随机波动。传统RNN在处理长序列时会遇到梯度消失,早期信息传不到后期,所以LSTM用三个门——遗忘门、输入门、输出门——来控制信息保留和丢弃。
遗忘门决定“过去的记忆保留多少”,输入门决定“当前信息写入多少”,输出门决定“当前状态输出多少”。这套机制让LSTM在序列长度20到60的范围内能有效捕捉趋势惯性。但注意,LSTM不是万能的,它对突变事件几乎没有反应能力,比如突发利空直接跳空低开,这类样本在训练集里本质上是异常值,模型学不到规律是正常的,不要因此怀疑代码写错了。
3.2 模型定义:用PyTorch写一个可复用的LSTM预测器
下面是一份我常用的LSTM模型定义,结构很简单,但该有的组件都有:嵌入层、两层LSTM、Dropout、全连接输出层。这里用二分类输出,所以最后是sigmoid。
import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size=2, hidden_size=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x 形状: (batch, seq_len, input_size) out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size) out = out[:, -1, :] # 取最后一个时间步的输出 out = self.fc(out) # 线性映射到1维 return torch.sigmoid(out).squeeze(-1)逻辑说明:batch_first=True让输入维度从(seq_len, batch, input_size)变成更直观的(batch, seq_len, input_size)。out[:, -1, :]取序列最后一个时间步的隐藏状态作为整个序列的摘要,这是分类任务的标准做法。sigmoid把输出压缩到0到1之间,配合BCELoss使用。dropout=0.2在两层LSTM之间做随机丢弃,缓解过拟合。
这里有个参数选择的依据:hidden_size决定了记忆容量,64对日线级股票数据通常够用。num_layers=2是性价比比较高的选择,1层表达力不够,3层以上在数据量不够大的时候反而容易过拟合。input_size=2对应前面构造的两列特征——收盘价和成交量。
3.3 训练循环:早停、学习率与梯度裁剪的配合
训练LSTM最常见的两个翻车场景:loss不降和loss降到0点几以后过拟合。我现在的训练循环里固定了三样东西:早停、余弦退火学习率、梯度裁剪。
import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset # 转换数据为 Tensor 并构造 DataLoader X_t = torch.tensor(X, dtype=torch.float32) y_t = torch.tensor(y, dtype=torch.float32) dataset = TensorDataset(X_t, y_t) loader = DataLoader(dataset, batch_size=128, shuffle=True) model = LSTMPredictor() criterion = nn.BCELoss() optimizer = optim.Adam(model.parameters(), lr=1e-3) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) best_val_loss = float('inf') patience = 10 counter = 0 for epoch in range(100): model.train() for batch_X, batch_y in loader: optimizer.zero_grad() pred = model(batch_X) loss = criterion(pred, batch_y) loss.backward() # 梯度裁剪,防止 LSTM 训练中梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() # 验证集早停 model.eval() with torch.no_grad(): val_pred = model(torch.tensor(X_val, dtype=torch.float32)) val_loss = criterion(val_pred, torch.tensor(y_val, dtype=torch.float32)) if val_loss < best_val_loss: best_val_loss = val_loss counter = 0 torch.save(model.state_dict(), "best_lstm.pt") else: counter += 1 if counter >= patience: break scheduler.step()逻辑说明:batch_size=128在日线数据场景下是个稳妥的起步值,数据量大可以调到256,数据量小降到64。CosineAnnealingLR让学习率从1e-3余弦衰减到接近0,比固定学习率在后期更容易收敛到平稳区域。clip_grad_norm_是LSTM训练的关键保护——梯度爆炸在循环网络里非常常见,裁剪到1.0能避免参数直接飞掉。早停的patience=10意思是验证集连续10个epoch不改善就停止训练,同时保存验证集最优模型。
3.4 二分类 vs 回归:为什么方向预测比价格预测更实用
标题写的“股票价格预测”,但我在2.2里把它转换成了分类问题,这是有意的。在量化交易里,预测“涨3%”和预测“涨”需要的模型能力完全不同。价格回归模型对噪声极其敏感,训练集的MSE好看是因为模型学会了“预测明天价格接近今天价格”,也就是滞后预测,纯粹是把昨天的价格搬过来,毫无交易价值。
方向预测则不一样,它天然忽略幅度,只关心概率P(上涨)。实际交易中,你真正需要的是“上涨概率大于某个阈值时做多,小于某个阈值时做空或观望”,幅度预测可以交付给仓位管理模型,而不是让LSTM一个模型全包。
4. 从预测到信号:把模型输出变成可回测的交易策略
4.1 阈值选择与死区设计:不要把每个预测都变成交易
模型输出的一个0.6的上涨概率,不等于你就要开仓。实盘中,信号必须有门槛。我常用的方式是双阈值:buy_threshold设为0.65,sell_threshold设为0.35,预测值落在中间区域就空仓观望。
def generate_signal(prob, buy_th=0.65, sell_th=0.35): if prob >= buy_th: return 1 # 做多 elif prob <= sell_th: return -1 # 做空或离场 else: return 0 # 观望逻辑说明:buy_th和sell_th不是对称的,这是有意为之。上涨和下跌在A股语境下不对称(做空受限),同时模型对上涨的预测可靠性通常高于下跌,所以阈值设计不能机械地以0.5为分界。死区(0.35到0.65之间)的价值在于过滤掉低置信度的噪声信号,减少无效交易带来的手续费损耗。
这里要注意一个细节:阈值参数本身是可调的,但它的优化应该在验证集上完成,而不是测试集。否则你就是用测试集的信息去调参,回测结果自然好看。怎么判断阈值是否过拟合?把阈值从0.5到0.8按0.05步长扫一遍,如果收益对阈值变化极其敏感,说明模型输出本身就不稳定,策略不可靠。
4.2 连续信号的平滑与去抖:避免频繁开平仓的手续费吞噬
LSTM的输出是逐日预测,今天给出做多信号、明天给出观望信号、后天又给做多,这种高频抖动在实际交易里会让手续费和滑点吃掉大部分利润。我一般加一层“确认机制”:连续N天信号一致才实际开仓。
def confirm_signal(signal_series, confirm_days=2): s = signal_series.copy() confirmed = s * 0 streak = 0 for i in range(len(s)): if s[i] == 0: streak = 0 continue if s[i] == s[i-1]: streak += 1 else: streak = 1 if streak >= confirm_days: confirmed[i] = s[i] return confirmed逻辑说明:confirm_days=2意味着连续两天的LSTM信号方向一致才确认开仓。这个思路来源很简单,LSTM单个样本的预测方差很大,但连续两天都输出同一个方向,说明模型对当前行情状态的判断比较稳定。代价是会错过第一天的行情启动,但换来的好处是大幅减少假信号。短线策略可以把confirm_days降到1,中长线设2到3比较合理。
4.3 从信号到持仓:头寸管理与止损止盈的朴素搭配
信号确认之后,还需要一套简单的资金管理规则。完整的仓位管理是很大的一篇文章,但这里至少要解决净值曲线波动过大的问题。我常用的做法是固定比例仓位加移动止损:每次开仓使用总资金的20%,止损线设在入场价下方3%到5%的位置,止盈可以设盈亏比2比1。
这套组合的意义在于:LSTM预测的方向准确率一般也就是55%到65%之间,胜率并不是压倒性的。能在长期赚钱,靠的是“亏的时候亏小钱,赚的时候赚大钱”。如果你不做仓位控制,一次预测失误就可能把前面十次盈利全部吐回去。
5. 回测框架搭建与避坑:验证策略有效性的完整路径
5.1 自建回测还是用backtrader:取舍标准与最小实现
LSTM预测结果要验证策略有没有效,回测框架是绕不开的。社区常用的是backtrader,它的优点是有现成的订单撮合、滑点模型和绩效统计模块,缺点是文档不够系统,很多功能要靠翻源码才能搞明白。我的建议是:信号生成逻辑复杂、需要频繁和模型交互的场景,自建一个轻量回测更灵活;信号逻辑简单、主要看绩效指标的,backtrader完全够用。
这里给一个backtrader跑LSTM信号的最小示例,重点是讲清楚“模型在外部算好信号,backtrader只负责撮合和统计”。
import backtrader as bt class LSTMSignalStrategy(bt.Strategy): def __init__(self): # signal_df 是外部 DataFrame,包含模型产出的信号列 self.signal = self.datas[0].signal self.order = None def next(self): if self.order: return # 当前K线对应的信号值 sig = self.signal[0] if sig == 1 and not self.position: self.buy(size=0.2 * self.broker.cash / self.datas[0].close[0]) elif sig == -1 and self.position: self.close()逻辑说明:size=0.2 * self.broker.cash / 当前价格实现的是单次投入20%资金的规则。self.position判断当前是否有持仓,避免重复开仓。backtrader的next方法在每个bar上被调用,self.datas[0].close[0]是当前bar的收盘价。
需要指出的是,backtrader默认使用下一个bar的开盘价成交,这是为了防止用当根K线的收盘价信号、同根K线成交这样不可能发生的“未来交易”。很多第一次写回测的人在这里翻车——信号和成交在同一根K线上完成,相当于偷看了未来。
5.2 三个必看的绩效指标:收益曲线之外的真实信号
回测跑完之后,不要只看最终收益率和最大回撤。我每次做策略评估,至少看三个指标,每个都会暴露一类问题:
年化收益率与最大回撤的比值,也就是卡玛比率。如果收益30%但最大回撤40%,这种策略毫无意义。卡玛比率至少大于1,才算风险和收益匹配。
每笔交易的平均盈亏比与胜率的乘积。胜率60%、盈亏比0.8的策略,长期是亏钱的(期望为负),因为赚的时候赚得少、亏的时候亏得多。这个指标能快速区分策略是“真的会选方向”还是“靠运气”。
交易次数。回测一年只产生3次交易,就算收益翻倍也不可信,样本太少说明策略失效的可能性极高。我一般要求至少30次交易,统计结果才有参考价值。
5.3 避坑:LSTM回测中常见的五个陷阱
第一条是前视偏差。现象:回测曲线完美上涨,实盘一塌糊涂。原因:数据预处理或信号生成里混入了未来信息,比如用当天收盘价计算信号,却用当天收盘价成交。解决:严格按信号在T日收盘产生,T+1日开盘成交的规则设计回测。这也是我上面backtrader示例里用next bar撮合的原因。
第二条是幸存者偏差。现象:回测选取的股票池表现很好,但实盘里选不出这些股票。原因:股票池用的是当前仍在上市交易的股票,退市的股票被排除了。解决:如果你的策略要选出所有股票,股票池必须是“当时存在”的股票,包含后来退市的。
第三条是过拟合。现象:训练集和验证集表现很好,测试集崩掉。原因:超参数调得太多——lookback、hidden_size、学习率、阈值,每个参数都像在验证集上试了很多次,验证集的信息被榨干了。解决:把超参数搜索限定在合理范围内,比如lookback只试{10, 20, 30}三个值,hidden_size只试{32, 64, 128},减少组合数。更重要的是,最终测试集只跑一次,无论结果好坏都不回去改参数。
第四条是交易成本设置过低。现象:回测年化50%,实盘年化只有10%。原因:手续费和滑点设置得太乐观,A股双边手续费加滑点大约万五到千一,加密货币更高。解决:回测中把手续费设为万三到万五,滑点按成交价的0.1%到0.2%计提,看看策略还能不能赚钱。
第五条是信号与执行脱节。现象:回测中信号是收盘价产生的,但实际下单有延迟,导致成交价和信号价格差很多。解决:在回测里引入“信号产生后延迟一根K线执行”的机制,看净值曲线变化多大。如果延迟一根K线就大幅亏损,说明策略逻辑本身可能依赖了过强的短期动量,这类策略实盘极不稳定。
5.4 用walk-forward验证避免过拟合幻觉
除了常规的train/val/test切分,量化策略还有一个更贴近实战的验证方法——滚动前推(walk-forward)。它的做法是:用第1到第12个月的训练集训练模型,第13个月验证;然后滑到第2到第13个月训练,第14个月验证,如此滚动下去。每一步都模拟了真实交易中“用过去数据预测未来”的体验。
滚动前推的美妙之处在于,它测试的不是模型在某一段行情上的表现,而是在连续多段时间切片上的稳定性。如果一个策略在一半以上的时间段里是亏损的,只在某一两段行情里赚了大钱,说明它大概率是依赖特定市场风格的,而不是普遍规律。
6. 落地前的最后一步:从回测到模拟盘的三个关键检查
回测做得再漂亮,最后都要过“模拟盘”这一关。我自己的流程是:回测通过后,先跑至少两周的模拟盘,期间不只看收益,还要检查三件容易被忽略的事情。
第一是模型推断延迟。LSTM在回测中预测一次只需几毫秒,但在实盘环境里,数据获取、预处理、模型加载、推理、下单这一整套链路可能耗时几百毫秒甚至几秒。对于日线级别的策略,这个延迟可以接受,但需要确认你的调度框架能稳定地在每个交易日收盘前完成整个流程。我遇到过的情况是:每天早上9点15分开始跑预测,跑到9点25分才出信号,结果开盘价已经变了,和回测假设完全脱节。
第二是数据源的稳定性。回测用的历史数据是静态的,但实盘的数据源可能有缺失、延迟和错误。一个常见的坑是:复权因子在除权除息日会发生跳变,如果程序没做处理,模型的输入特征会瞬间出现异常值。建议在模拟盘阶段就接入实盘数据源,并且写一个数据质量检查脚本,每天跑完记录数据行数、最新日期、价格是否在合理范围内。
第三是模型更新的频率。LSTM不是训练一次就能用到天荒地老的,市场风格会变。我习惯每周用最新的数据增量训练一次,但如果数据量增长缓慢,也可以只在验证集loss持续上升时才触发重训。
最后说一个我自己的经验:不要把LSTM预测当作圣杯,它只是整个策略系统里的一个信号源。真正稳定赚钱的系统,靠的往往不是预测准,而是亏的时候控制得住、赚的时候跟得住。把预测模型、仓位管理、回测验证三块拼完整,才是这个标题里那套框架真正想让你掌握的技能。希望帮到你。
本文还有配套的精品资源,点击获取