简介面向LSTM初学者与时间序列预测开发者的一份开源可执行代码包以股价预测为典型案例完整演示LSTM如何借助输入门、遗忘门、输出门的选择性记忆机制学习历史价格走势并预测未来趋势。代码包覆盖数据读取与归一化、滑窗序列构造、技术指标特征工程、模型搭建、训练与预测结果可视化等全流程并附带股票历史CSV数据、训练好的h5模型权重、配置文件和checkpoint可以直接运行验证也可以基于已有权重继续训练或调整参数模块划分清晰便于按需阅读和修改。资源共153个文件其中Python脚本负责数据预处理与模型构建CSV文件提供历史行情及示例数据h5和checkpoint保存训练结果txt、xml等提供说明与配置参考整体压缩包仅5.49MB轻量且结构清晰。已有90人学习下载适合作为LSTM入门实战的对照代码也可用于课程设计、毕业设计或面试项目模板帮助快速打通从数据处理到模型部署的完整链路是理解循环神经网络在金融时序场景中落地价值的实用参考。1. 开源 LSTM 股价预测代码先分清能跑通和能用这两个目标在 GitHub 上搜“LSTM 股价预测”能翻出一堆挂着“可执行代码”标签的开源项目但真正能直接跑通、并且预测结果经得起验证的其实不到一半。所谓“开源可执行”通常只是说仓库里有一份完整脚本数据可以拉、模型可以训、图可以画而“能用”是另一回事它要经得起换数据、换周期、看方向准确率的检验。下面要讲的就是从“能跑通”到“能用”之间那段路LSTM 的选型理由、数据切成什么格式、参数怎么设、以及最常见的几个翻车点。这篇文章适合刚接触 LSTM 时间序列预测的开发者也适合已经在本地跑通过开源代码、但总觉得预测结果不靠谱的从业者。2. LSTM 做股价预测的原理与代码骨架为什么门控机制救不了非平稳序列2.1 LSTM 到底在学什么门控机制的直观理解LSTM 在 lstm 神经网络 的各类资料里经常被描述成“解决梯度消失的 RNN”这个说法没有错但放在股价预测场景里容易让人产生误解它能记住长期依赖并不意味着股价里的长期依赖是稳定存在的。LSTM 的每个时间步维护两个状态隐藏状态 h_t 和细胞状态 c_t。细胞状态负责跨时间步传递信息遗忘门和输入门决定哪些旧信息被丢弃、哪些新信息被写入输出门决定当前时刻输出什么。直观地说它是对“用过去多少个时间步的信息来推断当前输出”这件事做可学习的加权而不是像朴素 RNN 那样只是简单叠加。正是这套门控机制让 LSTM 在长序列上比普通 RNN 抗遗忘能保留更早时间步里的有效特征。这带来一个很具体的后果当序列存在稳定的周期性或趋势性时LSTM 很容易学到那个规律。比如温度预测、设备寿命预测、流量预测这些场景里序列的自相关性很强LSTM 效果普遍不错。但股价是另一类序列它的均值、方差、波动模式都在变训练集里的规律到测试集里可能完全失效。所以做这个标题下的实战第一步不是“把网络搭起来”而是先认清你处理的是一个非平稳、低信噪比的序列。2.2 股价序列的三重特殊性非平稳、低信噪比、无固定周期金融价格序列和传感器数据最大的差异有三点这三点会直接决定你后续能不能用同样的开源代码换数据复现。第一是非平稳。序列的统计特性随时间变化一段时间的价差范围和波动率到另一段时间完全不一样。举个具体例子训练段是温和震荡行情测试段突然进入单边上涨模型在训练段学到的价格波动幅度在测试段完全不成立测试集 loss 很难看就是从这个缺口来的。第二是低信噪比。价格里真正可预测的成分占比很小大部分是噪声。LSTM 在这种输入下很容易把噪声也当成模式记下来产生过拟合而损失函数又看不出问题因为它在训练集上拟合得很好。你会发现验证集 loss 和训练集 loss 的差距越拉越大但单看训练集曲线你会误以为模型已经学到位了。第三是没有固定周期。日线数据的周期长度不固定周线、月线的规律也在漂移。LSTM 对周期性敏感但前提是周期真实存在且稳定股价周期更多是事后解释出来的不是事前可用的。这三点解释了为什么很多开源 LSTM 股价预测代码跑出来的曲线普遍好看但拿去验证总出问题训练集贴合测试集漂移方向和幅度都不可靠。认清这一点你再看代码时就不会被漂亮的训练集拟合图带偏。2.3 技术栈选型PyTorch 还是 Keras开源项目里常见两套实现一套基于 TensorFlow/Keras一套基于 PyTorch。我一般这样判断方向PyTorchKeras/TensorFlow上手难度中需要自己写训练循环低compile/fit 一条龙调试灵活性高前向传播可控中回调机制能满足大部分需求时间步处理需要手动管理 batch_first对 RNN/LSTM 封装更友好社区代码量目前新开源项目占比更高老项目存量多如果你只是想把开源代码跑起来看个效果Keras 是快速路径如果你后续要改网络结构、加注意力、做多步预测用 PyTorch 会更顺手。我下面的示例用 PyTorch因为目前检索里 pytorch lstm 源码 的诉求更集中而且你要做验证和回测时PyTorch 的 tensor 操作和 numpy 互转更直接不需要在框架边界上反复做类型转换。2.4 开源项目常见结构与最小执行骨架大多数开源的 lstm 股价预测代码结构都逃不出这套流程数据读取 → 构造滑动窗口样本 → 标准化 → 定义模型 → 训练 → 预测 → 画图对比。你看懂这条主线再去看哪个仓库都不陌生。一个最小骨架如下pip install torch numpy pandas scikit-learn matplotlibimport numpy as np import pandas as pd import torch import torch.nn as nn # 1. 读数据 df pd.read_csv(stock.csv, parse_dates[date]).sort_values(date) close df[close].values.reshape(-1, 1) # 2. 切分数据只按时间切不洗牌 train_len int(len(close) * 0.8) train_raw close[:train_len] test_raw close[train_len:] # 3. 标准化只用训练集 fit后面详细介绍 from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler() train_scaled scaler.fit_transform(train_raw) test_scaled scaler.transform(test_raw) # 4. 构造序列样本 def make_sequences(data, lookback): X, y [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i, 0]) y.append(data[i, 0]) return np.array(X), np.array(y) lookback 20 X_train, y_train make_sequences(train_scaled, lookback) X_test, y_test make_sequences(test_scaled, lookback)逻辑说明这段代码前 6 行的核心是先按时间顺序切分 train/test避免后续任何“用未来数据训练”的操作。make_sequences 把连续价格序列切成 (样本数, lookback, 特征数) 形状的监督样本每个样本的输入是过去 lookback 个价格输出是下一个价格。这里特意先划分再构造窗口而不是在全量数据上构造窗口后划分就是为了防止测试集样本里掺入训练段信息。参数说明lookback 是窗口长度常见取值在 10 到 60 之间train_len 用 80% 是常见默认做法但后面会提到它并不是通用最优值。这个骨架对应的就是网络上常见的“lstm 时间序列预测 python”教程套路先跑通它再往下调整细节。3. 把股价切成 LSTM 的监督样本窗口长度、标准化与训练/测试划分3.1 原始行情数据先解决数据源和字段开源代码里的数据来源五花八门有直接在线拉取的也有内置 CSV 的。我的建议是优先用你本地能拿到的行情文件把字段统一成 date、open、high、low、close、volume 六列再开始做窗口化。在线接口不稳定而且返回的字段格式在不同版本里可能不一样本地文件至少能保证重复实验时数据一致。df pd.read_csv(stock.csv, parse_dates[date]) df df[[date, close]].sort_values(date) df df.dropna(subset[close]) # 停牌日、空值直接剔除这里需要注意停牌和缺失日期的处理。如果直接用行号当作连续交易日中间的空缺等于让模型凭空多了一段“等待期”。简单做法是保留自然日并只保留有交易的记录更讲究的做法是重采样到交易日历上对齐但作为第一版脚本去空行就够了。另一个容易被忽略的点排序。很多来源的 CSV 不保证时间升序不 sort_values 直接切窗口样本内部的时间顺序是乱的模型学到的“顺序”毫无意义。代码里 sort_values(date) 加上 inplace 之前先确认日期列真的被 parse_dates 解析成了 datetime 类型。3.2 滑动窗口为什么 LSTM 需要“监督样本”LSTM 不是直接把价格序列喂进去。它需要把连续序列切成 (X, y) 的样本对——用前 lookback 个价格预测下一个价格。这一步是把时间序列问题转换成监督学习问题的关键。def make_sequences(data, lookback): X, y [], [] for i in range(lookback, len(data)): X.append(data[i - lookback:i, 0]) # 前 lookback 天 y.append(data[i, 0]) # 第 lookback1 天 return np.array(X), np.array(y) X_train, y_train make_sequences(train_scaled, lookback) X_test, y_test make_sequences(test_scaled, lookback) print(X_train.shape, y_train.shape) # 例如 (752, 20, 1) (752,)逻辑说明X_train 的每一行是一个长度为 lookback 的价格窗口y_train 是对应的下一日价格。后续 DataLoader 会把 X 扩展成 (batch, lookback, 1) 作为 LSTM 输入所以这里第三维先保留为 1。注意 train_scaled 和 test_scaled 要分开调用 make_sequences绝不能用全量数据构造完再切分。窗口长度的选择和序列本身的“记忆长度”有关。日线数据用 20 到 30 天比较常见对应一个月的交易日分钟线可以把窗口拉长到 60 或 120。窗口太长不会明显提升效果反而增加训练时间和过拟合风险。窗口太短比如 3、5 天模型基本学不到什么。另一个判断方法是算一下序列的自相关衰减速度价格序列的自相关系数通常衰减很慢但有效信息大部分集中在前 20 到 30 天里再往前的贡献基本被噪声覆盖。3.3 标准化最容易埋雷的一步股价的数值范围会随着时间变化今天的 20 元、明天的 25 元绝对值差异很大直接喂给 LSTM 会导致梯度不稳定。标准做法是把数据缩放到 0 到 1 之间但缩放的方式决定了你的测试是否“干净”。from sklearn.preprocessing import MinMaxScaler train_len int(len(close) * 0.8) train_raw close[:train_len] test_raw close[train_len:] scaler MinMaxScaler(feature_range(0, 1)) scaler.fit(train_raw) # 只在训练段 fit train_scaled scaler.transform(train_raw) test_scaled scaler.transform(test_raw)这里的要点是 scaler.fit 只用在训练数据上。如果你先拿全量数据 fit 再切分测试段的 min/max 信息已经泄露到标准化参数里预测结果会偏乐观这在回测里属于典型的“未来函数”。很多开源代码都在这一步埋了雷粘贴下来直接跑看着效果好换数据就露馅。为什么不直接用 StandardScaler因为 MinMaxScaler 把数据压到 0-1 区间后和 LSTM 默认的 tanh 激活函数输出范围更匹配训练初期梯度更稳定。StandardScaler 也能用但 MinMax 在股价这种有明确上下界的场景里更直观。还有一个好处反标准化时inverse_transform 能把预测值还原成真实价格区间不容易出现预测范围越界的问题。最后一个细节反标准化时要用同一个 scaler 实例。预测结果是标准化空间里的数字直接画图和真实价格画在一起是错位的必须先映射回去pred_price scaler.inverse_transform(pred_scaled.reshape(-1, 1))在这一点上正确做法是在训练集上确定 min/max在测试集上只做转换。我在实际改造开源代码时还会额外打印 scaler.data_min_ 和 scaler.data_max_就是为了确认它到底是在全量数据上 fit 的还是在 train 段上 fit 的。3.4 训练/测试划分按时间顺序切不是随机切分类问题里随机切分没有风险时间序列里随机切分等于“拿未来预测过去”。开源代码里最常见的一个错误就是先随机抽样再切窗口导致模型偷偷看到未来数据。正确做法是按时间顺序前面 80% 作为训练、最后 20% 作为测试。如果需要调参再用验证集从训练集末尾再切出一段。比如一个 1000 天的序列常见划分是前 750 天训练后 250 天测试如果要做早停再从训练段末尾切出最后 50 天当验证集绝对不用测试集做模型选择。这里的 80/20 只是惯例关键是顺序不能乱。按顺序切分还有个隐含要求两个集合都从各自序列的第 lookback 个位置开始构造样本。如果你在 train_scaled 和 test_scaled 上分别调用 make_sequences中间会自然丢掉 lookback 个点这不是 bug是窗口构造的正常代价。如果看到测试集样本数比预期少首先检查这里而不是怀疑代码写错了。4. 用 PyTorch 跑通最小 LSTM 模型网络结构、训练循环与参数设置4.1 模型定义一个单变量 LSTM 怎么写这里给出一个可以直接跑的最小模型输入是单变量只有 close输出是下一日的 close。import torch import torch.nn as nn class LSTMPrice(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x: (batch, lookback, 1) out, _ self.lstm(x) out out[:, -1, :] # 取最后一个时间步 return self.fc(out)逻辑说明nn.LSTM 输出所有时间步的隐藏状态 (batch, lookback, hidden_size)out[:, -1, :] 取出最后一个时间步的输出再接一个全连接层映射为 1 个标量。这里只取最后一步是因为我们的任务是“看完窗口后预测下一步”而不是输出整个窗口的逐点预测。dropout 只在 num_layers 1 时生效它作用于层间而不是时间步之间这也是很多人设置了 dropout 却发现训练时失效的原因。参数说明hidden_size 控制记忆容量64 是起步值num_layers2 是常见配置再深在股价预测里收益很小batch_firstTrue 表示输入形状是 (batch, sequence, feature)这样和 numpy 切出来的样本一致省去转置的麻烦。4.2 训练循环不炫技的写法很多开源代码喜欢在训练里加各种花活但股价预测这个场景最朴素的过程最可靠。model LSTMPrice(input_size1, hidden_size64, num_layers2, dropout0.2) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() X_train_t torch.tensor(X_train, dtypetorch.float32).unsqueeze(-1) y_train_t torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) dataset torch.utils.data.TensorDataset(X_train_t, y_train_t) loader torch.utils.data.DataLoader(dataset, batch_size32, shuffleTrue) epochs 60 for epoch in range(epochs): model.train() total_loss 0.0 for xb, yb in loader: optimizer.zero_grad() pred model(xb) loss criterion(pred, yb) loss.backward() optimizer.step() total_loss loss.item() * len(xb) if (epoch 1) % 10 0: print(fepoch {epoch1}/{epochs} loss {total_loss/len(X_train_t):.6f})逻辑说明每个 batch 完成前向、计算 MSE loss、反向传播、参数更新最后打印平均 loss。这里要注意 shuffleTrue 是有争议的窗口样本内部有序但样本之间的时间顺序被打乱了。对 LSTM 来说单个窗口内部的时间顺序没有被破坏所以一般可以接受如果你比较谨慎可以改为 shuffleFalse但训练收敛会慢一些。参数说明训练循环里的 loss 只反映训练集的拟合程度不代表预测能力所以我在每次打印里只看它有没有收敛到稳定值不做早停早停用验证集判断。epochs 设 60 是起步值loss 到后期不再下降时就可以停了。4.3 六个必调参数每个参数改的是“什么”跑通过开源代码之后真正要花时间的是参数调整。这里按影响程度排序说明。lookback决定模型看多长的历史。20 是日线起步值分钟线可以试 60 到 120。窗口太短模型缺上下文太长容易引入过多噪声。调参时可以先看不同 lookback 下验证集的方向准确率变化而不是只看 loss。hidden_sizeLSTM 的记忆容量。64 通常够了128 会明显变慢但收益不高数据量大可以往上加数据少别超过 64否则过拟合。判断过拟合的简单方式是看训练集和验证集 loss 的差距差距拉大就减 hidden_size。num_layers层数。2 是性价比最高的选择1 层欠拟合3 层以上在股价序列上没有明显优势还容易让训练不稳定。加深网络解决不了数据本身的低信噪比问题这一点要克制。dropout层间随机丢弃比例。0.2 是常见值如果验证集 loss 明显高于训练集往 0.3 到 0.5 调。它是“后悔药”不是参数越大越好太大反而会让模型欠拟合。learning rateAdam 默认 1e-3 能跑但 loss 震荡时降到 5e-4 或 1e-4。反向传播后梯度不稳定时调小 lr 是最快的稳定手段。注意 loss 曲线如果出现锯齿状波动优先降 lr而不是加 batch_size。batch_size32 起步。显存小就降到 16数据量小几千样本时也用 16shuffle 后批次分布更均匀。调整 batch_size 后要重新跑完整训练它和其他参数是耦合的。这些参数的调整顺序我会固定成先 lookback再 hidden_size 和 num_layers最后 batch_size 和 lr。调参数这件事本身就带点玄学但只要每次只动一个变量记录下验证集指标就能避免改完一堆参数不知道是谁起的作用。4.4 从开源代码到自己的脚本改造的三个方向拿到一个开源实现不建议直接替换数据就训练至少做三处改造。第一把标准化拆成“训练集 fit 全流程 transform”。大多数开源脚本是一次性 fit这是预测偏乐观的根源必须拆开。具体做法就是第 3.3 节里的写法scaler.fit(train_raw)再分别 transform train 和 test。第二把测试集预测改成分段滚动。很多代码用一整段测试序列做一次性前向得到的预测是“在真实价格上下文中预测下一步”这符合真实使用场景。但如果你做的是多步预测就不能这么算需要把上一步的预测结果作为下一步输入后文会给出多步预测的代码。第三固定随机种子。股价预测实验的方差很大不固定种子两次训练结果差异巨大你会误以为自己调参调出了效果。def set_seed(seed42): import random random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) set_seed(42)这段放在脚本最前面能保证同一份数据在不同次运行中得到几乎相同的训练轨迹。它是排查问题时的第一道闸否则你很难判断一次效果变好是因为参数调对了还是运气好。4.5 预测与反标准化把数字还原成价格训练完之后最小可用的预测代码是model.eval() with torch.no_grad(): X_test_t torch.tensor(X_test, dtypetorch.float32).unsqueeze(-1) pred_scaled model(X_test_t).numpy().flatten() pred_price scaler.inverse_transform(pred_scaled.reshape(-1, 1)).flatten() true_price scaler.inverse_transform(y_test.reshape(-1, 1)).flatten() import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(true_price, labeltrue) plt.plot(pred_price, labelpred) plt.legend() plt.show()逻辑说明测试集样本的输入是真实历史价格输出是预测的标准化价格inverse_transform 用训练集拟合的 scaler 把预测值还原为价格。画图后你会立刻看到滞后现象这很正常下一章讲它的原因和处理方式。如果训练集和测试集的反标准化结果都对不上优先检查 scaler 的 data_min_ 和 data_max_ 是否来自训练集。另外y_test 也是标准化后的值举个例子train 段价格在 20 到 50 之间test 段价格涨到 60scaler 用 train 的 max50 做分母测试集的真实 y 可能超过 1这是允许的不影响反标准化只是你不要看到 y_test 里有大于 1 就觉得代码错了。5. LSTM 股价预测常见问题排查从滞后预测到数据泄漏的五个坑这几个坑基本是血泪经验每一个都能在开源代码里成批找到按发生频率排序。5.1 滞后预测loss 很低但总在“追昨天的价格”现象训练结束时 loss 很低测试集预测曲线和真实曲线高度重合但仔细观察预测曲线整体比真实曲线晚一天像把真实曲线向右平移。RMSE 指标不错实际没法用。原因预测目标是“下一日收盘价”而下一日和当日价格高度相关模型学会的最优策略是“输出接近上一日价格”。这在统计上就是自相关带来的滞后也是股价预测里最典型的现象。解决不要只看 RMSE改看方向准确率预测涨跌方向和真实涨跌方向一致的百分比。同时可以做差分数据把 close 转成收益率序列预测收益率而不是价格这能从根源上削弱序列的自相关性。快速检测是否有滞后可以算一个错位相关性import numpy as np same_corr np.corrcoef(pred_price, true_price)[0, 1] shift_corr np.corrcoef(pred_price[:-1], true_price[1:])[0, 1] print(same:, same_corr.round(3), shift:, shift_corr.round(3))如果 shift_corr 明显高于 same_corr说明预测比真实晚了一天这时候继续调模型结构意义不大先改预测目标。5.2 换一只股票模型就失效现象用某只股票训练出的模型在另一只股票上预测效果大跌甚至不如“用昨天价格当今天预测”的基线。原因LSTM 把第一只股票的特异性波动也学进去了这类特征在其他股票上不存在。并不一定是代码问题而是模型过拟合了个股行情。日线价格的特异性波动占比很高模型很容易记住“这段走势长什么样”而不是“什么因素推动价格变化”。解决做多股票验证。把数据扩展成多只股票按时间顺序统一切分训练/测试模型里把股票代码作为分组信息输入或者至少做多个单股实验取平均指标评估。如果手头只有一只股票的数据建议把训练段切细做滚动验证看模型在不同时间段的表现是否稳定。5.3 预测价格和真实价格完全不在一个区间现象预测结果反标准化后范围很奇怪比如真实价格在 30 到 60 元预测值在 0.4 附近。原因scaler 在真实价格上 fit但反标准化时却用了另一个 scaler或者预测的标准化结果不属于训练集那次 fit 的维度范围。也常见于 y_test 由全量数据构造、scaler 却只 fit 训练集导致两者标准化后的值不对齐。解决统一成一个 scaler 实例fit 训练集后同时 transform 训练集和测试集预测后也用同一个实例 inverse_transform。排查时打印 scaler 内部参数print(scaler.data_min_, scaler.data_max_) if scaler.data_min_[0] train_raw.min() or scaler.data_max_[0] train_raw.max(): print(scaler 可能在全量数据上 fit 过)这个检查能快速定位 90% 的反标准化问题。剩下的情况是数据类型错误inverse_transform 要求输入是二维数组shape 为 (样本数, 1)如果你直接传一维数组会得到奇怪的结果或者直接报错。5.4 DataLoader 和设备切换导致的内存/显存问题现象同一套代码在 CPU 上能跑但一放到 GPU 就 OOM或者 batch_size 从 32 改成 64 后内存直接爆掉。原因LSTM 的中间变量会随 lookback 和 batch_size 线性增长。数据从 numpy 转 torch.Tensor 时如果反复在 CPU/GPU 之间拷贝也会累积显存。最常见的写法是把整个训练循环里每次取 batch 都重新 .cuda()导致旧张量没有被及时释放。解决先把 batch_size 减半确认输入张量只创建一次训练循环里不要用 np.array(xb) 这种来回转换。真的要上 GPU用 model.cuda() 并把 xb 一次性转过去device torch.device(cuda if torch.cuda.is_available() else cpu) model LSTMPrice().to(device) for xb, yb in loader: xb, yb xb.to(device), yb.to(device)循环内避免重复搬运这样显存占用会稳定很多。如果你在 CPU 环境下跑batch_size 从 32 改到 64 变慢是正常的不要误判为死循环。5.5 指标好看到发慌你在测试集上用了未来信息现象测试集的 loss 和方向准确率都异常高高到不真实比如方向准确率超过 70%。原因标准化 fit 到了全量数据或者测试集的样本窗口跨越了训练/测试的分界点导致未来数据参与了样本构造。有些仓库为了让预测曲线“更完整”在切分前就调用 make_sequences这会直接导致测试集最后一个样本的窗口包含训练段末尾的数据。解决检查切分点。训练集样本只应来自 train_raw测试集样本只应来自 test_raw两个集合都从 lookback 索引开始。更稳妥的办法是分开两次调用 make_sequences不要在全量数据上切片。另外测试集指标好看还有一个原因是测试段本身趋势太规律比如持续单边上涨模型只需要记住“涨”就能拿高分这时候要换一个包含震荡行情的测试段重新验证。6. 验证模型是否真的能用方向准确率、回测与多步预测6.1 方向准确率与基线对比价格层级的预测目标容易滞后所以第一指标应该用方向准确率。它统计预测值和真实值相对上一日的涨跌方向是否一致。def direction_accuracy(pred, true): pred_dir np.sign(np.diff(pred)) true_dir np.sign(np.diff(true)) return np.mean(pred_dir true_dir) da direction_accuracy(pred_price, true_price) print(fdirection accuracy: {da:.2%})评价标准很直接超过 50% 才算有信息量日线随机猜方向的准确率接近 50%。连续预测能稳定超过 52% 才有一点实际意义。我见过很多开源代码在这个指标上只有 49% 到 51%等于白跑。这个基线一定要和“昨天价格就是今天预测”的朴素策略做对比跑不赢基线就继续调下去没有意义。6.2 快速回测别只画预测曲线画预测曲线只能看拟合回测能暴露真实可用性。最简单的方法是把预测方向转成每日收益的策略净值daily_ret np.diff(true_price) / true_price[:-1] pred_dir np.sign(np.diff(pred_price)) true_dir np.sign(np.diff(true_price)) hit (pred_dir true_dir).astype(float) strategy_ret daily_ret * hit # 方向对则获得当日收益方向错则空仓 cum_nav np.cumprod(1 strategy_ret)这里的关键是方向和收益的日期对齐t 日收盘产生预测方向t1 日的收益才能兑现。如果直接用当天的预测方向和当天的收益计算就是未来函数净值曲线会好看得离谱。另外这个计算没有扣手续费和滑点真实交易里还要再折掉一部分收益。6.3 多步预测把代码从单步改成滚动很多实际需求不是预测明天而是预测未来 5 天。常见做法是保持单步输出、预测时迭代把上一步输出作为下一步输入def multi_step_predict(model, init_window, steps, scaler): model.eval() window init_window.copy() preds [] with torch.no_grad(): for _ in range(steps): x torch.tensor(window, dtypetorch.float32).unsqueeze(0).unsqueeze(-1) p model(x).item() preds.append(p) window np.roll(window, -1) window[-1] p return scaler.inverse_transform(np.array(preds).reshape(-1, 1)).flatten()多步预测的误差会累积第一步预测偏了一点后续步骤会把偏差放大。所以我一般先看第 1 步的准确率是否稳定再决定要不要看第 5 步。过程中最好同时输出每一步的偏差而不是只给最终净值曲线。这套方法论换到设备寿命预测、流量预测时套路完全一样同样的窗口构造、同样的 LSTM 结构、同样的标准化与回溯验证只需要把“价格”替换成“振动特征”或“流量值”。如果你是从股价预测代码起步的这套验证流程可以直接迁移到其他时序场景。我自己做这类实验养成的习惯是永远先跑一个“上一日值复制”的基线如果 LSTM 跑不过它就不值得继续调参。这个习惯帮我挡掉了很多自我感动希望帮到你。本文还有配套的精品资源点击获取