简介:这是一份基于深度学习的股票价格预测与量化交易策略Python项目资料包,也是计算机科学与技术专业的本科毕业设计成果,适合机器学习入门者与金融科技方向学生。项目围绕LSTM时序预测模型与多维技术指标展开,完整覆盖数据预处理、特征工程、模型训练、策略回测与绩效评估等标准化流程,并引入波动率过滤与风险平价机制控制回撤,可让学习者系统理解深度量化策略的构建方式。整包共1692个文件,以Python源码与pyc文件为主,包含配置文件、可执行脚本、技术文档、研究报告及答辩演示文稿等,压缩包仅16.25MB,目录分层清晰。已有143人学习下载。包内不仅包含可直接运行的完整源码,还有详细技术文档、多种算法实现对比及详细注释,关键环节均经过测试验证,可作为毕业设计参考、期末大作业模板或量化投资入门研究案例。
1. 深度学习预测股票价格:这套 Python 源码能落地多少
深度学习做股票价格预测与量化交易策略,这几年在毕业设计和期末大作业里出现频率极高,但多数网上项目只给一段 LSTM 训练代码,数据怎么洗、回测怎么做、报告怎么落全要自己补。这套基于深度学习的股票价格预测与量化交易策略 Python 源码,打包了从数据预处理、滑动窗口特征、LSTM 模型训练,到信号生成、回测评估和研报文档的完整链路,解压之后沿着文档就能把全流程跑通。适合三类人:毕业设计需要完整交付物的学生,课程期末大作业想拿高分的,以及想快速验证"深度学习算法到底能不能用于交易决策"但不想从零搭回测框架的从业者。先给结论:模型训练只是中间一环,真正决定这个项目好坏的是它把数据处理、训练、策略、回测串成了闭环,而踩坑基本都集中在后半段。
2. 数据与特征工程:为什么直接拿 K 线喂 LSTM 会翻车
2.1 数据源与复权处理:前复权是默认,但除权日要小心
数据层是整套源码的地基。常见做法是从 tushare、akshare 或 baostock 拉日线数据,落到本地 CSV,再交给预处理脚本。这里有一个新手最容易忽略的坑:股票除权除息那天,价格会产生跳空,如果不做复权处理,模型会把这次跳空当成真实的市场波动去学习,结果就是预测值在除权日附近出现明显毛刺,方向准确率也被拖低。
源码里用的是前复权价,这也是国内量化入门最常见的处理方式。前复权的原理是拿复权因子把历史价格统一到当前价格基准,这样历史价格序列就是连续的,模型学到的是"价格连续变化"而不是"某天突然跌了 30% 的假象"。严格讲,前复权需要把后复权价再除以最新一期的复权因子,代码里如果只写close * adj_factor,得到的是后复权价,两者趋势等价,但数值基准不同,混用会导致回测和实盘对不上。我一般会在 data 目录保留一份原始不复权数据,复权结果单独落盘,方便排查"模型上周跑得好好的,这周突然变差"这种玄学问题。
import pandas as pd df = pd.read_csv("data/600000.csv", parse_dates=["date"]) df = df.sort_values("date").reset_index(drop=True) # 前复权:后复权价再除以最新复权因子 if "adj_factor" in df.columns: df["close_adj"] = df["close"] * df["adj_factor"] / df["adj_factor"].iloc[-1] df["open_adj"] = df["open"] * df["adj_factor"] / df["adj_factor"].iloc[-1] else: df["close_adj"] = df["close"] df["open_adj"] = df["open"] df = df.dropna(subset=["close_adj", "volume"]).reset_index(drop=True)这段代码的逻辑是:先按日期排序并重置索引,因为后续做滑动窗口时索引错一位,标签就会整体错位。复权因子字段来自数据接口返回的 adj_factor 列,用它计算前复权价。dropna是为了过滤停牌导致的空值行。实际使用中我建议把成交额、换手率这些列暂时剔除,只保留 open/high/low/close/volume 五列,先跑通基线模型,再加特征对比效果。
数据完整性检查也要在这里做掉:日线数据里经常混着停牌日空值、新股上市早期的极端波动。处理停牌日,常见做法是直接丢行而不是补 0,因为补 0 会让模型学到"股价横盘"的假象,而实际停牌日根本无法交易,这个信息对交易决策毫无帮助。
2.2 特征窗口与标签设计:滑点数据别直接丢给模型
LSTM 的输入是形状为 (batch, sequence_length, feature_dim) 的三维张量。sequence_length 是回看多少个交易日,源码默认 20,对应约一个月的交易日数;feature_dim 就是喂给模型的列数。这里有个常见误用:有人直接把整个 DataFrame 丢进模型,date、股票代码这些列也一起进去,等于让模型去学"哪一天"和"哪只股票",这些信息与预测下一日价格毫无关系,只会放大过拟合。
标签设计上,源码用的是回归方式,预测下一个交易日的收盘价。也有人用分类方式做涨跌二分类,但从我拆过的项目看,回归加阈值判断比直接分类更稳定,因为分类器把涨跌幅度完全丢掉了,后续做仓位控制时没有置信度依据。归一化用 MinMaxScaler,每列独立缩放到 [0,1]。注意 scaler 的 fit 只能放在训练段,验证段和测试段只做 transform,这个点涉及数据泄露,后面避坑章会重点展开。
import numpy as np from sklearn.preprocessing import MinMaxScaler feature_cols = ["open_adj", "high_adj", "low_adj", "close_adj", "volume"] scaler = MinMaxScaler(feature_range=(0, 1)) scaled = scaler.fit_transform(df[feature_cols].values) SEQ_LEN = 20 # 回看 20 个交易日 def make_windows(data, seq_len=SEQ_LEN): X, y = [], [] for i in range(seq_len, len(data) - 1): # 留出 i+1 作为预测目标 X.append(data[i - seq_len:i]) y.append(data[i + 1, 3]) # close_adj 在列索引 3 return np.array(X), np.array(y) X, y = make_windows(scaled) print(X.shape, y.shape) # (样本数, 20, 5) / (样本数,)这个循环从 seq_len 开始,到len(data) - 1结束,保证 y 取的是窗口末尾后一天的数据,也就是"用过去 20 天预测第 21 天的收盘价"。y 取data[i + 1, 3]是因为 feature_cols 里 close_adj 排在第 4 位。这是整套源码里最容易改错的地方——很多人写成data[i, 3],变成用当天预测当天,回测里看不出来,实盘一用就废。sequence_length 这个参数最影响结果:太短(5 以内)模型只学到近几天噪声,太长(60 以上)训练变慢且容易记住历史形态,20 到 30 是比较稳的区间。
2.3 训练 / 验证 / 测试集切分:时间序列不能随机分割
分类任务里常见的train_test_split(random_state=42)在时序项目里是灾难:随机抽样会把未来的数据混进训练集,验证集和测试集里出现"模型已经见过这段行情"的情况,回测收益高得离谱,实盘直接打回原形。时间序列必须按时间顺序切分,常见比例是 7:2:1 或者 7:1.5:1.5。
源码里按样本行数切,因为 X 已经按时间排序,直接切片就是最干净的切法:
total = len(X) train_end = int(total * 0.7) val_end = int(total * 0.85) X_train, y_train = X[:train_end], y[:train_end] X_val, y_val = X[train_end:val_end], y[train_end:val_end] X_test, y_test = X[val_end:], y[val_end:]三个集合之间没有重叠,测试集永远是时间上最后的那部分。这个切法配合后面讲的滚动再训练,才能证明模型不是背答案。熟悉 sklearn 的人这时候容易手滑加一个shuffle=True,记住:时序数据切完训练、验证、测试三块之后,每一块内部也不该再 shuffle,因为 LSTM 的序列本身是有时间顺序的,窗口内部顺序打乱等于把因果链扯断。很多期末大作业只看训练集 loss 下降曲线,验证集和测试集完全不用,答辩时一问就露馅。建议至少把验证集 loss 曲线也画出来,训练 loss 降、验证 loss 不降,过拟合信号就已经很明显了。
3. 模型训练与调参:LSTM 参数怎么设才不过拟合
3.1 网络结构:单层 LSTM 和堆叠 LSTM 怎么选
模型部分,源码核心用的是 LSTM,这是深度学习算法里处理时间序列最稳的入门选择。hidden_size 默认 64,num_layers 默认 1。单层 LSTM 适合金融数据这种信噪比极低的任务:堆叠两层 LSTM 虽然表达能力更强,但金融时序里真正能利用的规律很弱,网络变深反而把噪声一并学进去,表现为训练集 loss 很低、验证集和回测一塌糊涂,这是典型的过拟合幻觉。
如果你做过图像或 NLP 项目,会嫌这个结构太简单,但股票价格预测的难点不在模型容量,而在特征设计和标签对齐。我拆过的项目里,把 LSTM 换成 GRU、加上 Attention 带来的提升,远不如把数据泄露修掉带来的提升大。源码里的结构是 LSTM 后接一个全连接层输出标量,这就是完整的预测头。想补 LSTM 理论基础的,拿《动手深度学习》里循环神经网络那一章对着看,足够应付答辩了。
import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, input_size=5, hidden_size=64, num_layers=1, 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 if num_layers > 1 else 0 ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, seq_len, feature_dim) out, _ = self.lstm(x) # 取最后一个时间步的隐藏状态做预测 last = out[:, -1, :] return self.fc(last)batch_first=True表示输入形状是 (batch, seq_len, feature_dim),out[:, -1, :]取序列最后一个时间步的输出,对应"用完整窗口信息预测下一日"。参数取舍上,input_size 必须等于 feature_dim(这里是 5);dropout 在单层 LSTM 上实际上不生效,这是 PyTorch 的机制限制,别看到 dropout=0.2 就以为有正则效果,真正发挥作用需要 num_layers 至少为 2。
3.2 训练参数与早停:学习率、批次、随机种子怎么配
训练部分源码用的是 MSE 损失,因为标签是连续价格。优化器选 Adam,默认 lr=1e-3,batch_size 取 64,epochs 设置 100 但配合早停机制。金融时序噪声大,训练到后期验证集 loss 往往先降后升,所以必须保存"验证集最优"的模型权重,而不是最后一个 epoch 的模型,这是训练这类项目的默认动作。
随机种子不锁,复现就是玄学。深度学习训练的随机性来自 PyTorch 的参数初始化、DataLoader 的 shuffle 顺序、GPU 上原子操作顺序。锁种子并不能保证 GPU 完全可复现,但至少能保证 CPU 环境下每次跑出的结果一致,这对毕设报告和答辩至关重要。
import random import numpy as np import torch def seed_everything(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) seed_everything(42) loss_fn = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) best_val = float("inf") patience = 0 for epoch in range(100): model.train() for xb, yb in train_loader: optimizer.zero_grad() pred = model(xb).squeeze() loss = loss_fn(pred, yb) loss.backward() optimizer.step() model.eval() val_loss = 0.0 with torch.no_grad(): for xb, yb in val_loader: pred = model(xb).squeeze() val_loss += loss_fn(pred, yb).item() * len(xb) val_loss /= len(val_loader.dataset) if val_loss < best_val: best_val = val_loss torch.save(model.state_dict(), "best_model.pt") patience = 0 else: patience += 1 if patience >= 10: print(f"early stop at epoch {epoch}") breakseed_everything 必须在模型初始化之前调用,否则模型参数初始化的随机性已经发生,锁了等于没锁。早停 patience=10 表示验证集连续 10 个 epoch 没创新低就停。核心训练参数按下面这张表调就行:
| 参数 | 建议值 | 说明 |
|---|---|---|
| learning_rate | 1e-3 ~ 5e-4 | 大于 1e-2 会震荡,loss 可能直接发散 |
| batch_size | 32 ~ 128 | 日线数据量不大,64 够用 |
| epochs | 100(带早停) | 固定 100 不早停容易过拟合 |
| patience | 10 | 验证集连续 10 轮不降就停 |
常见翻车点是 loss 一开始就 nan:先检查数据里有没有 0 成交量或停牌日空值,MinMax 归一化分母出现 0 时结果就是 nan,训练集有 nan 的话 Adam 会把整个参数空间带崩。另一个隐蔽问题是 DataLoader 的 drop_last 参数:末批样本数不足 batch_size 时,Loss 计算取平均会体现不出全局水平,建议训练时设置drop_last=True,省去这个变量。
3.3 模型评估:回归指标和方向准确率
模型评估不能只看 loss。MSE 和 RMSE 反映的是价格预测的绝对误差,但策略真正关心的是方向——明天是涨还是跌。源码里提供了方向准确率:预测下一日涨跌方向与实际涨跌方向一致的比例。两个指标要一起看:RMSE 衡量校准程度,方向准确率衡量收益潜力。
from sklearn.metrics import mean_absolute_error, mean_squared_error preds = predict(model, X_test, scaler) # 注意逆归一化回原价格 actuals = y_test_original mae = mean_absolute_error(actuals, preds) rmse = mean_squared_error(actuals, preds, squared=False) direction_acc = np.mean(np.sign(preds[1:] - actuals[:-1]) == np.sign(actuals[1:] - actuals[:-1]))方向准确率的计算逻辑是:preds[t] 预测的是 actuals[t+1] 的价格,所以预测涨跌方向要拿preds[1:]和actuals[:-1]做差,实际涨跌方向拿actuals[1:]和actuals[:-1]做差,两者相等才算预测对。这个指标能到 0.55 以上就是有价值的模型了,别指望 0.7、0.8 那种数字,真出现了大概率是数据泄露或者标签错位。
4. 量化策略与回测:预测信号如何变成可交易决策
4.1 信号生成:阈值、仓位、止损参数怎么设
预测模型输出的是明天的价格,但"预测涨了"不等于"要买"。源码里信号生成用的常见做法是:预测收益率超过某个阈值才买入,低于负阈值则清仓或做空。阈值默认 0.002(0.2%),因为日线级别的价格波动里,低于这个数连手续费都覆盖不了。如果你在网上搜 python 量化交易策略代码,会发现大量片段只给信号生成、不给仓位管理和止损,这套源码把阈值、仓位比例、止损线都做成了可配置参数,这是它比那些半成品片段完整的地方。
PRED = preds # 模型输出的下一日收盘价预测 CLOSE = actuals # 当日实际收盘价 THRESHOLD = 0.002 # 预测收益阈值 POSITION = 0.8 # 单次最大仓位 STOP_LOSS = 0.03 # 亏损 3% 强制平仓 signal = np.zeros(len(PRED)) for t in range(1, len(PRED)): pred_ret = PRED[t] / CLOSE[t - 1] - 1 if pred_ret > THRESHOLD: signal[t] = POSITION elif pred_ret < -THRESHOLD: signal[t] = -POSITION # 介于两者之间 signal 保持 0,空仓等待pred_ret 用 CLOSE[t-1] 做基准,因为模型预测的是 t+1 日价格,而 CLOSE[t-1] 是发出预测时能得到的最新收盘价。threshold 是策略敏感度的核心参数:0.002 偏保守,信号少但每次出手的信息质量更高;0.0005 信号频繁,但收益会被手续费吃掉一大块。POSITION 默认不超过 0.8,保留 0.2 现金本身就是风控,这个习惯比模型参数更值钱。
需要注意 A 股做空受限,signal 为负时,实盘里更多是"清仓观望"而不是"开空仓"。源码里有的版本内置了多空模式开关,毕设场景我建议只用多头和空仓两档,别在报告里去谈做空,答辩时逻辑更干净。
4.2 回测引擎:手续费、滑点、涨跌停过滤
回测是整套资源里最容易被低估的部分。源码自带的回测引擎是向量化实现,核心思想是把每天的持仓、资金变化、净值用数组一次算出来,速度快、代码短。但手续费和滑点必须算进去:A 股佣金双边万 2.5 左右,卖出还要加印花税(2023 年 8 月之后减半征收,为 0.05%),合起来单边大约万 7.5,双边万 15。滑点按开盘价下单和实际成交价的差额估,小盘股给 0.1% 到 0.2% 不过分。这些成本不进回测,收益外观看翻倍,实盘直接亏损。
涨跌停过滤是另一个黑匣子:涨停板买不上,跌停板也卖不出。回测里如果不加过滤,策略会在涨停日"买入成功",然后享受次日继续上涨的盈利,但实际交易根本成交不了。我一般这样处理:当日涨幅大于等于 9.8% 且信号为买入时,强制把信号置为 0;跌幅小于等于 -9.8% 且信号为卖出时,同样置为 0。
def backtest(signal, actuals, fee_rate=0.0015, slippage=0.001): cash = 1_000_000 position = 0.0 pnl = [] for t in range(1, len(signal)): if signal[t] == 0: pnl.append(cash + position * actuals[t - 1]) continue # 涨跌停过滤:涨停买不进,跌停卖不出 daily_ret = actuals[t] / actuals[t - 1] - 1 if signal[t] > 0 and daily_ret >= 0.098: pnl.append(cash + position * actuals[t - 1]) continue if signal[t] < 0 and daily_ret <= -0.098: pnl.append(cash + position * actuals[t - 1]) continue trade_price = actuals[t] * (1 + slippage if signal[t] > 0 else 1 - slippage) cost = abs(signal[t]) * trade_price * fee_rate cash -= signal[t] * trade_price + cost position += signal[t] pnl.append(cash + position * actuals[t]) return np.array(pnl)fee_rate=0.0015 是双边费率,包含佣金、印花税和过户费,按 100 万本金、每次 0.8 仓位计算,单次交易成本大约 1200 元。slippage 是滑点假设,实盘中开盘跳空大的日子实际成交成本更高。这段回测的逻辑是:信号为 0 时只记录持仓市值;有信号但触发了涨跌停过滤就跳过成交;正常成交时按信号方向加减仓位,并扣除手续费和滑点。最终返回的是每日净值序列,可以直接画资金曲线。
注意涨停判断的阈值:沪深主板是 10%,代码里用 9.8% 是为了过滤掉四舍五入和 ST 股 5% 的干扰;创业板、科创板涨跌幅是 20%,阈值要改成 19.8%;北交所是 30%。这个阈值在源码里做成了参数,换市场前先改掉。
4.3 评估指标:收益、夏普比率与最大回撤
回测跑完,源码里集成了一套指标计算函数。最有参考价值的是年化收益率、夏普比率、最大回撤和胜率。夏普比率是超额收益除以收益波动,代表承担单位风险换取的回报;最大回撤衡量净值从峰值跌下来的最大幅度,决定了策略在实盘中能不能拿得住。回测里年化 20% 但回撤 40% 的策略,与年化 12% 回撤 8% 的策略相比,实盘中往往后者活得久。
| 指标 | 计算方式 | 参考界限 |
|---|---|---|
| 年化收益率 | 净值累计收益 / 持仓年数 | 回测 10%~20% 已是理想区间 |
| 夏普比率 | (年化收益 - 无风险利率) / 年化波动 | 大于 1 及格,大于 1.5 少见 |
| 最大回撤 | max(1 - 净值 / 历史峰值) | 低于 15% 更容易实盘持有 |
| 胜率 | 盈利交易次数 / 总交易次数 | 0.5 左右需结合盈亏比看 |
单看收益会被骗,最大回撤才是实盘持仓体验的核心。如果你跑出来夏普 3、回撤 5%,先不要急着发朋友圈,按第 5 章的清单逐条排查,大概率是某一项检查没过。
5. 常见问题与避坑:数据泄露到回测幻觉的四个血泪经验
这一章是整套源码的排查手册。我拆这类资源时,几乎每次都在这几类问题上翻车,也帮人排过不少——现象雷同,根因就那么几个。
5.1 预测很准,回测却亏钱
现象:模型 loss 很低,方向准确率 0.6 以上,但回测资金曲线一路向下。
原因:最常见的是标签对齐错了。make_windows 里写成y.append(data[i, 3]),预测目标变成当天收盘价,模型学到的是"今天价格是多少"而不是"明天价格会怎样",预测值与真实值高度接近,但没有任何前瞻信息,交易决策等于拿当天已实现的价格做开仓依据,自然赚不到钱。
解决:把 y 改成data[i + 1, 3],并且检查训练数据长度,确认 X 的最后一个样本与预测目标之间没有重叠。检查方法很简单:把模型预测曲线和真实价格曲线画在一起,如果预测曲线比真实曲线滞后一天左右,基本就是这个原因。
5.2 训练集表现好,验证集一塌糊涂
现象:训练 loss 降到很低,验证 loss 一直下不去,两条曲线像剪刀一样分开。
原因:两个来源串在一起。一是时序切分用了 sklearn 的随机切分,验证集里混进了训练期前后的行情,模型在"背答案";二是归一化 scaler 在全部数据上执行了 fit_transform,验证集的均值和最大值信息已经提前进入训练集,属于典型的数据泄露。
解决:切分改用第 2 章的时序切片,scaled 只对训练段执行fit_transform,验证段和测试段用同一个 scaler 只做transform。改完之后验证集 loss 变高是正常的,说明之前那个低 loss 是假象。
5.3 回测收益翻倍,实盘完全不是这么回事
现象:回测年化 80%、夏普 3,实盘跑一周就亏,净值曲线和回测对不上。
原因:回测里没有算手续费、滑点,或者涨跌停日"成交了"而实际根本买不进、卖不出。另一个隐蔽原因是前视偏差,比如在 t 日收盘前用了 t 日收盘后才会产生的数据指标做决策。
解决:把 fee_rate 至少设到 0.0015(双边含佣金和印花税),slippage 设 0.001 到 0.002,并加涨跌停过滤。如果过滤完之后收益腰斩,说明原始策略的收益集中在无法真实成交的极端行情里,这本身就是重要结论,毕设里写出来反而是加分项。
5.4 每次跑结果都不一样,复现不了
现象:同一个模型、同一份数据,隔天再跑 loss 和预测结果完全不同,报告里没法写。
原因:随机种子没锁。Python 的 random、numpy、PyTorch 各自维护随机数状态,只锁其中一个没有用。DataLoader 的 shuffle=True 也会引入顺序随机性。
解决:按第 3 章的 seed_everything 在导入库之后、读数据之前调用,DataLoader 里指定固定 seed 的 generator。这里给一段补丁代码:
g = torch.Generator() g.manual_seed(42) train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, generator=g)另外,如果你是在 pycharm 或 vscode 里配置 Python 环境跑这个项目,确保依赖版本和研报里记录的 requirements 一致。PyTorch 小版本升级会改变某些算子的实现细节,同样影响复现。锁完随机种子之后,CPU 环境基本能复现,GPU 上有小浮动属正常,但趋势不会变。
5.5 涨跌停日策略突然失灵
现象:回测里某段时间资金曲线直线跳水,凑巧发生在连续大涨或大跌的几天。
原因:涨停日买入信号触发后实际无法成交,但回测假设已经持仓,净值按涨停价计算,产生了"纸面盈利";跌停日止损单也成交不了,亏损被放大。两者叠加,回测曲线在极端行情里严重失真。
解决:在回测循环里加涨跌停过滤,把当日涨幅大于等于 9.8% 的买入信号置 0,跌幅小于等于 -9.8% 的卖出信号置 0。创业板、科创板阈值改成 19.8%,北交所改成 29.8%,做成参数即可,不要把阈值写死在回测函数里。
6. 进阶技巧:用滚动再训练验证模型稳定性,别被单次回测骗了
6.1 Walk-Forward 验证:每半年重训一次
单次训练单次测试切分,只能证明模型在最后那段行情里有效,不能证明策略能持续运转。金融市场特征是漂移的,两年前有效的规律放到现在可能完全失效。我跑这类项目,最后都会做一次 walk-forward 验证:把历史数据按"2 年训练 + 半年测试"的窗口滚动,每次滚动重新训练模型、重新回测,最后把所有测试段的资金曲线拼接成一条完整曲线。每一段窗口的代码结构是这样:
results = pd.DataFrame() for start in range(0, len(X) - TRAIN_LEN - TEST_LEN, STEP_LEN): train = X[start:start + TRAIN_LEN] test = X[start + TRAIN_LEN:start + TRAIN_LEN + TEST_LEN] model = StockLSTM() train_on(model, train) preds = predict(model, test) # 记录该窗口的夏普和最大回撤 results.loc[start] = [sharpe(preds), max_drawdown(preds)]TRAIN_LEN、TEST_LEN、STEP_LEN 分别是训练长度、测试长度和滚动步长,比如 500、125、125,对应两年训练、半年测试、每季度滚动。滚动窗口跑完,如果各窗口指标波动很大——比如第一个窗口夏普 2.5,第三个窗口变成 -0.3——说明模型只在特定行情里有效,不具备稳健性。这个验证结果放进毕业设计报告里,比单次切分有说服力得多。
6.2 预测时间对齐检查:一个必做的十分钟检查
任何时序预测项目都要做一次滞后对齐检查:把模型输出和真实值画在同一张图上,如果预测曲线看起来像真实曲线向右平移了一截,说明模型学到的是"今天的价格接近昨天",而不是预测未来。
import matplotlib.pyplot as plt plt.figure(figsize=(12, 4)) plt.plot(actuals[-100:], label="actual") plt.plot(preds[-100:], label="predict") plt.legend() plt.show()如果预测曲线明显比真实曲线滞后,优先去查 make_windows 的 y 是不是取了data[i, 3],把这个修掉之后大部分对齐问题都能解决。
我拆这个包的时候,花掉的时间大头不在模型调参,而在修正第 5 章那几类问题——尤其是标签错位和归一化数据泄露,都是回测里亏钱之后才回头定位的。从那以后,我每次跑完一个新项目都会强制先做一遍滞后对齐检查和涨跌停过滤审计,再去看收益曲线。这套源码的价值在于它把完整链路摊开了,你可以拿它交差,也能把它改造成自己的策略框架。希望帮到你。
本文还有配套的精品资源,点击获取