股价预测项目实战:LR、LSTM、ARIMA、KNN四模型对比与避坑指南
简介一份适用于毕业设计、期末大作业及课程设计的机器学习股价预测项目源码整合线性回归LR、LSTM、ARIMA、KNN等多种模型配有完整文档与代码注释新手也能快速上手。项目已通过调试部署简单运行后可获得多模型预测结果与可视化对比包含每日资金情况、持有期收益率等交互图表界面直观、功能完整。压缩包共31个文件大小约1.45MB涵盖Python脚本模型构建、回测、绘图、PNG图表趋势分析、自相关、分解等、HTML可视化页面、CSV与Excel数据文件、Markdown说明及依赖清单目录结构经过整理便于按需查阅。已有117人学习/浏览适合希望快速搭建股价预测流程、完成课程作业或进行多模型比对的开发者可直接作为项目基础或二次开发参考。1. 预测股价项目不是要你百发百中是把四种模型放上同一把尺子做量化入门的人迟早会撞上这个组合LR、LSTM、ARIMA、KNN。四个模型分属线性回归、深度学习、统计时序、近邻检索四条路线却总被同一个任务拉到一起——预测股价。拿到这类源码文档项目很多人第一反应是跑通LSTM看损失结果先被ARIMA的定阶绕晕又被KNN的尺度坑一次。这个标题真正的价值不在某个模型多神而在它给了你一套对比基线ARIMA定线性下限LSTM探非线性上限LR和KNN补中间假设。适合谁量化入门、面试项目、想完整走一遍机器学习流程的人。后面从数据切分讲到参数设置再落到翻车现场每步都给能抄的代码。2. 先把数据切对股价预测预处理里那四个决定生死的选择数据预处理决定项目成败这一点在股价预测里比在其他机器学习任务里更明显。金融时序数据自带三个其他数据集少有的特性非平稳、制度性截断、时间顺序不可打乱。这三个特性处理不好后面无论LSTM多强都救不回来。拿到项目源码先别急着建模把四个选择做对复权方式、停牌与涨跌停处理、标签设计、切分与归一化顺序。这类项目自带的文档说明一般会交代数据来源、复权方式和参数设定建议先读数据来源那一节确认它用的复权口径再碰代码这步能省掉后面很多排查时间。2.1 行情数据清洗复权、停牌、涨跌停怎么处理才不会带偏模型先拉数据。常见做法是通过tushare、akshare这类行情接口拉日线或者直接用项目里附带的csv。无论哪种来源先做排序和去重接口偶尔会返回重复行直接按日期去重就行。import pandas as pd df pd.read_csv(stock_daily.csv, parse_dates[date]) df df.sort_values(date).drop_duplicates(subsetdate).reset_index(dropTrue) # 停牌日成交量通常为0对训练没有意义直接剔除 df df[df[volume] 0].reset_index(dropTrue) # 涨跌停单独标记不删除 df[limit_flag] 0 df.loc[df[close] df[close].shift(1) * 1.095, limit_flag] 1逻辑说明停牌日直接删除而不是填空值是因为停牌期间没有真实交易价格是冻结的插值会让模型以为价格在两条真实交易之间平滑过渡学出一个根本不存在的连续轨迹。涨跌停则反过来不能删——删了会破坏时间序列的连续性让LSTM的序列窗口缺一段。正确做法是加一个limit_flag标记列。1.095是按10%涨跌幅做的近似判断如果标的来自科创板或创业板改成1.195ST股改成1.045。复权我一般用前复权。原因很具体后复权价格会随着每次除权除息不断累加几十年的老股票价格能到几千块最新价格和早期价格差好几个量级KNN和LSTM这类对数值尺度敏感的模型会被直接带偏前复权以最近一次除权为基准往回调整历史价格统一在一个量级上适合喂给模型。前复权有个坑要注意除权次数多且历史长的标的早期价格可能被调成负数遇到直接换数据源或改拉后复权。2.2 特征与标签设计预测收盘价还是预测涨跌方向股价预测项目的标签有两种常见设计回归任务预测下一天收盘价分类任务预测下一天涨跌方向。我强烈建议用分类。原因有两个回归任务里RMSE再低只要方向判断错了照样亏钱价格本身是非平稳序列让模型去拟合绝对价格等于逼它复现一个随机游走训练难度和过拟合风险都远大于预测方向。特征不要一上来堆几十个指标。常见做法是先做四类基础特征动量、均线偏离、波动率、成交量变化。这四类对应市场里真实存在的趋势、均值回归、风险状态、资金活跃度四个维度先跑通再逐步加。import numpy as np # 动量对数收益率比简单收益率更接近正态分布 df[log_ret] np.log(df[close] / df[close].shift(1)) # 均线偏离价格相对5日均线的位置衡量短期超买超卖 df[ma5] df[close].rolling(5).mean() df[ma_bias] (df[close] - df[ma5]) / df[ma5] # 波动率近10日收益率标准差衡量风险状态 df[volatility] df[log_ret].rolling(10).std() # 成交量变化当日成交量相对5日均量的倍数 df[volume_ratio] df[volume] / df[volume].rolling(5).mean() # 标签明日收盘是否高于今日二分类 df[label] (df[close].shift(-1) df[close]).astype(int) df df.dropna().reset_index(dropTrue)参数说明ma_bias用5日窗口反应快但噪声大改成20日就是更慢的趋势信号对应预测周期也要拉长。volatility窗口10天捕捉近两周的波动如果你关注月度风险改成20到30天。核心原则就一条特征窗口和你的预测周期匹配预测下一天用5到10天短窗口预测一周后用20天以上。label用了shift(-1)这意味着最后一行数据的标签是NaN被dropna吃掉是正常的。2.3 训练测试切分与归一化未来函数和shuffle泄漏都埋在这一步这是整个项目最容易被新手写错的地方。很多人在sklearn的train_test_split里习惯性shuffleTrue这在股价预测里是致命的。时间序列一旦打乱模型就能在训练时偷看未来的分布测试集失去意义回测指标全线虚高。正确做法是顺序切分归一化只fit训练集。from sklearn.preprocessing import StandardScaler feature_cols [log_ret, ma_bias, volatility, volume_ratio, limit_flag] train_size int(len(df) * 0.7) train_df df.iloc[:train_size].copy() test_df df.iloc[train_size:].copy() # 关键scaler只fit训练集测试集的均值方差来自训练分布 scaler StandardScaler() scaler.fit(train_df[feature_cols]) train_x scaler.transform(train_df[feature_cols]) test_x scaler.transform(test_df[feature_cols]) train_y train_df[label].values test_y test_df[label].values这里有个细节值得展开测试集在transform时用的是训练集的均值方差这是合规的。但很多项目源码里会写成先对整个df做scaler.fit再切分等于测试集的统计信息提前进入了训练流程这是最典型的未来函数泄漏。你在读文档时如果看到这种写法直接改掉。另外标准化只对连续特征有意义limit_flag这种0/1标记列不进scaler否则一个取值只有0和1的列被拉伸后反而失真。提示滚动回测时每个窗口都要重新fit scaler和模型不能用第一次训练的scaler处理后面所有窗口。窗口越往后旧scaler的均值方差越偏离当前市场的真实分布。3. 从ARIMA到LSTM四个机器学习模型的假设边界与选型理由拿到项目源码很多人第一件事是挑一个模型跑起来跑完看指标再换下一个。这样跑完四个模型除了记住几个数字什么都没得到。选模型之前先搞清楚每个模型的假设才知道预测结果里哪些信号是真本事哪些是巧合。这四个模型不是四个按钮是四套完全不同的世界观选型错了调参调不出上限。3.1 ARIMA为什么值得先跑线性时序下限与定阶逻辑ARIMA自回归积分滑动平均是统计时序的经典方法它的假设很明确当前值可以由过去若干期的值、过去若干期的预测误差、加上差分阶数组合出来。金融价格高度噪声这个假设对价格序列来说太强了但正因为假设强它给了一个别的东西替代不了的东西——线性基线。项目里如果没有ARIMA做对照你根本不知道LSTM的提升是来自非线性建模能力还是单纯捡了数据预处理的红利。ARIMA会逼你先过这三关ADF检验判断平稳性差分定d阶再用ACF和PACF定p和q。这三步本身就是一次完整的时序分析训练做完你对数据的理解会明显深一层。实操中我一般先用pmdarima的auto_arima扫一遍让它按AIC自动选(p,d,q)再做一次残差检验确认信息被榨干。auto_arima不是黑匣子本质上就是网格搜索配AIC打分只是把脏活封装起来了。3.2 LR和KNN把股价预测当成回归与相似性检索LR在这个项目里通常是逻辑回归预测涨跌方向。它的假设是特征与上涨概率之间存在对数线性关系。这个假设对股价这种系统来说过于简单但LR有两个无法替代的优点可解释性强系数直接告诉你哪个特征在驱动预测训练极快滚动回测里跑几百轮毫无压力。如果你是做股票量化分析的LR的系数变化本身就是一种市场状态信号这个价值被很多人忽略。KNN换了一个思路不做任何线性假设而是假设相似的历史时段会重复出现相似的结果。这个假设在震荡市里常成立在单边趋势市里容易被打破。三个参数最关键K值、距离度量、特征标准化。K值从5起步特征维度不高时欧氏距离够用维度高了再考虑余弦相似度标准化是KNN的命门具体坑放在避坑章节展开。KNN是四个模型里唯一没有训练过程的预测时直接查历史样本所以它受异常历史点的影响也最大。3.3 LSTM的胜负手序列建模能力与它带来的过拟合代价LSTM是四个模型里唯一真正为序列数据设计的模型。门控结构让网络可以选择性地记住长期信息这在时间序列上天然占优。但这项能力是把双刃剑参数数量大训练数据量又小过拟合几乎必然发生。股票数据一年大约250个交易日十年也就2500行这个量级喂给深度网络想不过拟合都难。所以LSTM在股价预测里的正确用法不是追训练集损失最低而是严格控制模型容量。我从一开始就用单层LSTMhidden_size给16或32dropout加在0.2以上训练轮数限制在50以内。这些限制不是拍脑袋是样本量约束下的现实选择。四个模型的定位可以收成一张对比表模型核心假设输入形式主要超参数过拟合风险项目中的定位ARIMA线性自回归加误差修正单变量价格序列p、d、q低线性时序基线LR特征与概率对数线性多维特征C、正则化低可解释基线KNN相似历史时段重复出现多维特征K、距离度量中非参数参照LSTM门控单元记忆长短期依赖定长特征序列hidden_size、dropout、seq_len高非线性上限这张表用来回答一个实际问题四个模型跑完如果LSTM比ARIMA提升不到3个百分点说明数据里的非线性结构本来就不强这时候继续调LSTM是浪费时间回去改特征或换标的效果更好。选型的本质是给每个模型安排正确的参照位置而不是指望某一个模型单点封神。4. 四个模型的最小可复现代码定阶、训练与统一评估数据切好模型边界清楚该上代码了。这个项目用到的Python包栈很固定pandas做数据清洗sklearn跑LR和KNNstatsmodels或pmdarima跑ARIMAPyTorch跑LSTM。这个章节给出每个模型的最小实现外加统一评估函数目标是让读者在本地把四个模型依次跑完拿到同一口径的指标。4.1 ARIMA定阶实操AIC、PACF与auto_arima两条路from pmdarima import auto_arima # 用训练集价格建模d1是因为价格一阶差分后通常已平稳 model_arima auto_arima( train_df[close], start_p0, max_p5, start_q0, max_q5, d1, # 固定一阶差分 seasonalFalse, # 日线无季节性设为False traceFalse, error_actionignore, suppress_warningsTrue ) print(model_arima.order) # 输出自动选出的(p,d,q)逻辑说明auto_arima对(p,q)组合用AIC打分AIC越低说明模型在拟合度和复杂度之间取得越好的平衡。固定d1是因为股价对数价格的一阶差分在绝大多数情况下已经平稳让auto_arima自己搜d也可以但搜索空间翻倍速度慢很多。max_p和max_q给到5以内就够日线级别超过5阶的自回归项基本是过拟合选出来的阶数超出了金融数据的信噪比能支撑的范围。跑完之后补一步残差检验把残差序列的ACF画出来如果仍有显著超过置信带的峰说明p或q没选够模型没把信息榨干。这一步是ARIMA和其余三个模型的本质区别——它有统计检验的完整闭环而sklearn和PyTorch模型没有。4.2 LR与KNN三行跑通sklearn回归器与分类器的参数选择LR和KNN放一起说因为它们的sklearn接口完全一致可以共用同一个训练循环。注意这里用的是分类版本预测涨跌方向。from sklearn.linear_model import LogisticRegression from sklearn.neighbors import KNeighborsClassifier from sklearn.metrics import accuracy_score lr LogisticRegression(C1.0, max_iter500) knn KNeighborsClassifier(n_neighbors5, metriceuclidean, weightsdistance) for name, model in [(LR, lr), (KNN, knn)]: model.fit(train_x, train_y) pred model.predict(test_x) acc accuracy_score(test_y, pred) print(f{name} direction accuracy: {acc:.4f})逻辑说明LogisticRegression默认带L2正则C是正则强度的倒数C越小正则越强。股价特征之间存在共线性比如ma_bias和volatility都由价格衍生L2正则能压制系数爆炸C1.0是稳妥起点。KNN的weightsdistance让近邻按距离加权投票比等权投票对噪声更鲁棒在金融数据上通常表现更好。参数说明K值从5起步然后在验证集上依次扫[3, 5, 7, 9]一次只改一个参数。不要同时调K和metric否则你分不清是哪个参数起了作用。如果上涨和下跌样本数量明显不均衡给LogisticRegression加class_weightbalanced防止模型把所有样本都预测成多数类准确率虚高但毫无意义。4.3 LSTM的PyTorch实现输入形状、归一化还原与训练细节LSTM代码量最大坑也最多。先看序列构造和模型定义。import torch import torch.nn as nn def make_sequences(features, labels, seq_len10): seqs, lbls [], [] for i in range(len(features) - seq_len): seqs.append(features[i:i seq_len]) lbls.append(labels[i seq_len]) # 预测第seq_len1天的方向 return (torch.tensor(np.array(seqs), dtypetorch.float32), torch.tensor(np.array(lbls), dtypetorch.float32)) seq_len 10 train_seq, train_lbl make_sequences(train_x, train_y, seq_len) test_seq, test_lbl make_sequences(test_x, test_y, seq_len) class StockLSTM(nn.Module): def __init__(self, input_size, hidden_size32, num_layers1): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.dropout nn.Dropout(0.2) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) out self.dropout(out[:, -1, :]) # 取最后时间步的隐状态 return self.fc(out)逻辑说明batch_firstTrue让输入形状是(batch, seq_len, features)对新手最直观。out[:, -1, :]取序列最后一个时间步的隐状态它包含了前面所有时间步压缩后的信息这是LSTM做序列到单点预测的标准做法。seq_len10大约两个交易周这个长度能覆盖短周期动量又不至于把噪声也装进窗口。训练循环里最容易翻车的是归一化还原。特征用了StandardScaler模型输出的是一个logit要先用sigmoid转成概率再和标签算损失。标签没有做标准化所以不需要还原但如果你改成回归任务预测价格标签必须有自己的scaler还原时用fit标签的那个scaler不能和特征共用一个。model StockLSTM(input_sizetrain_seq.shape[2], hidden_size32) loss_fn nn.BCEWithLogitsLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() out model(train_seq).squeeze(-1) loss loss_fn(out, train_lbl) optimizer.zero_grad() loss.backward() optimizer.step() model.eval() with torch.no_grad(): test_pred torch.sigmoid(model(test_seq).squeeze(-1)).numpy()逻辑说明BCEWithLogitsLoss把sigmoid合并进损失函数数值上比先sigmoid再算BCE更稳定这是PyTorch的常见做法。测试时手动sigmoid是为了拿概率做后续的阈值判断和融合。30轮训练对金融小样本足够再多就会过拟合。学习率1e-3是Adam的默认档位如果训练损失震荡降到3e-4。4.4 统一评估口径RMSE、方向命中率与回测曲线四个模型跑完后必须用同一套函数评估否则比较没有意义。def evaluate_strategy(y_true, pred_prob, daily_ret, threshold0.5): pred_dir (pred_prob threshold).astype(int) acc accuracy_score(y_true, pred_dir) # 预测涨就持有预测跌就空仓计算累计收益 strategy_ret daily_ret * (pred_dir * 2 - 1) cum_return np.cumprod(1 strategy_ret) return acc, cum_return[-1]逻辑说明pred_prob是模型输出的上涨概率阈值默认0.5。daily_ret必须和y_true按时间对齐这里最容易出索引错位错一位累计收益曲线就会失真。用np.cumprod(1 ret)算累计收益符合复利逻辑比直接累加更接近真实账户表现。四个模型拿同一份daily_ret和y_true去评估出来的acc和累计收益才能横向比较。补充一个视角只报总体准确率会掩盖模型在上涨日的表现。实盘里踏空预测跌但实际涨和亏损预测涨但实际跌的代价不对称建议额外打印「上涨日预测正确的比例」和「下跌日预测正确的比例」两个分项能看出模型是偏向追涨还是偏向防御。5. 预测股价项目避坑实录5个让人反复翻车的现场这一章是全文最值钱的部分。以下五个坑我在不同阶段都踩过有的花了整整两天排查。每条按现象、原因、解决写方便对照排查。5.1 未来函数泄漏回测曲线漂亮得不像话实盘一开就崩现象测试集方向命中率做到0.7以上回测累计收益曲线几乎45度角向上看起来发现了印钞机。换一段新数据立刻打回原形命中率掉回0.5附近。原因数据预处理或特征构造里用了未来信息。最常见的三处Scaler先fit全量数据再切分特征里用了shift(-1)之类的未来值标签构造时不小心把当天的收盘信息带进了特征。股价预测项目里未来函数泄漏是第一大翻车原因。解决按时间顺序重写预处理特征只用当天及之前的数据。做完后做一次信息审计随机打乱测试集标签再训练如果模型准确率依然很高说明特征里已经包含标签信息这就是泄漏。这个方法屡试不爽比人工检查代码可靠得多。5.2 归一化还原出错预测值整体偏移一个量级现象LSTM训练损失持续下降但把预测值inverse_transform回原尺度后预测价格比真实价格整体高出几百点方向对但数值完全不可用。原因用错了scaler。常见两种用训练scaler还原测试集两边均值方差不同模型输出的是标准化后的数值还原时减错了均值。还有一种隐蔽情况特征和标签各自归一化后一起训练还原时只对输出做了一遍还原忘了标签的scaler可能根本不存在。解决特征和标签各用独立的scaler并且都在训练集上fit。还原预测值时确保调用的是当初fit标签的那个scaler。加一行断言还原后的预测值应当在测试集真实价格的最小最大值范围内超出就查scaler来源。5.3 LSTM每次跑结果都不一样随机种子与数据装载顺序现象同一份数据、同一个模型跑三次得到三个不同的准确率有时候差0.01有时候差0.05不知道信哪个数。原因PyTorch的权重初始化、DataLoader的shuffle、dropout都带随机性。默认情况下每次运行重新初始化结果自然不同。解决训练脚本开头固定所有随机源。import random, numpy as np, torch seed 42 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)注意固定种子只能让人复现你的数字不代表这个数字稳定。更稳妥的做法是固定种子跑出主结果然后换3到5个种子各跑一遍取均值。种子敏感性本身就是稳健性指标——如果换种子后准确率波动超过0.03说明模型方差太大需要更强的正则或更小的容量。5.4 涨跌停日预测失真制度性截断让模型学到错误映射现象预测的涨跌方向在涨跌停日前后特别差回测收益曲线在某个时间段出现突兀拐点单日亏损超过其他日子总和。原因涨跌停是制度强制截断不是市场自然形成的价格。涨停日买盘巨大但卖盘不足价格被钉在涨停价模型训练时看到的是「这天价格只涨了10%」但它不知道这是被截断的结果于是学到错误的概率映射。解决在特征里加limit_flag标记列让模型知道这天是制度性截断。更激进的做法是把涨跌停日从测试集剔除但剔除会破坏时间连续性给LSTM制造人为间断我一般不用。做分类预测时还可以考虑把涨跌停单独设为一类让模型输出三分类而不是硬在二分类里纠结。5.5 KNN不标准化直接跑距离计算被大数值特征绑架现象KNN的预测结果几乎等于「成交量最大或最小的那几天涨跌」把volume_ratio这个特征删掉后准确率反而上升怎么调K都没用。原因KNN直接依赖特征距离某个特征数值范围一大欧氏距离就被它主导。volume_ratio的量级可以到几十log_ret只有0.01量级距离计算里成交量贡献了绝大部分权重其他特征形同虚设。解决KNN之前必须标准化和LR共用一个scaler。如果成交量存在极端值StandardScaler的均值会被拉到一边改用RobustScaler用中位数和四分位距缩放对极端行情更稳。这个经验同样适用于LSTM的输入层但影响没有KNN这么致命。6. 把预测结果变成能信的策略滚动回测与模型融合模型跑完评估做完最后一个问题这个模型到底能不能用单次切分的指标回答不了这个问题你需要滚动回测和融合技巧。6.1 用walk-forward代替单次切分训练窗口每步滚动单次切分测试的是一段历史窗口上的泛化能力walk-forward测试的是每一段历史之后的近期预测能力。实现上就是循环训练窗口向前推一步预测紧接着的下一天至少推进3到5次才算有说服力。LR和KNN跑这个循环很快LSTM每步从头训练会慢到怀疑人生折中做法是每5个交易日重训一次而不是每天重训。对比每次滚动的预测方向和实际方向统计命中率的稳定性比单次切分的一个数字可靠得多。6.2 方向命中率比RMSE更接近实盘收益RMSE衡量预测值和真实值的数值距离但实盘盈亏取决于方向预测涨买进去只要真涨就赚钱赚多赚少是另一码事。方向命中率配合换手率比RMSE更能反映策略可用性。如果滚动回测中方向命中率稳定在0.53以上且换手率可控这个模型就有作为策略信号的价值。低于0.53基本等于噪声再调参数也是自欺欺人。6.3 三种简单的模型融合方式加权平均、投票与排序平均融合不是为了堆模型是为了平滑单模型的方差。三种做法从简单到复杂一是对LR和KNN的预测概率做加权平均权重按各自在验证集上的准确率比例分配二是三者方向投票至少两个模型同方向才下单这个策略牺牲一部分交易机会换取胜率三是排序平均把四个模型的预测值各自排序后取平均再映射回概率这个方法对输出尺度差异最鲁棒因为LSTM的原始输出和LR的概率本就不可直接比较。我现在的习惯是任何预测股价的项目永远先用ARIMA跑出基线再上LSTM中间LR和KNN做对照。LSTM如果比ARIMA的提升不超过3个百分点我就停止调参回去改特征或换标的。这套习惯帮我避免了很多无意义的时间浪费。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →