news 2026/10/2 2:36:53

基于LSTM的股票价格预测与量化策略实战:从数据到回测的完整链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于LSTM的股票价格预测与量化策略实战:从数据到回测的完整链路

简介:这份资源是面向计算机相关专业学生与项目实战学习者的深度学习股票价格预测与量化策略研究完整项目,源自大四毕业设计,经导师指导并获99分评审认可。内容涵盖股票价格预测模型构建与量化策略实现,适合作为毕业设计、课程设计或期末大作业的参考方案,也便于初学者按完整流程复现与练习。压缩包共1409个文件,约16.2MB,以636个py源码与671个pyc编译文件为主体,另含pptx报告、h5模型、csv预测结果、xlsx数据表及txt说明文档,并附带exe、dll等运行依赖,目录结构完整、代码可运行。目前已有247人学习下载。读者可获得可运行的源码工程、配套文档说明与答辩PPT,理解数据预处理、模型训练、预测输出与策略评估的完整链路,并借助现成脚本与结果文件快速排错、二次开发,降低从零搭建量化项目的门槛。

1. 从一份高分项目说起:深度学习做股票预测到底靠不靠谱

很多人第一次看到「基于深度学习的股票价格预测和量化策略研究」这类项目标题,第一反应是:这不就是拿 LSTM 预测明天涨跌吗,能有多难。真动手做过的人都知道,难点从来不在模型本身,而在于你把什么问题喂给了模型。股票价格序列是非平稳、低信噪比、带强噪声的时间序列,用深度学习直接回归收盘价,几乎必然得到一个「滞后一天」的曲线——看着拟合得很好,实盘一用就废。这个项目真正有价值的地方,是把「价格预测」和「量化策略」串成了一条完整链路:数据获取与清洗、特征工程、深度学习模型训练、预测信号转成买卖规则、回测评估、报告输出。它适合两类人:一类是刚学完 python 和深度学习、想找一个能写进简历的完整项目练手的学生;另一类是想把量化交易策略从「拍脑袋指标」升级到「数据驱动」的从业者。下面我按自己复现这类项目的顺序,把每一步讲透,包括参数怎么设、哪里容易翻车。

2. 数据、特征与标签:决定项目上限的三件事

2.1 数据从哪来,以及为什么不能只用收盘价

常见做法是用 akshare 或 tushare 拉 A 股日线数据,前者免费、接口稳定,适合学生项目。核心字段至少要有:开盘、最高、最低、收盘、成交量、成交额、换手率。只喂收盘价给模型,等于把 90% 的信息扔了。我一般会构造三类特征:价格类(收益率、对数收益率、高低价差、收盘价在当日区间的相对位置)、成交量类(量比、成交量移动平均偏离、换手率)、技术指标类(MA、EMA、MACD、RSI、布林带宽度)。注意技术指标要用滚动窗口计算,绝不能用未来数据。

import akshare as ak import pandas as pd import numpy as np # 拉取单只股票日线,adjust="qfq" 表示前复权,避免除权跳空污染序列 df = ak.stock_zh_a_hist(symbol="600519", period="daily", start_date="20180101", end_date="20231231", adjust="qfq") df = df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume", "换手率": "turnover" }) df["date"] = pd.to_datetime(df["date"]) df = df.sort_values("date").reset_index(drop=True) # 对数收益率:比简单收益率更接近正态,模型更稳 df["log_ret"] = np.log(df["close"] / df["close"].shift(1)) # 收盘价在当日高低区间的位置,衡量当日强弱 df["close_pos"] = (df["close"] - df["low"]) / (df["high"] - df["low"] + 1e-9) # 量比:当日成交量与过去5日均量之比 df["vol_ratio"] = df["volume"] / df["volume"].rolling(5).mean() # 布林带宽度,衡量波动率状态 ma20 = df["close"].rolling(20).mean() std20 = df["close"].rolling(20).std() df["boll_width"] = (4 * std20) / (ma20 + 1e-9) df = df.dropna().reset_index(drop=True) print(df[["date", "close", "log_ret", "close_pos", "vol_ratio", "boll_width"]].tail())

这段代码的关键参数有三个:adjust="qfq"必须开,否则除权日的价格断层会被模型当成真实暴跌;滚动窗口rolling(5)、rolling(20)是经验值,短线用 5、中线用 20,不要用全样本均值,那是未来函数;dropna()会砍掉前 20 行,这是正常的,因为指标需要预热期。跑完先看log_ret的分布,均值为 0 附近、有厚尾才算正常,如果均值明显偏离 0,说明复权或数据源有问题。

