☰
基于深度学习的股票价格预测与量化策略:Python源码与回测避坑指南
2026/10/2 2:36:52 网站建设 项目流程

简介:这是一套面向计算机相关专业学生与量化投资初学者的深度学习实战项目资料,围绕股票价格预测与量化策略研究展开,可作为毕业设计、课程设计或期末大作业的完整参考方案。资源包共收录1409个文件,以636个Python源码与671个编译缓存文件为主体,另含exe可执行程序、nbc与nbi笔记文件、csv预测结果数据、h5模型权重、pkl序列化模型、pptx汇报文档及xlsx实验记录等,压缩包约16.2MB,目录结构完整,覆盖数据预处理、模型训练、结果输出与策略回测等环节。目前已有247人学习下载。读者可据此获得一套经导师指导并通过评审的高分项目实现,包括可运行的预测代码、训练好的模型文件、量化策略逻辑说明、实验数据与答辩PPT,便于快速理解深度学习在金融时序预测中的落地流程,并在此基础上完成二次开发或论文撰写。

1. 从一份高分项目拆起:股票价格预测加量化策略到底在做什么

很多人第一次看到“基于深度学习的股票价格预测和量化策略研究”这类题目,第一反应是拿 LSTM 套一段收盘价,画出预测曲线,然后宣称能赚钱。真跑过实盘或者做过完整回测的人都知道,预测曲线好看和账户曲线好看之间隔着一条鸿沟。这个标题真正要解决的不是“预测明天涨跌”,而是把深度学习模型输出的信号,翻译成一套可回测、可复现、有风控约束的量化策略,并且用 Python 源码、文档说明和报告 PPT 把整条链路讲清楚。它适合两类人:一类是正在做课程设计或毕业设计、需要一份结构完整的高分项目的学生;另一类是想从零搭一套“数据—模型—信号—回测”闭环的 Python 开发者。核心词深度学习、股票价格预测、量化策略、Python 源码会贯穿后面所有章节,因为缺任何一环,这个项目都只是半成品。

2. 数据管道与特征工程:把原始行情变成模型能吃的张量

2.1 为什么不能直接把收盘价丢给 LSTM

股票价格序列是非平稳的,均值和方差随时间漂移。直接把原始收盘价喂给网络,模型很容易学到“最近价格接近当前价格”这种平凡解,验证集 loss 很低,但一到样本外就崩。常见做法是先做差分或对数收益率变换,把绝对价格转成相对变化,再配合滚动窗口标准化。另一个坑是未来函数:用全量数据算均值和方差再标准化,等于把未来信息泄漏进训练集。正确做法是每个滚动窗口内单独 fit 标准化器,只 transform 当前窗口及之后的数据。

我一般会把特征分成三类:价量特征(开高低收、成交量、换手率)、技术指标(MA、EMA、RSI、MACD、布林带宽度)、市场状态特征(波动率、大盘同期收益率)。技术指标不要用现成库一把梭,最好自己按公式实现一遍,方便控制窗口和避免未来函数。下面这段代码演示如何用 pandas 构造对数收益率、滚动标准化和技术指标,并保证不泄漏未来信息。

import numpy as np import pandas as pd def build_features(df, window=20): # df 需包含 open/high/low/close/volume,索引为交易日 out = pd.DataFrame(index=df.index) # 对数收益率,平稳化处理 out['log_ret'] = np.log(df['close'] / df['close'].shift(1)) # 滚动波动率,衡量市场状态 out['volatility'] = out['log_ret'].rolling(window).std() # 均线偏离度,避免直接使用绝对价格 ma = df['close'].rolling(window).mean() out['ma_bias'] = (df['close'] - ma) / ma # RSI 简化版,窗口内涨跌幅均值比 delta = df['close'].diff() gain = delta.clip(lower=0).rolling(window).mean() loss = (-delta.clip(upper=0)).rolling(window).mean() out['rsi'] = 100 - 100 / (1 + gain / (loss + 1e-8)) # 成交量相对变化 out['vol_change'] = df['volume'] / df['volume'].rolling(window).mean() - 1 # 逐窗口标准化,避免使用全量统计量 out = out.dropna() return out def rolling_normalize(features, window=60): # 对每个时间点,只用过去 window 条数据做标准化 normed = (features - features.rolling(window).mean()) / (features.rolling(window).std() + 1e-8) return normed.dropna()

逻辑说明:build_features先把价格转成对数收益率和偏离度,再计算波动率和 RSI,所有 rolling 操作都只回看历史。rolling_normalize是防未来函数的关键,它按时间点滚动计算均值和标准差,而不是对整列做一次标准化。参数方面,window=20适合日线级别约一个月的交易周期,window=60适合季度级别;如果做分钟级数据,窗口要相应缩小到 60 或 120 根 K 线。标准化分母加1e-8是防止波动率为零时除零。

2.2 标签怎么打才和量化策略对齐

预测任务和策略收益必须对齐。如果你预测的是“未来一天收益率”,但策略按“未来五天持仓”调仓,标签和信号就错位了。常见做法有两种:一是回归未来 N 日累计收益率,二是分类未来 N 日涨跌方向。分类任务更容易和阈值策略结合,比如预测上涨概率超过 0.6 才开仓。标签构造时要注意最后 N 天没有未来数据,必须丢弃,否则会引入 NaN 或错误标签。

def make_label(df, horizon=5, threshold=0.0): # 未来 horizon 日累计对数收益率 future_ret = np.log(df['close'].shift(-horizon) / df['close']) # 三分类:涨、跌、震荡 label = pd.Series(0, index=df.index) label[future_ret > threshold] = 1 label[future_ret < -threshold] = -1 # 最后 horizon 行没有未来数据,置为 NaN 后丢弃 label[future_ret.isna()] = np.nan return label, future_ret

horizon=5对应周频调仓,threshold=0.0表示只要方向;如果要做多空策略,可以把 threshold 设为交易成本的两倍,过滤掉覆盖不了手续费的微小波动。标签和特征合并后,必须按时间顺序切分训练集、验证集、测试集,不能随机打乱,否则时间序列的先后关系被破坏,回测结果会虚高。

3. 深度学习模型选型与训练:LSTM、TCN 还是 Transformer

3.1 序列模型在股票数据上的真实表现边界

LSTM 和 GRU 是这类项目最常用的基线,优点是能捕捉一定长度的时序依赖,缺点是训练慢、对超参敏感。TCN(时间卷积网络)感受野可控、并行度高,在小样本金融数据上往往比 LSTM 稳。Transformer 适合长序列,但股票日线数据通常只有几千条,注意力机制容易过拟合,除非你做多股票联合训练或者引入大量另类数据。我的建议是:课程设计或毕设先用 LSTM 跑通全流程,再换 TCN 做对比实验,报告里放两组结果,比只堆一个复杂模型更有说服力。

模型输入形状是(样本数, 时间步长, 特征数)。时间步长一般取 20 到 60,对应一个月到一个季度。特征数就是上一章构造的列数。输出层根据任务定:回归用 1 个神经元加线性激活,三分类用 3 个神经元加 softmax。损失函数回归用 MSE 或 Huber,分类用交叉熵。优化器 Adam 学习率从 1e-3 开始,配合 ReduceLROnPlateau 在验证集 loss 不降时减半。

import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features, hidden=64, layers=2, n_class=3, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=hidden, num_layers=layers, batch_first=True, dropout=dropout if layers > 1 else 0 ) self.head = nn.Sequential( nn.Linear(hidden, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, n_class) ) def forward(self, x): # x: (batch, seq_len, n_features) out, _ = self.lstm(x) # 只取最后一个时间步的输出做分类 last = out[:, -1, :] return self.head(last)

逻辑说明:batch_first=True让输入维度是(batch, seq, feature),和 DataLoader 默认输出一致。dropout在 LSTM 内部和全连接层都加,金融数据噪声大,dropout 低于 0.2 基本压不住过拟合。hidden=64、layers=2是日线数据上的常用起点,数据量超过 5 万条可以加到 128 和 3 层。训练时每个 epoch 后在验证集上算准确率和 F1,不要只看 loss,因为类别不平衡时 loss 会骗人。

3.2 训练循环里必须加的三个约束

第一,早停。验证集 loss 连续 10 个 epoch 不下降就停,保存验证集最优权重。第二,学习率调度。用ReduceLROnPlateau(patience=5, factor=0.5),避免后期震荡。第三,梯度裁剪。torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0),防止 LSTM 梯度爆炸。这三条不加,训练曲线会像过山车,报告里没法解释。

