尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多模型股价预测实战:数据管道、验证与LSTM调参

多模型股价预测实战:数据管道、验证与LSTM调参 简介使用Python实现机器学习股价预测源码面向毕业设计、期末大作业或课程设计场景帮助计算机相关专业学生快速完成股票价格预测系统。资源融合线性回归、LSTM、ARIMA、KNN等多种主流模型完整覆盖数据预处理、模型训练、回测和预测可视化流程代码注释详尽新手也能看懂并灵活修改项目经过严格调试部署简单即可运行。压缩包共31个文件结构清晰包含5个Python脚本负责模型构建、回测、预测及工具函数、2个CSV数据文件、2个HTML可视化页面、1个Excel数据汇总、17张模型效果图与分析图以及README等文档整体仅1.45MB。项目曾以98分获导师认可界面美观、功能完善可直接作为高分毕设或期末大作业提交也可作为课程设计与工程实践的有力参考。目前已有372人学习使用整体具备较高的实际应用价值与参考意义。1. 多种模型做股价预测的源码先跑通数据管道再谈模型找一段可运行的“Python 机器学习股价预测源码”并不难难的是让 LR、LSTM、ARIMA、KNN 这些模型放在同一份行情数据上跑出可复现、可对比、不说谎的结论。我第一次把 LSTM 跑出 60% 方向准确率时以为找到了圣杯后来发现只是切训练集时把未来数据洗进了训练集这种“玄学”其实可以靠一套固定的流程规避。这篇笔记就按我在本地机器上实际会用到的方案把多模型股价预测拆开讲先解决数据管道和验证方式再依次用 LR、KNN、ARIMA、LSTM 做同等条件对比最后落在参数调整和踩坑记录上。适合正在跑量化策略、做课程设计或者想确认 LSTM 到底值不值得投入的读者。2. 数据管道与两个轻量模型先用 LR 和 KNN 把流程跑通2.1 为什么从日线开始复权、采样周期与任务定义做股价预测的第一道坎不是模型而是「你要预测什么」。常见做法有两种预测明天的涨跌方向或者预测未来 N 日的累计收益而不是直接预测收盘价的绝对值。原因是价格本身非平稳直接拟合收盘价会让模型学成“复制上一个价格”而不是学规律。这个选择直接决定后续所有模型的效果。这里套用机器学习应用流程的标准顺序先定义标签再构造特征最后才选模型。对 A 股数据我默认使用前复权日线理由有三条日线能避开分钟线的噪声和不同交易时段的非同步问题前复权能避免除权除息造成的价格跳空日线数据量对本地训练和反复调参足够友好。如果只有周线或月线样本量不够 LSTM 用上手就用分钟线又会把大量时间耗在数据清洗上都不适合做多模型对比的第一步验证。2.2 用 akshare 拉日线并构造收益率特征数据源我习惯用 akshare 拉 A 股日线不需要注册 token如果接口临时抽风也完全可以换成本地 CSV下面的代码只依赖date和close两列后续处理完全一致。import akshare as ak import pandas as pd # 拉取平安银行前复权日线symbol 是股票代码adjustqfq 表示前复权 df ak.stock_zh_a_hist(symbol000001, perioddaily, start_date20190101, end_date20240601, adjustqfq) # akshare 返回中文字段名不同版本列名可能有差异先打印确认 print(df.columns.tolist()) df df.rename(columns{日期: date, 收盘: close}) df df[[date, close]].copy() df[ret] df[close].pct_change() # 日收益率 df[ret] df[ret].fillna(0) # 第一行是 NaN填 0 df df.reset_index(dropTrue)这段代码的逻辑是先用一个免费数据接口拿全历史行情只保留日期和收盘价然后计算日收益率。后续所有模型都基于ret序列而不是close这就是 2.1 里说的“预测收益而不是预测价格”的落地。参数上值得说明的是adjustqfq表示前复权做效果回测应该优先选它symbol换成其他代码时akshare 内部会按沪深不同市场自动处理。如果拉下来的数据不足 300 条后面的 LSTM 基本没有训练意义需要把start_date往前推。2.3 逻辑回归把预测目标改造成「明天涨还是跌」逻辑回归在股价预测里通常作为第一个必须跑的基线。它给的不是神准答案而是一个成本极低的锚点如果 LSTM 在这个数据集上赢不了 LR问题大概率出在数据预处理而不是模型复杂度不够。这里把目标定义为“明天收益率是否大于 0”特征用过去 5 天收益率窗口也可以换成 10 做对比不是越长越好后面会展开。import numpy as np from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score window 5 X, y [], [] for i in range(window, len(df) - 1): X.append(df[ret].iloc[i - window:i].values) y.append(1 if df[ret].iloc[i 1] 0 else 0) X np.array(X) y np.array(y) # 时间顺序切分前 80% 训练后 20% 验证不能随机打乱 split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] scaler StandardScaler().fit(X_train) X_train scaler.transform(X_train) X_test scaler.transform(X_test) clf LogisticRegression(C1.0, max_iter1000) clf.fit(X_train, y_train) pred clf.predict(X_test) print(LR 方向准确率:, accuracy_score(y_test, pred))代码逻辑X 的每一行是连续 5 天的收益率向量y 是下一天的涨跌标志循环构造窗口特征时必须把边界写对——最后一个样本的索引不能取到i1之后否则会在不知不觉中把标签泄漏给特征。标准化只用训练集的fit结果去transform测试集避免把测试集的均值方差带入训练流程。参数方面C是正则化强度的倒数C越小惩罚越强股价特征一般从 1.0 往下调max_iter1000是为了防止默认求解器在大特征量下收敛慢而报警告如果遇到ConvergenceWarning第一个调整对象就是它。2.4 KNN 用最近邻行情做五日收益回归三个必调参数KNN 在标题里出现得有点意外但它其实是很好用的非参数基准思路很直接过去哪几天和当前这 5 天涨跌形态最像未来就按那些邻居的平均收益来走。它可以验证一个朴素经验——相似行情是否真的导致相似后续走势。这里把预测目标改成未来 5 日累计收益这样 KNN 和 LR 的结论可以互补一个看方向一个看幅度。from sklearn.neighbors import KNeighborsRegressor from sklearn.metrics import mean_squared_error horizon 5 Xk, yk [], [] for i in range(window, len(df) - horizon): Xk.append(df[ret].iloc[i - window:i].values) yk.append(df[close].iloc[i horizon] / df[close].iloc[i] - 1) Xk np.array(Xk) yk np.array(yk) split int(len(Xk) * 0.8) Xk_train, Xk_test Xk[:split], Xk[split:] yk_train, yk_test yk[:split], yk[split:] scaler StandardScaler().fit(Xk_train) Xk_train scaler.transform(Xk_train) Xk_test scaler.transform(Xk_test) knn KNeighborsRegressor(n_neighbors5, weightsdistance, p2) knn.fit(Xk_train, yk_train) yk_pred knn.predict(Xk_test) print(KNN 五日收益预测 RMSE:, mean_squared_error(yk_test, yk_pred) ** 0.5)KNN 的三个必调参数是n_neighbors、weights、p。n_neighbors5是默认起步值样本几千条时可以从 5 扫到 50看 RMSE 曲线拐点weightsdistance表示按距离加权比默认的uniform更平滑也更符合“越近的历史越有参考价值”的直觉p2是欧氏距离连续收益特征用它通常就够p1是曼哈顿距离对异常值更迟钝。KNN 的劣势也明显样本量变大后预测每个点都要计算与全部训练样本的距离回测窗口一长速度会明显变慢这也是它通常只作为基线的原因。3. ARIMA 统计基线先看趋势是否可预测再决定要不要上深度模型3.1 为什么还要跑 ARIMA模型对比的基准线很多公开的 Python 股价预测例子里ARIMA 被当成“过时统计方法”一笔带过但真实做下来ARIMA 在日线短期预测上往往不比 LSTM 差。原因不复杂股票日线收益的自相关性本来就弱复杂模型很难从弱信号里变出花来。ARIMA 在这里的价值不是拿最优精度而是提供一条传统统计模型基线如果 LSTM 连 ARIMA 都跑不赢说明数据、特征或训练环节有更大的问题没被发现。ARIMA 的核心假设简洁当前值由自回归项 AR、差分阶数 I、移动平均项 MA 组合解释。股价对数价格通常非平稳但差分后可以近似平稳这就是(p,d,q)里d存在的意义。实际操作我不会拍脑袋定p和q而是先用检验确认差分阶数再用 AIC 搜索定阶。3.2 ADF 检验与差分先确认序列是否平稳ADF 检验是判断序列平稳性的标准手段零假设是“序列存在单位根、非平稳”。p 值小于 0.05 就拒绝原假设认为序列稳定如果 p 值很大就需要对序列做差分。这一步在 ARIMA 里是硬性前提跳过它直接拟合得到的参数可能毫无意义。from statsmodels.tsa.stattools import adfuller # 对收盘价做 ADF 检验adf_result[1] 是 p 值 adf_result adfuller(df[close].dropna()) print(close p-value:, adf_result[1]) # 一阶差分后再检验一次 df[close_diff] df[close].diff() adf_diff adfuller(df[close_diff].dropna()) print(diff p-value:, adf_diff[1])代码逻辑是对原始价格和差分序列各做一次 ADF 检验。这里diff()的结果记得用dropna()去掉首个 NaN否则检验函数会因为缺省值在部分版本直接报错。如果close的 p 值大于 0.05而close_diff的 p 值小于 0.05说明一阶差分已经把价格序列改造成平稳序列d取 1。如果原始 p 值已经小于 0.05d取 0 即可这种情况在真实股价里很少见多出现在被人为平滑过的数据上。3.3 用 AIC 搜索 (p,d,q)别靠拍脑袋定阶确定了d之后p和q的取值不要靠盯着 ACF/PACF 图猜先跑一个暴力搜索更高效。statsmodels 的 AIC 越小表示模型在“拟合度 复杂度惩罚”的综合表现越好。搜索范围一般p、q都取 0 到 3超过 3 在日线样本上容易过拟合而且求解不稳定的概率会明显上升。from statsmodels.tsa.arima.model import ARIMA import itertools d 1 # 由上面的 ADF 检验决定 best_aic, best_order float(inf), None for p, q in itertools.product(range(0, 4), range(0, 4)): try: model ARIMA(df[close], order(p, d, q)).fit() if model.aic best_aic: best_aic model.aic best_order (p, d, q) except Exception: continue print(最优阶数:, best_order, AIC:, best_aic)这段代码的本质是网格搜索加异常跳过。try-except是必须的ARIMA 在部分参数组合下会出现收敛失败、矩阵奇异等问题有的版本抛ValueError有的只打印警告不用异常保护会中断整轮搜索。参数说明p是自回归阶数取历史前 p 期数值q是移动平均阶数取前 q 期残差itertools.product(range(0,4), range(0,4))共 16 个组合单只股票几千条数据时几秒就能跑完。旧版 statsmodels 的导入路径是from statsmodels.tsa.arima_model import ARIMA新版改为from statsmodels.tsa.arima.model import ARIMA如果你在跑老教程时遇到模块不存在先查这一处。3.4 用 get_forecast 拿置信区间预测的不是单点是分布ARIMA 预测最容易被忽略的一点均值预测本身不带信心真实项目里必须看置信区间。置信区间越宽说明模型对远期越没把握这也是判断“这段行情到底可不可预测”的快速手段。from statsmodels.tsa.arima.model import ARIMA train_close df[close].iloc[:split] test_close df[close].iloc[split:] model ARIMA(train_close, orderbest_order).fit() forecast_result model.get_forecast(stepslen(test_close)) pred_mean forecast_result.predicted_mean conf_int forecast_result.conf_int(alpha0.05) # 方向准确率比较预测序列和真实序列自身的涨跌方向 pred_direction (pred_mean.iloc[1:].values pred_mean.iloc[:-1].values) true_direction (test_close.iloc[1:].values test_close.iloc[:-1].values) print(ARIMA 方向准确率:, (pred_direction true_direction).mean())代码逻辑用训练段拟合get_forecast(stepslen(test_close))预测整个测试段predicted_mean是每个时点的预测均值conf_int给出 95% 置信区间上下界。这里不要用model.forecast()直接拿点数因为get_forecast才能同时拿到分布信息。参数说明steps等于测试段长度alpha0.05对应 95% 置信水平想更保守可以取 0.1。方向准确率的计算方式是各序列自己和自己比关注的是涨跌方向而不是点位误差这也是后续与 LR 对比时唯一公平的口径——预测方向和预测实际价格是两个不同的任务。ARIMA 的局限也要讲清楚它对阶段性的趋势结构敏感数据里出现大的跳空或除权除息未正确复权结果会被单个异常点拉偏。如果 AIC 搜索出来的最优阶数是 (0,d,0)基本说明差分后的序列没有显著自相关“不可预测”就是统计模型给出的最强结论此时对 LSTM 的高准确率要格外谨慎地解读。4. LSTM 时间序列预测PyTorch 最小实现与三个关键参数4.1 构造序列样本window 长度怎么选训练集验证集按时间切LSTM 是循环神经网络适合带顺序依赖的数据这是它被选进来和 ARIMA 对比的主要原因。但股价日线上的可预测性非常弱LSTM 最大的问题不是学不会而是容易把“噪声中的巧合”背下来也就是过拟合。构造样本时window不要盲目取大我一般先取 5 或 10与 LR 的特征窗口保持一致保证对比公平。def make_sequences(series, window): xs, ys [], [] for i in range(window, len(series) - 1): xs.append(series[i - window:i]) ys.append(series[i 1]) # 预测下一日收益率 return np.array(xs), np.array(ys)这个函数接收收益率序列输出窗口特征和下一日收益。每一行是一个长度为window的收益序列LSTM 会把它当作时间步依次读入。为什么不预测下一日收盘价而预测收益率因为收益率序列相对平稳也更容易解释方向准确率收盘价序列会让 LSTM 学成“上一日价格加微小修正”loss 很低却没有预测价值。边界要记得留出训练集最后一个时间步否则模型在训练时能看到标签之后的未来数据。提示如果想把目标改成“未来 N 日累计收益”ys.append里的表达式要同步换成 N 日后的累计收益不能只在训练损失函数上做文章。4.2 归一化MinMax 与 Standard 的选择以及泄漏风险收益率序列用 StandardScaler 或 MinMaxScaler 都行但必须先 fit 训练集再 transform 测试集。MinMax 会把数据压缩到 0-1 之间适合 LSTM 这类对输入尺度敏感的模型Standard 对存在负值的收益率序列更自然。我的固定做法是 MinMaxScaler 处理收益率因为它保留相对大小关系也不容易被极端值主导。from sklearn.preprocessing import MinMaxScaler # 只 fit 训练段测试段只 transform这是防止泄漏的底线 scaler MinMaxScaler(feature_range(0, 1)) ret_train scaler.fit_transform(df[ret].iloc[:split].values.reshape(-1, 1)) ret_test scaler.transform(df[ret].iloc[split:].values.reshape(-1, 1))关键在第二行scaler是用训练集拟合的测试集只用transform。很多 LSTM 翻车案例就是把这里写成对整个ret序列fit_transform让测试集的取值范围提前进入训练过程验证阶段指标虚高到了后续行情就回归原形。如果同时预测多只股票每只股票要单独 fit不要混在一起归一化再切分那会造成跨样本的信息泄漏。4.3 PyTorch 实现最小 LSTM 模型先定义模型结构。这里给一个容易改造成预测任务的 LSTM 类输入特征是一维收益率序列输出是标量收益率预测。用 PyTorch 而不是 keras是因为 pytorch 的 LSTM 源码结构更直白方便看清每一层在做什么。import torch import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, input_size1, hidden_size32, num_layers1, dropout0.0): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout if num_layers 1 else 0.0) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) return self.fc(out[:, -1, :])forward里的out[:, -1, :]表示取序列最后一步的隐藏状态再接一个全连接层输出收益预测。input_size1是因为特征只用收益率一个维度如果后面加了成交量和涨跌幅等特征input_size要对应改成特征数量。hidden_size是 LSTM 隐藏单元数默认 32先从小模型起步。num_layers1时dropout必须为 0因为 PyTorch 只允许多层 LSTM 启用 dropout这是新手很容易踩到的报错点。4.4 训练循环与参数表训练循环没有花活重点是先把数据转成 tensor并保持维度是 (batch, window, input_size)。做 LSTM 时间序列预测时我最常设的参数是 lr1e-3、epoch 100 到 200、hidden 32batch size 小一点更稳。X_train torch.tensor(X_train, dtypetorch.float32).unsqueeze(-1) y_train torch.tensor(y_train, dtypetorch.float32).unsqueeze(-1) model PriceLSTM(input_size1, hidden_size32, num_layers1) optimizer torch.optim.Adam(model.parameters(), lr1e-3) criterion nn.MSELoss() for epoch in range(150): model.train() optimizer.zero_grad() pred model(X_train) loss criterion(pred, y_train) loss.backward() optimizer.step() if epoch % 30 0: print(fepoch {epoch}, loss {loss.item():.6f})训练循环的逻辑是每个 epoch 把全部训练样本过一次网络计算 MSE loss反向传播更新梯度。optimizer.zero_grad()必须在loss.backward()之前清空旧梯度否则梯度累加会让更新方向混乱。参数上lr 从 1e-3 起步loss 几个 epoch 不降就降到 1e-4epoch 取 150 是兼顾训练和过拟合的默认值训练 loss 持续下降但验证 loss 开始抬升时就要准备早停hidden_size 从 32 往上加不一定变好多数情况只是让训练变慢。参数默认值调整说明window时间步10过小特征不足过大噪声过多hidden_size32验证集不降就不要加num_layers1数据量小时不建议超过 2 层lr1e-3loss 振荡时降到 1e-4epochs150配合验证集早停使用batch_size32日线几千条样本32 是合理下限5. 多模型股价预测的避坑清单五条踩坑记录5.1 提前 shuffle 训练集指标最漂亮但预测的是未来现象用 sklearn 的train_test_split默认参数切分LSTM 在测试集上的方向准确率接近 60%看起来比 ARIMA 强一大截。原因train_test_split默认会随机打乱样本时间序列被打散后训练集里混进了测试时段的数据模型在训练时就见过了未来更隐蔽的是相邻窗口样本之间信息高度重叠打乱后训练集和测试集的样本可能只差一个交易日泄漏几乎无法避免。解决所有切分必须按时间顺序手动切片或用train_test_split(..., shuffleFalse)并在代码注释里写明“不许随机”。这是整套源码里性价比最高的一条修复。5.2 归一化参数在全量数据上计算验证集被偷看了现象验证阶段 RMSE 很低换到后续时段回测效果明显变差模型像是一离开“考场”就失灵。原因用全量数据 fit scaler测试集的均值和范围在训练阶段已经参与了归一化尺度的计算测试结果被系统性高估。解决scaler 永远只 fit 训练集测试集只用transform。这条在 LR、KNN、LSTM 三处都可能出现ARIMA 不需要归一化所以不涉及。排查方法很简单把保存下来的 scaler 重新加载后对验证集 transform数值应该和训练时完全一致。5.3 拿全部历史做交叉验证非平稳序列不能按均布切现象用 KFold 对五年日线做交叉验证每个 fold 的准确率方差特别大有的 fold 超过 60%有的不到 45%完全看不出模型的真实水平。原因股票数据是非平稳的不同年份的市场结构完全不同随机打乱的 KFold 会让模型在某个 fold 里碰到“与训练集风格差异巨大”的时段更关键的是 KFold 默认打乱索引会造成和 5.1 一样的未来泄漏。解决改用TimeSeriesSplit保证每个训练段都严格在测试段之前如果要做多窗口对比就按年份或固定滚动步长切分。单次切分只看一个时间点的表现结论只能作为参考。5.4 LSTM 递归预测越来越平学成了均值回归现象用最后一个窗口做输入把输出再接回输入递归预测未来 30 天曲线很快就变成一条水平的线预测值停在历史均值附近。原因LSTM 的训练目标是 MSE最优解天然是条件期望而收益率序列的条件期望在大多数日子里就是零附近所以模型学会了“预测均值”这个最稳但不赚钱的答案。解决不要做长序列递归预测改成滚动预测——每天用新出现的真实收益率更新窗口再预测下一天。评估时优先看方向准确率而不是 MSEMSE 低不代表方向准。如果必须递归预测就把目标改成未来 N 日累计收益让模型被迫学幅度变化而不是逐日学“零”。5.5 ARIMA 报错却不给原因版本差异与过大的 p/q 阶数现象ARIMA 拟合时抛ValueError或收敛警告报错信息模糊满屏 warning 但不知道从哪动手。原因statsmodels 新旧版本 API 不同旧版导入路径是arima_model新版是arima.model同时p或q超过 3 时模型容易遇到矩阵不可逆或收敛失败。解决导入路径统一用新版搜索阶数限制在 0 到 3遇到奇异矩阵错误时先检查数据里有没有重复值或极端值仍不收敛就给fit传method_kwargs{maxiter: 500}提高迭代上限而不是掩盖警告。记住放弃某个参数组合继续跑也是一种合理策略不值得在一个组合上死磕。6. 用滚动窗口与多模型排名做最终选型的小技巧6.1 为什么单次切分不可信walk-forward 与时间顺序法则单次切分只验证了一个时间点的表现运气成分很大。做量化策略的常见做法是滚动窗口回测训练段长度固定每预测完一个测试段就把训练段向前滑动一格重复训练与评估。这样覆盖不同市场阶段结论相当于多个独立实验的汇总比单次切分可靠得多。6.2 一个最小 walk-forward 评估骨架def walk_forward(df, train_days500, test_days50, make_modelNone): scores [] for start in range(0, len(df) - train_days - test_days, test_days): train_df df.iloc[start:start train_days] test_df df.iloc[start train_days:start train_days test_days] model make_model(train_df) pred model.predict(test_df) score evaluate(pred, test_df) scores.append(score) return np.mean(scores), scoresmake_model是一个接收训练集、返回已拟合模型的工厂函数evaluate是方向准确率或 RMSE 的评估函数train_days500表示每次用 500 个交易日训练test_days50表示预测 50 天然后整体向前滑动 50 天。这样得到的结果不会因为某一段牛市或熊市而失真多模型在同一个框架下跑出来的均值排名才是选型依据。6.3 我现在的默认选型习惯我固定用同一套数据管道和 walk-forward 评估跑完 LR、KNN、ARIMA、LSTM 后会做三件事。第一先看方向准确率是否都接近 0.5如果四个模型集体在 0.5 附近波动说明这段时间的行情本身缺乏可预测性我会停止调参而不是硬调 LSTM 的 hidden_size继续调只是在拟合噪声。第二看方向准确率相同时谁的 RMSE 更小这决定持仓期内的波动感受。第三看排名在多个滑动窗口上的稳定性某个模型可能在一段窗口排第一换个窗口就垫底这时优先选排名稳定的胜率略低也能接受。这里有一个实战教训如果你把自己第一个跑通的模型当成基准而不是答案做对比时心态会稳很多。在真实量化策略里预测环节只是第一环后面还要接持仓逻辑、交易成本和风险控制如果只是学习这套源码建议把模型对比的结论限定在“这几段行情的样本内”不要轻易外推成“某模型适合炒股”。这个方向真正值得投入的地方是形成一套可重复、可排查的评估流程而不是一个看似完美的预测结果。希望帮到你。本文还有配套的精品资源点击获取
返回列表