2.2 标签怎么定义,直接决定模型在学什么

这是最容易翻车的地方。如果你把标签设成「明天的收盘价」,模型会学成回归问题,输出一条平滑曲线,回测时你会发现它永远慢半拍。正确做法是把标签定义成「未来 N 日的收益率方向或幅度」。我一般用两种:分类标签(未来 5 日收益率 > 阈值记 1,否则 0)和回归标签(未来 5 日收益率本身)。分类适合做择时信号,回归适合做仓位管理。

# 未来5日累计收益率作为回归标签 df["future_ret_5"] = df["close"].shift(-5) / df["close"] - 1 # 分类标签:未来5日收益超过1%记为正样本 df["label_cls"] = (df["future_ret_5"] > 0.01).astype(int) # 构造模型输入:过去20天的特征窗口 FEATURES = ["log_ret", "close_pos", "vol_ratio", "boll_width"] WINDOW = 20 def make_sequences(data, features, window): X, y_reg, y_cls = [], [], [] arr = data[features].values reg = data["future_ret_5"].values cls = data["label_cls"].values for i in range(window, len(data) - 5): # 留出未来5天,防止标签泄漏 X.append(arr[i - window:i]) y_reg.append(reg[i]) y_cls.append(cls[i]) return np.array(X), np.array(y_reg), np.array(y_cls) X, y_reg, y_cls = make_sequences(df, FEATURES, WINDOW) print(X.shape, y_reg.shape, y_cls.shape) # (样本数, 20, 4)

参数说明:WINDOW=20对应约一个月的交易日,太短模型看不到趋势,太长参数量爆炸且容易过拟合;shift(-5)配合循环里的len(data)-5是双重保险,确保不会把未来数据泄漏进训练集。这一步做完,务必打印正负样本比例,如果正样本占比超过 70% 或低于 30%,说明阈值选得不好,模型会退化成「全猜多数类」。

2.3 训练集、验证集、测试集怎么切才不骗自己

时间序列绝对不能随机打乱切分,这是血泪经验。正确做法是按时间顺序切:前 70% 训练,中间 15% 验证调参,最后 15% 测试。而且测试集只能用一次,调完参再跑测试集,否则你调出来的「好结果」只是对测试集的过拟合。

n = len(X) train_end = int(n * 0.7) val_end = int(n * 0.85) X_train, y_train = X[:train_end], y_reg[:train_end] X_val, y_val = X[train_end:val_end], y_reg[train_end:val_end] X_test, y_test = X[val_end:], y_reg[val_end:] # 标准化:均值方差只能用训练集算,再应用到验证和测试 mean = X_train.reshape(-1, X_train.shape[-1]).mean(axis=0) std = X_train.reshape(-1, X_train.shape[-1]).std(axis=0) + 1e-9 def normalize(x): return (x - mean) / std X_train, X_val, X_test = normalize(X_train), normalize(X_val), normalize(X_test) print(X_train.shape, X_val.shape, X_test.shape)

标准化这步很多人图省事用全量数据算均值和方差,这是典型的未来函数,会让验证集表现虚高。记住一个原则:任何用到「全局统计量」的操作,都只能在训练集上拟合,再 transform 到其他集合。

3. 模型搭建与训练:LSTM 不是唯一解,但够用

3.1 为什么选 LSTM 而不是 Transformer 或纯 CNN

先说选型理由。股票序列长度通常只有几百到几千个交易日,样本量小,Transformer 的自注意力机制在这种数据量下极易过拟合,而且训练成本高。纯 CNN 能提取局部模式,但对长程依赖建模弱。LSTM 在中小样本、中短序列上表现稳定,参数量可控,是这类项目的性价比之选。如果你的数据量上到几十万条(比如分钟级、多股票联合训练),再考虑 Transformer 或 Temporal Fusion Transformer。常见做法是先用 LSTM 跑通基线,再决定要不要上更复杂的结构。