from torch.optim.lr_scheduler import ReduceLROnPlateau def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0 for x, y in loader: x, y = x.to(device), y.to(device) optimizer.zero_grad() logits = model(x) loss = criterion(logits, y) loss.backward() # 梯度裁剪,防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() total_loss += loss.item() * x.size(0) return total_loss / len(loader.dataset) # 训练主循环片段 scheduler = ReduceLROnPlateau(optimizer, mode='min', patience=5, factor=0.5) best_val = float('inf') wait = 0 for epoch in range(100): train_loss = train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss = evaluate(model, val_loader, criterion, device) scheduler.step(val_loss) if val_loss < best_val: best_val = val_loss torch.save(model.state_dict(), 'best.pt') wait = 0 else: wait += 1 if wait >= 10: break

参数说明:max_norm=1.0是常用值,梯度范数超过 1 就按比例缩放。patience=5表示验证 loss 5 个 epoch 不降才减学习率,太短会导致学习率过早衰减。早停wait>=10配合学习率调度,一般能比固定 epoch 提升 2 到 5 个百分点的样本外准确率。设备用cuda如果可用,否则cpu,日线数据量下 CPU 也能跑,只是慢。

4. 从预测信号到量化策略:回测框架与仓位管理

4.1 信号生成和交易成本建模

模型输出概率后,要转成实际仓位。最简单的是阈值法:预测上涨概率大于 0.6 开多,小于 0.4 开空,中间空仓。但这样换手率很高,交易成本会吃掉大部分收益。常见改进是加持有期约束,比如开仓后至少持有 3 天,或者用概率变化率触发调仓。回测时必须计入手续费和滑点,A 股手续费按万分之三、印花税千分之一(卖出)、滑点按一个最小变动价位估算。不做成本建模的回测,夏普比率能到 3,加上成本直接变负。

def generate_position(proba, threshold_long=0.6, threshold_short=0.4, min_hold=3): # proba: 模型预测的上涨概率序列 pos = np.zeros(len(proba)) current = 0 hold_days = 0 for i, p in enumerate(proba): if hold_days > 0: hold_days -= 1 pos[i] = current continue if p > threshold_long: current = 1 hold_days = min_hold elif p < threshold_short: current = -1 hold_days = min_hold else: current = 0 pos[i] = current return pos def backtest(returns, position, fee=0.0003, tax=0.001, slippage=0.0002): # returns: 标的日收益率序列 # position: 每日仓位,1 多 -1 空 0 平 pos = pd.Series(position, index=returns.index) turnover = pos.diff().abs().fillna(0) cost = turnover * (fee + slippage) + (pos.diff() < 0).astype(float) * tax strat_ret = pos.shift(1).fillna(0) * returns - cost return strat_ret

逻辑说明:generate_position用min_hold控制最低持有天数,减少无效换手。backtest里pos.shift(1)表示今天收盘后生成的信号,明天才能生效,避免用当日信号交易当日收益的未来函数。cost里卖出才收印花税,所以用pos.diff() < 0判断。参数方面,threshold_long=0.6和threshold_short=0.4可以根据验证集上的精确率和召回率调,min_hold=3适合日线周频调仓,分钟级可以降到 1。

4.2 评价指标不能只看准确率

分类准确率在类别不平衡时没有意义。上涨和下跌样本各占一半时,全预测上涨也有 50% 准确率。要看的是:样本外夏普比率、最大回撤、胜率、盈亏比、换手率。夏普比率用日收益年化,无风险利率按 2% 估。最大回撤从净值曲线算。胜率是盈利交易占比,盈亏比是平均盈利除以平均亏损。这四个指标一起看,才能判断策略是否值得继续。

指标计算方式参考阈值
年化夏普日收益均值/标准差 × sqrt(252)大于 1 可接受,大于 1.5 较好
最大回撤净值从峰值到谷底的最大跌幅小于 20% 较稳
胜率盈利交易数/总交易数40% 以上配合盈亏比
盈亏比平均盈利/平均亏损大于 1.5 才有优势
换手率日均仓位变化绝对值日线策略低于 0.5 较合理

