ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

量化交易策略开发:PyTorch构建LSTM预测模型与回测框架实战

量化交易策略开发:PyTorch构建LSTM预测模型与回测框架实战 简介《量化交易策略开发基于PyTorch的LSTM股票价格预测与回测框架》是一份面向量化交易入门与进阶开发者的系统教程聚焦PyTorch在金融时间序列建模中的应用。文档结合PyTorch动态计算图优势从实战角度讲解LSTM股票价格预测与策略回测的完整流程涵盖数据获取与清洗、缺失值和异常值处理、特征工程与归一化标准化到LSTM网络结构设计、训练循环构建、损失函数与优化器选择、正则化与早停策略以及预测结果可视化和回测框架的模块化实现。内容还深入展开回测绩效评估体系包括总收益率、最大回撤、波动率、夏普比率等指标助读者理解策略优劣与改进方向。资源为53页PDF单文件压缩包2.33MB带完整目录书签支持章节跳转与左侧大纲快速定位。已有196人学习下载适合希望用PyTorch搭建自有量化回测框架的技术人员参考学习。1. 量化交易策略开发的起点为什么偏偏是LSTM PyTorch 回测框架做量化交易策略开发的人早晚会撞上同一个问题手上有一堆历史行情数据想用深度学习预测股票价格走势模型训出来回测也挺漂亮可真要敢把钱投进去又总觉得某个环节不可控。这个标题把三件事焊在一起——用PyTorch搭LSTM模型做股票价格预测再套一个回测框架验证策略能不能落地。它本质上是在回答一个很务实的问题预测结果到底能不能转化成可执行的交易策略以及这个转化过程里哪些环节最容易自欺欺人。适合读这篇文章的人是那种已经会用Python取数、看过几篇LSTM教程、但还没完整跑通“数据→训练→预测→回测”闭环的从业者。新手能照着把链路搭起来熟手可以重点看第五章的坑和第六章的滚动验证。这里先给一个反直觉的结论股票价格预测最难的不是模型精度而是你把预测结果当成信号的那一刻——信号生成、手续费、前视偏差任何一环出错回测收益都可能是幻觉。2. 先处理数据再谈模型价格序列清洗、归一化与滑动窗口构建2.1 数据源怎么选tushare / akshare / baostock 的取舍做LSTM股票价格预测第一步不是写模型而是把可靠的历史数据拿到手。国内常见做法是用tushare、akshare或baostock三者定位不太一样tushare需要注册获取token积分越高接口越全日线数据稳定适合做研究akshare免费、接口多但字段命名乱爬虫失效要频繁更新baostock无需token、数据免费行情接口返回的是pandas DataFrame写起来最顺手。我一般在本地实验用baostock因为它免注册、上手快专门针对A股设计后复权数据直接给到省去自己复权计算的麻烦。取数的代码比较固定import baostock as bs import pandas as pd # 登录baostock lg bs.login() # 取平安银行(000300)从2020年到2023年的日线数据 rs bs.query_history_k_data_plus( sz.000300, date,code,open,high,low,close,volume,amount,turn, start_date2020-01-01, end_date2023-12-31, frequencyd, adjustflag2 # 2代表后复权1代表前复权3代表不复权 ) # 把结果转成DataFrame data_list [] while (rs.error_code 0) rs.next(): data_list.append(rs.get_row_data()) df pd.DataFrame(data_list, columnsrs.fields) bs.logout()复权参数是关键。训练LSTM时如果直接用不复权价格遇到除权除息日会出现价格跳空模型会把这当作真实的价格变动去学习预测自然偏离。后复权以最早价格为基准调整价格曲线连续更适合建模前复权以最新价格为基准适合看当前价附近的图形。做模型训练我统一用后复权回测时再换算成实际成交价。字段方面close是收盘价volume是成交量turn是换手率。LSTM做价格预测常用的输入不只是收盘价还会拼上开盘价、最高价、最低价、成交量和换手率让模型自己学价格之间的关系。取数之后要做一件事把空值处理掉。A股停牌日不会返回数据直接用前向填充会引入未来信息最稳妥的做法是停牌日直接删除不补。2.2 归一化与序列切分训练集、验证集、测试集不能混着来LSTM对输入数据的尺度很敏感。收盘价如果是几十块成交量是几百万股数值范围差好几个数量级梯度更新会被大数值特征主导。常见做法是做MinMax归一化把每列压缩到0到1之间from sklearn.preprocessing import MinMaxScaler # 只对数值列做归一化日期列不参与 feature_cols [open, high, low, close, volume, turn] scaler MinMaxScaler(feature_range(0, 1)) scaled_data scaler.fit_transform(df[feature_cols]) # 转成DataFrame保持列名 scaled_df pd.DataFrame(scaled_data, columnsfeature_cols)这里有一个新手最容易踩的坑fit_transform必须只用在训练集上验证集和测试集要用训练集的scaler做transform不能再fit一次。因为MinMaxScaler的fit会记录每列的最小值和最大值如果测试集参与fit模型在训练阶段就“见过”测试集的统计信息这属于数据泄露。更准确的做法是先把数据按时间顺序切好再在训练集上fit然后transform三个集合。也就是先切分再归一化顺序不能反。另外一个常见坑是不知道什么时候归一化。你训练LSTM预测的是归一化之后的价格预测结果要反归一化才能算收益、画图对比。要把scaler的inverse_transform接口留好不然预测值和真实价格不在一个尺度上后续回测没法做。建议把scaler对象用joblib存下来回测阶段预测时要复用。2.3 用PyTorch的Dataset组织样本窗口长度、步长与批量加载LSTM看的是序列不能像普通回归那样随机抽一行。要给模型构造“过去N天→未来M天”的样本对。窗口长度是个玄学参数太短学不到中期趋势太长引入过多噪声且训练变慢。做日线预测我一般先试20到60天窗口数据量够的话直接上60。预测目标可以设成“未来1天的收盘价”也可以设成“未来5天的累计涨跌幅”后者更平滑但信号滞后也更明显。用PyTorch组织样本的标准做法是继承torch.utils.data.Datasetimport torch from torch.utils.data import Dataset class StockDataset(Dataset): def __init__(self, data, seq_len60, pred_len1): # data是归一化后的二维数组每行是一个交易日 self.data torch.FloatTensor(data) self.seq_len seq_len self.pred_len pred_len def __len__(self): # 样本数量 总行数 - 窗口长度 - 预测长度 1 return len(self.data) - self.seq_len - self.pred_len 1 def __getitem__(self, idx): x self.data[idx: idx self.seq_len] # 过去60天 y self.data[idx self.seq_len: idx self.seq_len self.pred_len, 3] # 未来收盘价 return x, y这里列索引3对应close列所以在构造feature_cols时要把close放在第三列的位置一旦调整列顺序索引要同步改。更稳妥的做法是先定义一个close_idx feature_cols.index(close)不要硬编码数字。计算样本数量时len里要同时减去seq_len和pred_len再用DataLoader打乱批量加载from torch.utils.data import DataLoader dataset StockDataset(train_scaled, seq_len60, pred_len1) dataloader DataLoader(dataset, batch_size128, shuffleTrue, num_workers0)注意shuffle只在训练集上开启验证集和测试集必须shuffleFalse因为序列预测要按时间顺序评估打乱会让时间关系失效。num_workers在Windows上设大于0偶尔会报错本地调试先设为0Linux服务器再开多进程。3. 用PyTorch搭建LSTM预测模型从网络结构到训练参数3.1 LSTM单元的输入输出:hidden_size、num_layers怎么定LSTM处理时间序列的核心优势在于门控机制它通过输入门、遗忘门、输出门控制信息的保留与丢弃相比普通RNN能更长时间地记忆序列中的依赖关系。在PyTorch里nn.LSTM接收三维张量形状是(seq_len, batch_size, input_size)返回输出序列和最后一步的隐状态。import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, input_size6, hidden_size64, num_layers2, dropout0.2): super(LSTMPredictor, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, # 输入形状变成(batch, seq_len, input_size) dropoutdropout # 多层LSTM时dropout才生效 ) self.fc nn.Linear(hidden_size, 1) # 只输出未来收盘价 def forward(self, x): out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) last_out out[:, -1, :] # 取最后一个时间步的输出 pred self.fc(last_out) # (batch, 1) return predinput_size是特征数量hidden_size是隐状态维度num_layers是堆叠的LSTM层数。hidden_size太小拟合能力不足太大容易过拟合且训练变慢经验区间是32到128。num_layers建议2层起步1层表达能力弱超过3层收益递减且训练不稳定金融时序数据本身噪声极大堆太深只会过拟合到历史噪声上。batch_firstTrue是个容易忽略的细节设置之后输入直接传(batch, seq_len, features)不用再手动permute。dropout只在num_layers1时生效单层设了也没用这点和很多人直觉相反。3.2 损失函数、优化器与学习率:预测涨跌幅比预测价格更稳LSTM回归预测的常见损失函数是MSE但用在股票价格上有两个问题一是价格绝对值大MSE数值大且对异常值敏感二是模型学到的是价格水平而不是价格变化。实践中更稳的做法是预测未来涨跌幅把目标从收盘价改成(未来收盘价 / 当前收盘价 - 1)模型学的是相对变化尺度更小更平稳。import torch.optim as optim model LSTMPredictor(input_size6, hidden_size64, num_layers2, dropout0.2) criterion nn.MSELoss() optimizer optim.Adam(model.parameters(), lr0.001) scheduler optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.5)优化器首推Adam它对学习率不敏感适合金融这类噪声数据。如果追求极致精度可以等模型收敛后切到SGD微调但对股票预测来说收益有限。学习率初始值0.001是PyTorch默认值对新模型多数情况下能正常收敛但最好跑20个epoch看loss曲线再决定。StepLR是简单好用的学习率衰减策略每20个epoch把学习率减半后期用更小步长做精细调整。训练循环写成标准的PyTorch流程epochs 100 for epoch in range(epochs): model.train() train_loss 0.0 for x_batch, y_batch in dataloader: optimizer.zero_grad() pred model(x_batch) # 预测涨跌幅 loss criterion(pred.squeeze(), y_batch) loss.backward() optimizer.step() train_loss loss.item() * x_batch.size(0) scheduler.step() avg_loss train_loss / len(dataloader.dataset) if (epoch 1) % 10 0: print(fEpoch {epoch1:3d} | Loss: {avg_loss:.6f})每次迭代先zero_grad清空上一步的梯度再前向计算、反向传播、更新参数这个顺序不能乱。如果loss在训练初期就出现NaN优先检查输入数据里是否有NaN或Inf其次是学习率太大。MSE loss如果输出层没有做激活函数限制预测值可能超出归一化范围需要后续做截断或反归一化时处理。3.3 训练曲线怎么看:过拟合的早期信号与早停训练LSTM最需要盯的是训练loss和验证loss的差距。理想情况是两条曲线一起下降并趋于平缓如果训练loss持续下降但验证loss先降后升说明模型开始把训练集里的噪声当规律记下来了也就是过拟合。股票价格预测里过拟合几乎是必然的因为价格本身是低信噪比信号模型很容易记住历史走势而不具备泛化能力。缓解过拟合的常见做法是加dropout但它的作用被很多人高估了。在LSTM里dropout加在层与层之间对最后那个全连接层没有约束。我给nn.LSTM加了dropout之后还会在self.fc之前加一个nn.Dropout(0.2)实测比只靠LSTM自带dropout更有效。另一个是早停即验证loss连续N个epoch不降反升就提前终止训练best_val_loss float(inf) patience 15 wait 0 for epoch in range(epochs): # ... 训练代码 ... val_loss evaluate(model, val_loader) # 自定义验证函数 if val_loss best_val_loss: best_val_loss val_loss wait 0 torch.save(model.state_dict(), best_model.pth) else: wait 1 if wait patience: break这里patience设15含义是验证loss连续15个epoch没有创新低就停止。早停期间要不断把最优模型保存下来而不是训练结束时才保存否则拿到的是已经过拟合的版本。验证集评估时记得切到model.eval()模式并且用torch.no_grad()关掉梯度计算否则会多占不少显存。4. 把预测转成策略回测框架的信号生成、手续费与绩效指标4.1 信号生成与仓位管理连续预测值如何变成买卖动作模型输出是连续的涨跌幅预测值不能直接拿来做交易。你需要一套规则把连续值映射成离散的仓位信号。最常见的做法是阈值法预测涨跌幅大于某个正阈值时开多小于负阈值时开空或清仓在阈值中间则维持原有仓位。阈值的作用是过滤掉模型没有把握的预测避免频繁交易。def generate_signals(pred_changes, buy_threshold0.005, sell_threshold-0.005): signals [] position 0 # 0为空仓1为满仓 for pred in pred_changes: if pred buy_threshold: position 1 elif pred sell_threshold: position 0 signals.append(position) return pd.Series(signals, indexpred_changes.index)用预测涨跌幅本身做信号比用预测价格更合理因为阈值是相对变化量对不同价格的股票都适用。如果预测的是绝对价格不同股票的阈值完全不同策略就没有迁移性。阈值取0.5%对应0.005是一个常见起点A股印花税加佣金单边成本大约在0.1%到0.3%之间阈值至少要覆盖交易成本的3倍以上否则策略的毛收益覆盖不了摩擦成本。更进阶的做法是仓位连续化预测值越大仓位越高用一个sigmoid或线性函数映射def position_from_pred(pred, min_pos0.0, max_pos1.0, scale100): # 用tanh把预测值映射到-1到1再线性变换到仓位区间 return min_pos (max_pos - min_pos) * (torch.tanh(torch.tensor(pred) * scale) 1) / 2这种仓位管理和阈值法不是二选一的关系而是两种思路阈值法适合把模型当交易信号发生器仓位连续化适合把预测值当仓位权重。对A股这种T1且不支持做空的场景模型只做多或空仓不做空空头信号全部映射成清仓。4.2 向量化回测与逐bar回测收益计算、滑点与手续费回测的本质是回答一个问题按照这个信号序列每天调仓账户资金会怎么变化。最基础的是向量化回测用pandas一次性算出持仓收益速度快但精度低适合快速验证策略# returns是每日收益率序列, signals是前一日生成的目标仓位 # 注意第t天用第t-1天收盘后的信号在实际交易中第t天开盘执行 strategy_returns signals.shift(1) * returns strategy_returns strategy_returns.fillna(0) # 策略净值曲线 equity_curve (1 strategy_returns).cumprod()这里的shift(1)是整个回测里最重要的一步它保证信号在第二天才生效避免使用当天收盘数据“预知”当天收益。如果不shift信号和收益同一天对齐回测会严重虚高这就是所谓的前视偏差。向量化回测里还要考虑手续费和滑点A股佣金约万2.5到万3卖出额外收千1印花税滑点每笔按成交价的千1到千2估算。费用实际是交易次数驱动的可以在每次仓位变化时扣除trade_cost 0.0013 # 单边买卖成本合计 position_change signals.diff().abs().fillna(0) # 仓位变化幅度 cost position_change * trade_cost net_returns strategy_returns - cost逐bar回测更贴近真实交易以每日为一个bar在每个bar结束时生成信号下一个bar的开盘价成交。它能更精细地处理涨跌停无法成交、停牌无法卖出等情况但代码量更大。建议先跑向量化回测判断策略方向是否正确再对值得深挖的策略写逐bar回测不必一上来就上重武器。4.3 绩效指标怎么算年化、回撤、夏普与换手率生成净值曲线之后要用几个指标量化策略质量。这些指标直接决定策略值不值得继续优化。下面是四个最基础也最常用的指标指标含义计算方式参考标准年化收益率策略按年折算的收益(最终净值)^(252/交易日数) - 1至少跑赢基准指数最大回撤净值从峰值跌落的幅度max(峰值 - 谷值) / 峰值越低越好超过30%心态容易崩夏普比率承担单位风险获得的超额收益(策略年化收益 - 无风险利率) / 年化波动率大于1才值得继续投入换手率平均多少天换一次仓总成交金额 / 持仓市值 / 天数太高说明信号频繁抖动指标计算代码import numpy as np def performance_metrics(net_returns, risk_free_rate0.02): equity (1 net_returns).cumprod() total_days len(net_returns) # 年化收益 annual_return equity.iloc[-1] ** (252 / total_days) - 1 # 最大回撤 running_max equity.cummax() drawdown (equity - running_max) / running_max max_drawdown drawdown.min() # 夏普比率 excess net_returns - risk_free_rate / 252 sharpe np.sqrt(252) * excess.mean() / excess.std() return { annual_return: annual_return, max_drawdown: max_drawdown, sharpe: sharpe, }注意excess.std()如果标准差为0说明策略一直在赚取固定收益显然不合理但要检查是不是信号全为常数。换手率单独算turnover (signals.diff().abs().sum()) / len(signals) # 日均换手率 annual_turnover turnover * 2525. 回测与模型脱节的5个坑数据泄露、前视偏差与参数过拟合5.1 归一化泄露全样本归一化让测试集提前看到未来现象训练集和测试集一起做MinMaxScaler.fit_transform模型在训练期间“提前看到”了整个数据区间的最大值和最小值预测精度虚高10%以上。原因股票价格的分布随时间变化。如果测试集里的最高价参与了归一化测试集的范围就被提前写入了训练数据等于考试时不小心瞄到了后半张试卷的答案分布。尤其是价格创新高的阶段测试集的最大值会撑大归一化区间导致训练数据被压缩到很窄的范围内。解决严格按时间顺序划分数据先切分再归一化。训练集fit之后验证集和测试集只用transformtrain_df, val_df train_test_split_by_time(df, test_size0.2) # 自定义按时间切分 scaler.fit(train_df[feature_cols]) train_scaled scaler.transform(train_df[feature_cols]) val_scaled scaler.transform(val_df[feature_cols])要检查代码里是否只有一处fit_transform其余全是transform。5.2 前视偏差当天的预测信号当天用等于开卷考试现象回测曲线极其完美净值稳步上升看不到回撤但实盘完全跑不出同样结果。原因信号和收益用了同一天的数据。模型用第t天的收盘价预测第t1天的涨跌如果回测在第t天就用这个信号交易并获取第t天的收益等于用了未来的信息。这个偏差在LSTM框架里很容易发生因为模型天然用过去N天预测未来但回测时信号生成和收益对齐的时间轴容易搞混。解决生成信号后强制shift(1)或者更彻底一点只用第t天收盘前完整可用的数据生成信号t1日开盘成交。我在回测里有一个检查习惯单独打印信号和收益率的前10行确认信号首日为0、次日才开始有收益肉眼确认过了再跑完整回测。5.3 策略参数过拟合在训练集上调到完美实盘必翻车现象把回测参数窗口长度、隐藏层大小、买卖阈值、止损线反复调直到训练集期间净值曲线漂亮到没有回撤但一换到样本外时间段绩效断崖式下跌。原因模型把训练期的特定走势特征当成普遍规律参数只在历史区间有效。这本质上是多重比较问题——参数组合越多找到一组恰好拟合历史的概率就越高跟彩票盲注次数多了总有一次中奖一个道理。解决按时间滚动划出训练集、验证集、测试集验证集用来调超参数测试集只在最终评估阶段用一次。如果资金和时间允许做滚动窗口验证把整个时间段切成K段轮流做训练和测试把所有测试段的结果拼接成完整的净值曲线。这是检验策略是否过拟合最有效的手段也直接对应第六章要讲的滚动回测。5.4 多步预测误差累积滚动预测的衰减与应对现象模型预测未来1天很准但把预测结果作为输入再预测下一天时误差像滚雪球一样快速放大预测曲线很快衰减成一条直线。原因LSTM是自回归结构多步预测时把上一步的输出当输入误差会自我强化。股票价格本身有均值回归特性预测值会逐步向训练集的均值靠拢。解决两种思路。第一是单步滚动每预测一天就加入真实观测值重新构造输入窗口再预测下一天第二是直接训练多步预测模型输出层改成pred_len个神经元让模型一次预测未来多天。单步滚动适合研究多步预测适合实盘信号生成后者代码要改模型输出层和损失函数class LSTMMultiStep(nn.Module): def __init__(self, input_size6, hidden_size64, num_layers2, pred_len5): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.fc nn.Linear(hidden_size, pred_len) # 一次输出5天预测 def forward(self, x): out, _ self.lstm(x) last out[:, -1, :] return self.fc(last)5.5 涨跌停与停牌无脑成交的回测收益不可信现象回测里策略在某只涨停股上精准买入第二天继续涨停卖出收益惊人但实盘当时根本买不进。原因回测假设所有信号都能按收盘价或开盘价成交忽略了A股涨跌停板制度。涨停时卖单稀少买不进跌停时买单稀少卖不出这些交易日在回测里被当成正常的可成交bar。解决逐bar回测时必须加成交约束。信号触发当天检查是否涨停或跌停是则不执行该笔交易并把这个限制写进回测引擎# 简化判断当日涨幅超9.8%视为涨停买入无法成交 limit_up (df[close].pct_change() 0.098) signals[limit_up] 0 # 涨停日买不进同样跌停日无法卖出也需要处理。建议至少过滤掉明显的涨停买入、跌停卖出这两种情况等量化回测框架跑通后再补充流动性和停牌过滤。6. 进阶把单次预测改成滚动预测用滚动回测验证“真的能赚”单次训练后在一个固定测试集上跑回测最多只能证明模型在那个历史区间内有效。真正能说明策略还有生命力的是滚动回测用不断更新的数据训练模型在样本外时间段验证模拟出实盘中定期重训的操作方式。实现思路是定义两个窗口训练窗口和测试窗口。比如训练窗口取500个交易日测试窗口取60个交易日每次用最近500天数据训练在下一个60天里做预测和回测然后把训练窗口向前滚动60天反复执行def rolling_backtest(df, train_window500, test_window60): results [] start 0 while start train_window test_window len(df): train_data df.iloc[start: start train_window] test_data df.iloc[start train_window: start train_window test_window] # 训练LSTM模型 model train_lstm(train_data) # 内部包含归一化、Dataset构建、训练 # 在测试窗口上逐日滚动预测 preds predict_rolling(model, test_data) # 用已知数据滚动预测下一个交易日 # 生成信号并计算这段回测的收益 segment_returns backtest_segment(preds, test_data) results.append(segment_returns) # 窗口前移 start test_window # 拼接所有段的净值曲线 full_equity pd.concat(results).cumprod() return full_equity滚动回测的计算量不小500天训练一个LSTM再预测60天循环十几轮CPU机器上可能要跑几个小时。优化思路有两种一是先用小模型hidden_size32、num_layers1快速跑通全流程确认收益方向正确后再放大模型二是把训练轮数从100降到30减少等待时间。滚动回测的核心价值不是精确评估收益而是验证策略在不同市场阶段牛市、熊市、震荡市下是否都能保持正收益。如果策略只在某一类行情中有效滚动回测会诚实地把这个弱点暴露出来。做完滚动回测还应该做一个最朴素的验证——和基准比。把策略净值曲线和沪深300指数放在同一张图上算超额收益和相对回撤。如果策略净值涨了50%但沪深300同期涨了80%那策略实际是跑输基准的投入产出比就值得重新考虑。我的习惯是先看滚动回测的最大回撤是否超过30%、夏普是否大于1两项都达标才考虑下一步不达标就回到特征工程和信号阈值上找原因而不是继续堆模型复杂度。LSTM在量化交易里的定位从来不是圣杯它更像一个信号发生器能不能赚钱取决于信号背后的策略逻辑和风险管理。把预测、信号、回测三层分开验证才不会被一个好看的净值曲线骗了。希望帮到你。本文还有配套的精品资源点击获取
返回列表