import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features, hidden=64, layers=2, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=hidden, num_layers=layers, batch_first=True, dropout=dropout ) self.head = nn.Sequential( nn.Linear(hidden, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, 1) # 回归输出未来收益率 ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq, hidden) last = out[:, -1, :] # 只取最后时间步 return self.head(last).squeeze(-1) model = LSTMPredictor(n_features=len(FEATURES), hidden=64, layers=2, dropout=0.3) print(sum(p.numel() for p in model.parameters())) # 参数量,控制在几万级

参数说明:hidden=64是中小样本的稳妥值,超过 128 在小数据集上基本过拟合;layers=2够用,层数越多梯度越难传;dropout=0.3是正则化关键,股票数据噪声大,dropout 低于 0.2 容易记住噪声。参数量打印出来控制在 5 万以内,超过就说明模型对这个数据量太大了。

3.2 训练循环里必须盯住的三个量

训练不是把 loss 跑低就完事。股票预测里,训练 loss 下降但验证 loss 上升是常态,你要盯的是:验证集 IC(预测值与真实收益的相关系数)、验证集方向准确率、以及 loss 曲线的分叉点。

from torch.utils.data import TensorDataset, DataLoader def to_loader(X, y, batch=64, shuffle=False): ds = TensorDataset(torch.tensor(X, dtype=torch.float32), torch.tensor(y, dtype=torch.float32)) return DataLoader(ds, batch_size=batch, shuffle=shuffle) train_loader = to_loader(X_train, y_train, shuffle=True) val_loader = to_loader(X_val, y_val) device = "cuda" if torch.cuda.is_available() else "cpu" model = model.to(device) opt = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) loss_fn = nn.MSELoss() best_val_ic, patience, wait = -1, 10, 0 for epoch in range(100): model.train() for xb, yb in train_loader: xb, yb = xb.to(device), yb.to(device) opt.zero_grad() loss = loss_fn(model(xb), yb) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) # 梯度裁剪防爆炸 opt.step() model.eval() preds, trues = [], [] with torch.no_grad(): for xb, yb in val_loader: preds.append(model(xb.to(device)).cpu().numpy()) trues.append(yb.numpy()) preds, trues = np.concatenate(preds), np.concatenate(trues) ic = np.corrcoef(preds, trues)[0, 1] acc = ((preds > 0) == (trues > 0)).mean() print(f"epoch {epoch} val_ic {ic:.4f} val_dir_acc {acc:.4f}") if ic > best_val_ic: best_val_ic, wait = ic, 0 torch.save(model.state_dict(), "best_lstm.pt") else: wait += 1 if wait >= patience: print("early stop") break

参数说明:lr=1e-3是 Adam 的常规起点,loss 震荡就降到 5e-4;weight_decay=1e-4是 L2 正则,抑制过拟合;clip_grad_norm_(..., 1.0)是 LSTM 训练的后悔药,梯度爆炸时能救回来;patience=10是早停耐心值,验证 IC 连续 10 轮不提升就停。判断模型有没有用的硬标准:验证集 IC 稳定在 0.03 以上、方向准确率稳定在 52% 以上,低于这个数说明特征或标签设计有问题,别急着调模型。

3.3 从预测值到交易信号:阈值和仓位怎么定

模型输出的是未来 5 日收益率预测值,要变成买卖信号,需要一个映射规则。最简单的是阈值法:预测值大于正阈值买入,小于负阈值卖出,中间持有不动。阈值不能拍脑袋,要在验证集上扫一遍,找收益和换手率的平衡点。

def signal_from_pred(pred, buy_th=0.005, sell_th=-0.005): # 1 买入,-1 卖出,0 持有 sig = np.zeros_like(pred) sig[pred > buy_th] = 1 sig[pred < sell_th] = -1 return sig # 在验证集上扫阈值 for th in [0.002, 0.005, 0.008, 0.01]: sig = signal_from_pred(preds, buy_th=th, sell_th=-th) # 简化收益:信号 * 真实收益,累乘 ret = (sig * trues).mean() turnover = (np.abs(np.diff(sig)) > 0).mean() print(f"th={th} avg_ret={ret:.5f} turnover={turnover:.3f}")

参数说明:buy_th和sell_th一般对称设置,阈值越高信号越少、换手越低但可能错过行情;阈值越低信号越多、交易成本吃掉收益。经验上日频策略阈值设在 0.5% 到 1% 之间,具体看你的手续费假设。这一步的输出直接决定策略能不能覆盖交易成本,A 股单边成本按万分之三到千分之一算,换手率超过 50% 就要警惕。

4. 回测与评估:别让漂亮曲线骗了你

4.1 回测必须包含的四个现实约束

很多学生项目的回测就是「信号乘收益累加」,跑出来年化 80%,一看就是假的。真实回测至少要加四个约束:交易成本(佣金+印花税+滑点)、T+1 限制(当天买入不能当天卖)、涨跌停无法成交、以及信号延迟(用 T 日收盘数据算的信号,T+1 开盘才能执行)。

def backtest(close, signal, cost=0.0013, delay=1): # close: 收盘价序列; signal: 每日目标仓位(1/0/-1) # delay: 信号延迟执行天数,A股T+1用1 pos = np.roll(signal, delay) # 信号延后一天生效 pos[0:delay] = 0 ret = close.pct_change().fillna(0).values strat_ret = pos * ret # 扣除换手成本 turnover = np.abs(np.diff(pos, prepend=0)) strat_ret = strat_ret - turnover * cost nav = (1 + strat_ret).cumprod() return nav, strat_ret nav, sr = backtest(df["close"].iloc[-len(y_test):].reset_index(drop=True), signal_from_pred(preds_test)) print(f"累计净值 {nav[-1]:.3f}")

参数说明:cost=0.0013是单边综合成本的经验值(佣金万三+印花税千一+滑点),保守一点可以设到 0.002;delay=1对应 T+1,如果你做的是分钟级或期货可以设 0。回测出来的净值曲线,重点看最大回撤和夏普比率,年化收益高但回撤 50% 的策略实盘拿不住。

4.2 评估指标怎么读,哪些数字是自欺欺人

评估一张表就够:年化收益、最大回撤、夏普比率、胜率、盈亏比、换手率。但要注意,日频策略的夏普超过 2 基本可以判定有未来函数或过拟合;胜率 55% 配合盈亏比 1.5 已经是好策略;换手率超过 100%(年化)在 A 股成本下很难盈利。我一般会做一次「打乱标签」的对照实验:把训练标签随机打乱再训一遍,如果模型在测试集上还能跑出正收益,说明你的回测框架有泄漏,不是模型厉害。

指标合理区间危险信号
年化收益10%~30%超过 50% 需查泄漏
最大回撤15%~30%超过 40% 实盘难承受
夏普比率0.8~1.8超过 2.5 大概率过拟合
日胜率50%~58%超过 65% 需查未来函数
年化换手30%~100%超过 200% 成本吃掉收益

4.3 报告和 PPT 里该放什么,不该放什么

高分项目的报告不是把代码贴一遍。评审和面试官想看的是:问题定义是否清晰、特征工程有没有逻辑、模型对比有没有做(至少 LSTM vs 线性回归 vs 随机森林)、回测有没有加现实约束、以及你对策略失效边界的认识。PPT 控制在 12 到 15 页:背景与问题 2 页、数据与特征 3 页、模型结构 2 页、回测结果 3 页、局限与改进 2 页、结论 1 页。把「打乱标签对照实验」和「不同阈值下的收益换手权衡图」放进去,比堆十个模型结构图更有说服力。

5. 避坑与排查:那些让我重跑过无数次的坑

5.1 验证集 IC 很高但回测亏钱

现象:验证集 IC 0.08,方向准确率 58%,但回测净值一路向下。原因通常是预测值和真实收益的量纲不匹配,或者信号阈值设得太低导致高频交易被成本吃掉。解决:先检查预测值的分布,如果预测值集中在 0 附近而真实收益波动大,说明模型没学到东西,IC 高只是偶然;再把阈值从 0.005 逐步提到 0.01,观察换手率是否下降、净值是否改善。

5.2 训练 loss 正常但预测值几乎不变

现象:模型输出所有样本的预测值都在 0.001 附近,方差极小。原因是标签的方差太小,或者模型退化成预测均值。解决:检查future_ret_5的标准差,如果小于 0.01,说明 5 日窗口太短,改成 10 日或 20 日;另外给模型加一层输出缩放,或者在 loss 里对极端值加权,让模型关注大幅波动样本。

5.3 换股票代码后结果全崩

现象:在茅台数据上跑得好好的,换成另一只股票 IC 直接变负。原因是模型学到的可能是这只股票的特有模式,而非通用规律。解决:要么做多股票联合训练(把多只股票序列拼起来,加股票 ID 嵌入),要么在特征里加入行业指数收益率、市场收益率等相对特征,让模型学「相对强弱」而不是「绝对价格」。

5.4 回测收益高但实盘滑点吃光利润

现象:回测年化 25%,实盘一跑变成 5%。原因是回测用了收盘价成交,实盘只能次日开盘或盘中成交,滑点没算够。解决:回测里把成交价改成次日开盘价,滑点从 0.001 提到 0.002,再看净值。如果这样还能盈利,策略才值得上实盘。

5.5 GPU 上训练结果和 CPU 不一致