如果夏普高但换手率也高,大概率是成本没算够。如果胜率高但盈亏比低于 1,说明赚小亏大,一次大亏能吞掉多次小赚。这些指标要放在报告 PPT 里,比只放预测曲线有说服力得多。

5. 避坑与排查:那些让回测曲线一夜回到解放前的细节

5.1 未来函数:最隐蔽也最致命

现象:回测夏普比率异常高,样本外测试集表现远好于验证集。原因:特征计算或标准化时用了全量数据统计量,或者标签构造时把未来收益泄漏进了特征。解决:所有 rolling 操作只回看历史,标准化器在每个时间窗口单独 fit,标签用shift(-horizon)后立即丢弃尾部 NaN,回测信号用shift(1)延迟一天生效。检查方法:把数据倒序跑一遍,如果结果和正序差不多,说明没有未来函数;如果倒序结果崩了,正序大概率有泄漏。

5.2 过拟合:验证集 loss 降但样本外崩

现象:训练集准确率 80%,验证集 75%,测试集 52%。原因:模型太复杂、特征太多、训练轮数太多。解决:减少 LSTM 层数和隐藏单元,加 dropout 和 L2 正则,早停 patience 调小,特征做相关性筛选去掉冗余列。另一个血泪经验是不要用测试集调参,测试集只能用一次,调参看验证集。

5.3 交易成本被低估

现象:回测年化收益 30%,实盘或加成本后变成 5%。原因:换手率太高,手续费和滑点没算够。解决:在回测函数里显式加入手续费、印花税和滑点,用min_hold降低换手,阈值调高减少无效交易。A 股印花税卖出千分之一,很多人只算佣金不算税,结果差一倍。

5.4 数据对齐和停牌处理

现象:回测净值曲线在某些日期跳空,或者信号和价格错位。原因:不同股票停牌日不一致,或者特征和标签索引没对齐。解决:用交易日历对齐,停牌日填充前收盘价或直接剔除,合并特征和标签时用pd.concat(axis=1).dropna(),确保每一行都有完整的特征和标签。回测时如果某日无交易,仓位保持不变,不要用 0 填充。

5.5 随机种子和可复现性

现象:每次跑出来的结果都不一样,报告里的数字对不上。原因:PyTorch 和 NumPy 的随机种子没固定,DataLoader 的 shuffle 没设种子。解决:在代码开头固定torch.manual_seed(42)、np.random.seed(42),DataLoader 的generator也设种子,CuDNN 设torch.backends.cudnn.deterministic = True。这样别人拿到你的 Python 源码才能复现出同样的曲线。

6. 把项目做成能讲清楚的高分交付物

一份高分项目不只是代码能跑,还要文档说明和报告 PPT 能把“为什么这么做”讲明白。文档说明至少包含:环境依赖(Python 版本、PyTorch、pandas、numpy 版本)、数据来源和字段说明、每个模块的输入输出、参数含义、运行命令。报告 PPT 按“问题—数据—方法—实验—策略—结论”六段组织,每段一页,重点放对比实验和回测指标表,不要堆代码截图。

我自己的习惯是:先写一个README.md把运行步骤压到五条命令以内,再写docs/目录放详细说明,最后用 Jupyter Notebook 把数据探索、模型训练、回测三段串起来,方便答辩时现场演示。PPT 里放一张净值曲线对比图(模型策略 vs 买入持有),一张指标表,一张特征重要性或注意力权重图,基本就能覆盖大部分评分点。

# 环境安装与运行示例 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install torch pandas numpy matplotlib scikit-learn python src/build_features.py --input data/raw.csv --output data/features.csv python src/train.py --config configs/lstm.yaml --seed 42 python src/backtest.py --model models/best.pt --start 2020-01-01 --end 2023-12-31

这段命令把数据构造、训练、回测串成一条线,--seed 42保证可复现,--start和--end控制回测区间。参数文件configs/lstm.yaml里放窗口长度、隐藏单元、学习率、阈值这些可调项,改参数不用动代码。最后提醒一句:这个方向值不值得做,取决于你能不能把预测、策略、成本、风控四件事都讲圆,只做预测曲线的话,离高分还差得远。希望帮到你。

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

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

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

立即咨询