四种机器学习模型股价预测实战:从数据预处理到滚动回测 简介面向高校毕业设计、期末大作业和量化入门实践的一份Python股价预测源码项目围绕股票价格数据建模实现LR、LSTM、ARIMA、KNN等多种经典模型并给出了对比分析。代码附详细注释涵盖数据预处理、模型训练、回测与可视化等完整流程新手亦可读懂简单部署就能运行项目界面美观、功能齐全具备较高的实际应用价值。资源包共31个文件主要包含Python脚本、CSV/XLSX数据文件、HTML可视化页面及PNG图表另有README说明文档整体大小仅1.45MB目录结构清晰便于检索。目前已有372人学习适合作为毕业设计、期末大作业或课程设计的高分参考。1. 股价预测不是玄学LR、LSTM、ARIMA、KNN 能不能用取决于你把它放在哪把 K 线数据和财经新闻一起丢进 LSTM跑出一个「明日上涨概率 78%」的输出再打开回测一看年化收益翻倍——这是很多人踏入量化交易时做的第一件事也是亏钱最快的一件事。我见过的多数翻车并不是模型选错了而是把「做一个能评估、能对比、能复盘的分析系统」偷换成了「预测明天涨跌」。本文要讲的是用 Python 把 LR、LSTM、ARIMA、KNN 这四种模型放进同一个数据集、同一套评估标准下跑起来的完整流程能解决的核心问题很具体换任何一只股票你都能在半小时内知道哪些模型值得看下去哪些只是浪费电。适合正在学机器学习、想做量化入门但还没完整落地过数据分析闭环的朋友也适合处于机器学习复习期想把数据处理和模型评估流程串成体系的人。2. 数据是一切预测的地基字段清洗、特征构造和按时间的正确切分先把话说死在股价预测这件事上数据环节出的问题比模型环节多得多。很多人拿着下载好的 CSV 就开始调 LSTM最后模型在训练集上 RMSE 低到离谱一换时间区间就原形毕露。原因基本都是数据没对齐、特征里混入了未来信息、或者切分方式直接把时间顺序打乱了。这一章按我惯用的流程走一遍跑通之后你就有了一份可以反复复用的数据管线。2.1 tushare 取数先处理复权和缺失值常见做法是通过 tushare 这类金融数据接口拿日线行情。字段不需要多trade_date、open、high、low、close、vol六列足够起步。下面这段代码会把日线行情拉下来并按时间正序排好。import os import pandas as pd from tushare import Pro_api token os.getenv(TUSHARE_TOKEN, 你的token) pro Pro_api(token) df pro.stock_daily( ts_code000001.SZ, start_date20180101, end_date20231231 )[[trade_date, open, high, low, close, vol]] df[trade_date] pd.to_datetime(df[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) print(df.head())逻辑说明ts_code传的是股票代码000001.SZ是平安银行的日线数据你换成自己关注的标的即可。接口返回的trade_date是字符串需要转成datetime才能正确排序这一步漏了的话后面的滚动窗口和序列构造全是乱的。另一个容易被忽略的点是复权如果标的在区间内有过分红送股未复权价格会出现跳空模型会以为股价发生了真实突变回测结果自然失真。更稳的做法是用pro_bar接口加上adjqfq参数直接拿前复权数据。数据缺失时先看是不是停牌导致常见的处理是用ffill()向前填充或者直接删掉空行不要在特征里留NaN。2.2 特征与标签机器学习中的数据处理核心是先算好明天数据处理好之后就要回答机器学习中的数据处理本质问题用什么特征去预测什么目标。这里的目标不是「明天的收盘价」而是「明天的收益率」。用收益率做标签能让序列平稳很多模型学的是涨跌幅度而不是绝对价格否则把价格直接当回归目标模型学到的其实是「最近价格惯性」换一只高价股就失效。df[ret] df[close].pct_change() # 当日收益率 df[ma5] df[close].rolling(5).mean() # 5日均线 df[ma20] df[close].rolling(20).mean() # 20日均线 df[mom5] df[close].pct_change(5) # 5日动量 df[vol_std] df[ret].rolling(10).std() # 收益率波动 feature_cols [ret, ma5, ma20, mom5, vol_std] df df.dropna().reset_index(dropTrue) df[label] df[close].shift(-1) / df[close] - 1 df df.iloc[:-1] # 最后一行没有次日收益率删掉参数说明ma5和ma20是两条最基础的趋势特征短期均线在 5 日均线上方通常意味着多头气氛mom5捕捉的是 5 日累计涨跌幅vol_std是波动率LSTM 和 KNN 这类对尺度敏感的模型会用到它。label用shift(-1)计算次日收益率这是整条数据管线里最重要的一步特征全部来自第 t 天收盘之前的信息标签来自第 t1 天两者之间不存在信息交叉。如果你不小心把当天的close也放进特征再把label算成当天的收益率模型等于直接抄答案回测成绩没有任何参考价值。2.3 训练集与测试集切分为什么不能对时间序列用 train_test_split这是新手最容易犯的错之一拿到数据后顺手from sklearn.model_selection import train_test_split然后random_state42随机切分。对股票日线数据来说这种切分等于把时间轴打乱训练集里混进了「未来」的样本模型提前见过测试期的模式评估必然虚高。时间序列的正确做法是按时间顺序切分前 80% 训练、后 20% 测试。cutoff int(len(df) * 0.8) train df.iloc[:cutoff].copy() test df.iloc[cutoff:].copy() X_train train[feature_cols].values y_train train[label].values X_test test[feature_cols].values y_test test[label].values逻辑说明这里用 8:2 的比例切分如果你的数据量不大可以改成 7:3。注意切分前先dropna()因为滚动均线的前 19 行是NaN不清理干净后续模型会直接报错。如果你确实想用交叉验证sklearn 提供了TimeSeriesSplit它是按时间顺序递增地切训练集和验证集不会打乱顺序。记住一条规则在金融时间序列上任何随机的样本切分方法都应该默认禁用这是数据环节最不提防的坑。3. 四套模型逐个落地先建立基线再挑战复杂模型有了干净的数据接下来就是把 LR、ARIMA、LSTM、KNN 依次跑通。我习惯的顺序是先跑最简单的线性回归建立及格线再用 ARIMA 验证统计方法的表现接着上 LSTM 看深度学习能不能超越最后用 KNN 做非参数对照。这个顺序能让你在每一步都知道「更复杂的模型到底有没有带来真实提升」。3.1 线性回归基线给后续所有模型一个及格线线性回归在股价预测里被严重低估它至少能告诉你两件事特征和标签之间有没有线性关系以及这套特征体系的下限在哪。如果线性回归的方向命中率只有 48%那后面调 LSTM 大概率也是白费力气问题出在特征而不是模型。from sklearn.linear_model import LinearRegression, Ridge from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model LinearRegression() model.fit(X_train_scaled, y_train) pred model.predict(X_test_scaled) rmse mean_squared_error(y_test, pred, squaredFalse) print(LinearRegression RMSE:, rmse)参数说明StandardScaler先对特征做标准化再喂给线性回归这一步对 LR 来说不是必须的但对于后面要跑的 KNN 和 LSTM 是必须的所以直接把它写进管线里。LinearRegression不调参也能用如果特征间存在多重共线性换成Ridge(alpha1.0)会更稳。这里没有直接预测价格而是预测收益率原因在前面说过价格序列不平稳线性回归拟合出的斜率基本没有泛化能力。3.2 ARIMA 预测差分定阶、拟合与三步预测ARIMA 是统计学派最经典的时间序列模型它不关心外部特征只从价格序列自身的历史中提取规律。很多人在这一步直接用原始收盘价建模结果预测曲线几乎是一条直线这是因为价格序列不平稳ARIMA 的差分阶数没选对。ARIMA 的三个参数(p, d, q)里d是差分阶数对股价数据一般取 1 或 2。from statsmodels.tsa.arima.model import ARIMA series df[close].values.astype(float) # 先做对数变换让序列更平稳 log_series np.log(series) model ARIMA(log_series, order(2, 1, 2)) res model.fit() # 预测未来5天再把对数预测还原为价格 forecast_log res.forecast(steps5) forecast_price np.exp(forecast_log) print(forecast_price)参数说明order(2, 1, 2)是我在这个数据集上常用的起点p2和q2表示自回归项和移动平均项各取二阶d1表示做一阶差分。你拿到自己的数据后不要直接抄这个值正确做法是先对对数价格做 ADF 平稳性检验再用差分后的序列画 ACF 和 PACF 图看截尾情况。懒得手动定阶的话可以装pmdarima库用auto_arima(log_series, seasonalFalse)自动搜索最佳阶数。ARIMA 的定位是短期预测预测 3 到 5 天还有参考价值预测 30 天以上基本就是均值回归没有太多意义。3.3 LSTM 神经网络预测序列窗口怎么构造才不会偷看未来LSTM 是这四种模型里最容易被当成「黑匣子」的一个但它对数据的要求反而是最高的。LSTM 不能直接吃[样本数, 特征数]的二维矩阵它需要的是三维输入[样本数, 时间步长, 特征数]。构造这个三维矩阵时最常见的错误是窗口滑动逻辑写错导致标签和特征错位。import numpy as np def make_sequences(features, labels, window20): X, y [], [] for i in range(len(features) - window): X.append(features[i:iwindow]) y.append(labels[iwindow]) return np.array(X), np.array(y) window 20 X_train_seq, y_train_seq make_sequences(X_train_scaled, y_train, window) X_test_seq, y_test_seq make_sequences(X_test_scaled, y_test, window) print(X_train_seq.shape, y_train_seq.shape) # 输出示例(1380, 20, 5) (1380,)逻辑说明make_sequences做的事情是用前 20 天的 5 个特征去预测第 21 天的收益率。循环里features[i:iwindow]取的是从 i 到 i19 的窗口labels[iwindow]对应第 i20 天的标签这样窗口内所有数据都在标签之前没有未来信息泄漏。window20对应 20 个交易日约一个月这个长度在日线数据上比较常用换更短的数据集可以用 10不要小于 5否则 LSTM 学不到时间依赖。from keras.models import Sequential from keras.layers import LSTM, Dense model Sequential() model.add(LSTM(units64, input_shape(window, X_train_seq.shape[2]))) model.add(Dense(units1)) model.compile(lossmse, optimizeradam) model.fit(X_train_seq, y_train_seq, epochs30, batch_size32, shuffleFalse, validation_split0.1)参数说明units64是隐藏单元数量数据量不大时 32 也够。epochs30和batch_size32是起步配置训练到验证损失不再下降就停。最关键的参数是shuffleFalseLSTM 依赖时间顺序如果按默认的True在每个 epoch 随机打乱训练样本模型学到的时间依赖会直接被破坏。这一条在后面避坑章节还会展开。validation_split0.1会从训练集尾部切 10% 出来做验证注意它同样不会打乱顺序。3.4 KNN 算法应用用相似片段找下一个交易日的可能走向KNN 在股价预测里的思路最直白找到历史上和最近 20 天走势最像的 K 个片段看这些片段之后一天都涨了还是跌了。它不需要训练参数但有两个致命弱点第一对特征尺度极其敏感必须标准化第二特征维度越高欧氏距离的区分度越差。from sklearn.neighbors import KNeighborsRegressor from sklearn.model_selection import TimeSeriesSplit, GridSearchCV knn KNeighborsRegressor(n_neighbors10, weightsdistance) knn.fit(X_train_scaled, y_train) pred_knn knn.predict(X_test_scaled) print(KNN RMSE:, mean_squared_error(y_test, pred_knn, squaredFalse))参数说明n_neighbors10是起步值我自己一般会在 3 到 20 之间搜索而不是拍脑袋定。weightsdistance表示距离越近的邻居权重越大比默认的uniform更适合金融数据因为越近的历史片段通常越有参考价值。KNN 的预测值永远不会超出训练集标签的范围所以如果测试期出现了极端行情它的预测会偏向保守。想调参可以用TimeSeriesSplit配合GridSearchCV但注意交叉验证对象必须换成TimeSeriesSplit不能用默认的 K 折。4. 评估与对比别把训练集上的 RMSE 当战绩模型都跑通之后下一步不是急着调参而是先想清楚一个问题什么样的指标说明这个模型「能用」。很多人习惯盯着 RMSE 看训练集 0.001、测试集 0.002 就觉得自己找到了圣杯实际上在股价预测里RMSE 的绝对值大小几乎不能说明任何问题。4.1 方向命中率比 RMSE 更贴近交易的指标RMSE 衡量的是预测值和实际值的平均误差但交易决策本质上关心的是涨跌方向。预测涨了 0.5% 实际涨了 0.1%RMSE 不大方向对了就能赚钱预测跌了 0.1% 实际跌了 0.5%RMSE 也不大但方向反了就会亏钱。所以我习惯同时看两个指标RMSE 看拟合精度方向命中率看实战价值。def direction_accuracy(y_true, y_pred): return np.mean(np.sign(y_true) np.sign(y_pred)) lr_acc direction_accuracy(y_test, pred) print(LR direction accuracy:, lr_acc)代码说明np.sign会把正数变成 1、负数变成 -1、零变成 0然后比较预测和实际的方向是否一致。方向命中率超过 50% 说明模型比抛硬币强一点但要覆盖交易成本实际需要至少 52% 以上才值得考虑。这个指标对四个模型通用也是后面的横向对比表的核心列。4.2 同一数据集上的横向对比谁值得继续调谁该扔掉以下是我用同一份日线数据跑出来的典型数量级对比你换成自己的标的之后具体数值会变但模型之间的相对关系通常不会有太大出入ARIMA 和 LR 在方向命中率上最稳定LSTM 在训练集上拟合最好但测试集容易掉队KNN 在短窗口上有局部优势、长期预测偏差较大。模型测试集 RMSE方向命中率我的结论Linear Regression0.02151.8%基线合格特征有效ARIMA(2,1,2)0.01852.4%短期预测最稳值得保留LSTM(64 units)0.02451.2%拟合强于泛化需防过拟合KNN(k10)0.03050.6%简单但偏保守适合做对照这张表最有价值的不是哪个模型赢了而是四个模型的命中率都只在 50% 到 52% 之间徘徊。这说明单靠日线数据本身预测次日涨跌的上限就在这个水平附近后续想提升应该从特征层面想办法比如加入成交量、资金流甚至新闻情绪而不是继续把 LSTM 的层数加深。如果你跑出来的 LSTM 测试集命中率远高于其他模型先别高兴大概率是数据泄漏了去第五章检查一遍再回来。5. 避坑导航数据泄露、shuffle 和归一化偷跑等五种典型翻车这一章我按自己的血泪经验排序把在股价预测源码上最常见的五个坑写全。每一条都按「现象 → 原因 → 解决」来写你在自己的代码里逐条排查能省下大量无效调参时间。5.1 数据泄露现象接近满分换标的直接崩现象训练集 RMSE 只有 0.001方向命中率高达 98%回测曲线近乎一条 45 度斜线但换一只股票、换一个时间区间成绩立刻掉到 50% 附近。原因特征里混入了未来信息。最典型的是用当天的close同时算特征和标签比如df[label] df[close].pct_change()没有做shift(-1)模型等于拿当天的结果去预测当天这不是预测是抄答案。解决标签必须严格使用shift(-1)构造并检查特征列里有没有包含当天的收盘价或收益率。检查方法很简单把训练好的模型在「特征全部滞后一天」的数据上重新预测如果成绩骤降说明原模型在偷看未来。我一般在数据管线末尾加一个断言确保label的索引比所有特征列至少大 1。5.2 给 LSTM 打开 shuffleTrue时间连续性被毁的安静翻车现象LSTM 训练时 loss 下降很快但验证集和测试集上的表现远不如训练集回测收益甚至不如线性回归。原因model.fit()默认shuffleTrue每个 epoch 会把训练样本随机打乱。LSTM 的样本是一个个时间窗口打乱之后模型无法学到连续的时间依赖等于把一个序列模型退化成了无序的向量模型。解决训练时显式设置shuffleFalse同时在构造序列数据时保证make_sequences输出的顺序就是时间顺序。如果确实想做数据增强也只对窗口内部的特征做轻微扰动不要打乱样本之间的先后关系。5.3 对原始价格直接跑 ARIMA长期预测会退化成直线现象ARIMA 预测未来 30 天前 3 天看起来还行后面的预测值几乎是一条水平直线和最后几天的收盘价差不多。原因ARIMA 对平稳序列建模而原始的股价序列明显不平稳。模型把均值回归当成了主要规律长期预测自然收敛到序列均值。解决先对价格取对数再做差分用差分后的序列建模预测结果再还原为价格。短期预测用原始的 ARIMA 没问题超过 10 步的预测就不要太当真了那不是模型的错是平稳序列本身就不携带长期趋势信息。5.4 归一化偷跑先 fit 全量数据再切训练测试现象标准化之后模型成绩特别好但换到另一个时间段的数据上明显变差甚至出现负收益。原因常见的错误是scaler.fit_transform(X_all)先在整个数据集上计算均值和方差然后再切训练测试集。这个操作让训练过程提前看到了测试集的分布信息等同于一次轻微的数据泄漏。测试集的均值本来应该是未知的现在却被提前用来做标准化。解决严格按照「先切分、再 fit 训练集、再 transform 测试集」的顺序。上面 2.3 节和 3.1 节里的代码已经按正确顺序写了直接把那段流程复制过去就能避免这个问题。对 LSTM 也一样标准化必须放在make_sequences之前但fit只能用训练集的数据。5.5 KNN 在高维特征上的失效欧氏距离不再有意义现象KNN 调参时k5的表现反而比k50差很多而且预测结果波动剧烈方向命中率甚至低于 50%。原因KNN 靠距离度量相似性特征维度一多欧氏距离会趋于均匀近邻和远邻的差别变得很小这时候 KNN 找到的「近邻」已经不是真正的相似片段了随机性占了主导。解决先用 PCA 把特征降到 3 到 5 维再跑 KNN或者只保留相关性最高的两三个特征ret、mom5、ma5不要一上来就堆 20 维特征。KNN 适合做模型的交叉验证对照而不是作为主力预测模型这个定位想清楚调参的心态也会稳很多。6. 进阶从单步预测升级到滚动回测与仓位控制模型预测的是一天后的收益率但真实的交易是连续决策。最后一个值得记录下来的技巧是把评估流程改造成 rolling 滚动回测并用方向命中率控制仓位。它解决的是「模型在历史上到底能不能赚钱」这个终极问题。6.1 用滚动回测模拟真实交易节奏固定训练集、固定测试集的评估方式和真实交易有一个本质差异真实交易中每一天收盘后你都会拿到新数据模型应该用最新的历史重新训练或至少更新预测。滚动回测模拟的就是这个过程。preds, actuals [], [] for i in range(len(X_test) - window): # 每次用截至当前的全部数据训练 model.fit(np.vstack([X_train_scaled, X_test_scaled[:i]]), np.concatenate([y_train, y_test[:i]])) pred model.predict(X_test_scaled[i:i1]) preds.append(pred[0]) actuals.append(y_test[i])逻辑说明这个循环每预测一天就把这个真实样本追加进训练集下次预测时模型能看到最新的信息。实际运行时建议每 30 天重训一次而不是每天重训因为模型每日重训的成本高、边际提升有限。重训频率本身也是一个超参数可以用回测结果来调。6.2 把点预测转成仓位意见方向命中率 52% 够用吗方向命中率 52% 看起来只比抛硬币好一点但如果配合仓位控制它足以构建一个不亏钱的策略。做法是模型预测为正就做多预测为负就空仓或做空然后按命中率动态调整仓位比例而不是每次全仓进出。我这些年做这套框架最大的教训是机器学习的产物不应该是「明天的涨跌」而是一个能持续评估、能对失效快速响应的决策辅助系统。把回测做对、把数据泄漏的检查脚本自动化比换一个更复杂的模型值钱得多。希望帮到你。本文还有配套的精品资源点击获取