现象:同一份代码,CPU 跑出来 IC 0.05,GPU 跑出来 IC 0.02。原因是随机种子没固定,以及 GPU 上浮点运算顺序不同导致微小差异被放大。解决:在代码开头固定torch.manual_seed(42)、np.random.seed(42),并设置torch.backends.cudnn.deterministic = True。如果还有差异,把 batch size 调大,减小梯度估计的随机性。

6. 让策略更稳的两个进阶技巧

第一个技巧是「多模型集成 + 滚动重训」。单模型对市场风格切换很敏感,我一般会训 3 个不同随机种子的 LSTM,预测值取平均,信号更稳。同时每 60 个交易日用最新数据重训一次模型,让模型跟上市场节奏。滚动重训的代码结构是在回测循环里,每到一个重训点就用截至当前的数据重新训练,再预测下一段。这样做的代价是计算量翻几倍,但回测和实盘的差距会明显缩小。

def walk_forward(df, features, window=20, retrain_every=60): nav = [1.0] for t in range(300, len(df) - 5, retrain_every): train_df = df.iloc[:t] X_tr, y_tr, _ = make_sequences(train_df, features, window) # 这里省略标准化和训练,实际项目里复用前面的训练函数 # model = train_lstm(X_tr, y_tr) # 预测未来 retrain_every 天并累乘净值 # ... return nav

第二个技巧是「用分类模型做风控,回归模型做仓位」。回归模型输出预测收益率,分类模型输出涨跌概率。当分类模型给出的上涨概率低于 0.45 时,即使回归模型预测为正,也把仓位降到半仓。这个双模型结构在震荡市里能显著降低回撤。参数上,分类阈值设 0.45 到 0.5 之间,太严会错过行情,太松起不到风控作用。

我自己做这类项目最大的教训是:不要迷信模型结构,先把数据和标签做干净,再谈模型。我见过太多人花两周调 Transformer,结果输给一个特征工程做扎实的 LSTM。另一个习惯是每次改完代码,先跑一遍「打乱标签」对照,确认没有泄漏再继续。这个习惯帮我省下了无数次重跑的时间。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 2:36:26

开源大模型私有化部署与LoRA微调、LangChain应用全链路实战

简介&#xff1a;面向AI大模型应用开发与落地场景&#xff0c;这份资料围绕开源大模型的环境配置、私有化部署、LoRA微调与LangChain应用展开&#xff0c;覆盖DeepSeek、Yi、Qwen、Baichuan、ChatGLM、MiniCPM等主流模型&#xff0c;适合正在学习大模型技术栈并希望动手实践的开…

作者头像 李华
网站建设 2026/10/2 2:35:49

C++中的6种构造函数举例详解

在 C 中&#xff0c;构造函数是一种特殊的成员函数&#xff0c;用于初始化类对象。在对象创建时自动调用&#xff0c;构造函数的主要作用是分配资源、初始化数据成员等。根据不同的功能和使用场景&#xff0c;C 提供了多种类型的构造函数&#xff1a;1. 默认构造函数 (Default …

作者头像 李华
网站建设 2026/10/2 2:33:27

用 CodeArts AI 智能体零依赖打造「鲜拾 FreshKeep」食材保鲜管家

一键开通华为云码道 CodeArts 代码智能体&#xff1a;https://developer.huaweicloud.com/codeartsco.html?sourcedmzntgwatomgit1&sourceaddmzntgwatomgithd 引言&#xff1a;从家庭冰箱里的浪费说起 中国家庭每年因食材过期造成的浪费触目惊心。一项调研显示&#xff0…

作者头像 李华
网站建设 2026/10/2 2:33:20

基于单视频三维实时重构的危化园区人员/车辆无感定位与危险源空间关系持续感知技术方案

前言危化园区安全风险的核心管控难点&#xff0c;在于人员、车辆等动态流动要素与储罐、装置、高危管廊等静态危险源之间的实时空间耦合关系不可见、不可算、不可预。传统园区监测体系多采用独立摄像头二维观测、标签式人员定位、定点传感监测的离散模式&#xff0c;仅能实现单…

作者头像 李华
网站建设 2026/10/2 2:33:09

无人机巡检平台如何重塑光伏场站的缺陷检测

中国光伏装机量已连续多年位居全球首位&#xff0c;累计装机容量突破数百GW。场站规模快速扩张的同时&#xff0c;运维侧的压力也在同步放大——光伏组件分布在广袤的户外场地&#xff0c;长期承受紫外线、温差、风沙等环境应力&#xff0c;热斑、隐裂、二极管故障等缺陷难以避…

作者头像 李华