简介:Python实现机器学习股价预测源码,以LR、LSTM、ARIMA、KNN等多种模型为核心,围绕股票价格预测场景,提供了从数据读取、特征处理、模型训练到结果评估的完整实验链路,适用于毕业设计、期末大作业、课程设计等需要快速做出可运行项目的学习者。资源压缩包内共31个文件,包括Python源码(模型构建、回测、预测、可视化)、CSV股票与账户数据、Excel月度队列、HTML可视化图表、依赖清单及说明文档等,总大小仅1.45MB,轻量而完整,便于本地部署与二次开发。目前已有372人学习下载,项目均经过调试,可直接运行,能有效降低上手门槛。源码注释详细,新手也能看懂;项目功能完善,提供趋势图、自相关图、ARIMA/LSTM预测图、每日资金情况图等多种可视化结果,便于理解不同模型在股价预测中的表现差异。稍加调整即可适配其他股票数据,具备较强的实用性与展示价值,可直接用于毕业设计或期末大作业答辩。
1. 为什么股价预测要用多模型,而不是碰运气调参
当你在检索框里输入“Python实现机器学习股价预测源码”的时候,大概率已经经历过一次挫折:从某个网页下载了一份代码,跑起来准确率不错,换一只股票、换一段行情,结果完全对不上。这不是代码写得烂,而是股价预测这件事天然不适合用单一模型硬扛。线性回归能抓住缓慢趋势,却抓不住突发反转;KNN能匹配相似形态,却总是慢半拍;ARIMA对长期走势有解释力,但面对波动率聚集就失去脾气;LSTM在足够数据下能学非线性关系,可训练成本最高,翻车也最隐蔽。于是我把“到底用哪个模型”换成了“让每个模型各自打分”,于是有了这篇可以一步步落地复现的实战笔记。
这套方案的定位很简单:它不承诺“预测明天涨跌”,而是给你一条相对可靠的实验路径——数据怎么预处理、特征怎么构造、LR/ARIMA/KNN/LSTM各自的实现要点、怎么公平对比、哪些坑必须避开。适合的人群包括刚刷完机器学习入门课程、想拿金融数据做毕业设计或期末作业的同学,也包括已经在做股票量化分析、想用传统机器学习模型加深度学习模型做基准对比的从业者。无论你从哪一个入口进来,这套流程都希望帮你少走弯路。
2. 数据准备是预测的命根子:从数据源到防泄露的完整流水线
很多初学量化的人会犯一个顺序错误:先选模型,再找数据,最后才发现数据根本对不上。我一般会把数据准备放到整个流程的起点,因为后面所有模型都吃同一份数据,数据如果有坑,LR翻车、LSTM也翻车,没有任何模型能救得回来。
2.1 数据源怎么选:本地CSV日线、时间对齐与清洗
股价预测最常见的数据粒度是日线,包含日期、开盘、最高、最低、收盘、成交量六列就够用。我建议你把数据落成本地CSV再开始实验,而不是每次都在代码里实时下载。原因有两个:其一,实时接口可能因为网络或限流中断,导致实验不可复现;其二,本地保存的CSV可以固定一个区间,方便你反复对比不同模型的表现。
import pandas as pd import numpy as np df = pd.read_csv("price_data/TSLA.csv", parse_dates=["Date"], index_col="Date") df = df.sort_index() df = df[["Open", "High", "Low", "Close", "Volume"]].dropna() # 检查重复索引 dup_idx = df.index[df.index.duplicated()] if len(dup_idx) > 0: df = df[~df.index.duplicated(keep="last")] # 检查停牌缺口:连续交易日应该没有超过5天的间隔 gap_days = df.index.to_series().diff().dt.days print("最大间隔天数:", gap_days.max())这段代码做的事情很直白:读CSV、按时间排序、去掉空值、处理重复日期。最后一步打印时间间隔最大值,用来发现停牌或数据缺失。如果你的数据源只有交易日,那么间隔最大值通常是3天(比如周五到下周一);如果出现超过5天的缺口,就要回源头检查是否漏了交易日数据。这里我一般不会直接fillna,因为股价序列的空缺和普通表格不一样,补进去一个假价格会影响后续所有特征计算。
数据源的选择上,免费渠道比如akshare、baostock或者财经网站导出的交易数据都可以,关键不在于数据源多权威,而在于你记录了数据区间和数据来源,保证别人拿到同一份CSV能复现你的结果。环境配置我就不展开了,python安装教程和vscode python环境配置网上已经非常成熟,装个Anaconda基本不折腾。
2.2 特征构造与标签定义:让LR和KNN能吃同一份数据
股价预测的输入特征不需要多玄乎,常用的是几类:收益率、波动率、成交量变化、高低振幅、均线乖离。这些特征有一个共同特点——只用历史信息就能计算,不会用到未来数据。这是构造特征的第一原则。
# 标签:未来T日收益方向,1为上涨,0为下跌 T = 1 df["future_ret"] = df["Close"].shift(-T) / df["Close"] - 1 df["label"] = (df["future_ret"] > 0).astype(int) # 特征 df["ret_1d"] = df["Close"].pct_change(1) df["ret_5d"] = df["Close"].pct_change(5) df["vol_20d"] = df["Close"].rolling(20).std() df["volume_ratio"] = df["Volume"] / df["Volume"].rolling(20).mean() df["high_low_ratio"] = df["High"] / df["Low"] - 1 df["ma_gap"] = df["Close"] / df["Close"].rolling(10).mean() - 1 feature_cols = ["ret_1d", "ret_5d", "vol_20d", "volume_ratio", "high_low_ratio", "ma_gap"] df = df.dropna(subset=feature_cols + ["label"])这里的逻辑要解释清楚:future_ret用shift(-T)拿到未来第T天的收盘价,算出的收益才是标签。用T=1就是预测“明天是否涨”,如果你想预测未来5天方向,把T改成5即可。特征侧全部使用历史窗口计算,rolling函数不会往过去泄漏未来。末尾的dropna很重要,因为pct_change和rolling会让前20行产生NaN,标签也会让最后T行变成NaN,这些样本必须丢掉,否则模型会学到一堆空值。
不同模型对特征的预处理要求差别很大。LR要求特征在同一量纲下,否则大数值特征主导权重;KNN对量纲更敏感,因为距离计算完全依赖尺度;ARIMA根本不看这些特征,它只吃一条价格或收益率序列;LSTM虽然对量纲有一定鲁棒性,但训练时如果不做归一化,loss可能震荡得厉害。所以通用做法是通过“训练集拟合scaler、测试集应用同一个scaler”来标准化,下面这段代码示范了正确姿势。
from sklearn.preprocessing import StandardScaler split_idx = int(len(df) * 0.7) train_df = df.iloc[:split_idx] test_df = df.iloc[split_idx:] scaler = StandardScaler() train_scaled = scaler.fit_transform(train_df[feature_cols]) test_scaled = scaler.transform(test_df[feature_cols]) X_train, y_train = train_scaled, train_df["label"].values X_test, y_test = test_scaled, test_df["label"].values注意fit_transform和transform的配合:scaler只在训练集上拟合出均值和方差,测试集直接转换。如果你对全量数据先标准化再切分,测试集的信息已经混入训练过程,回测指标会虚高,这是最常见的“未来函数”之一。
2.3 切分与滑动窗口:验证集怎么切才不会偷看未来
时间序列切分和普通机器学习不一样,不能使用train_test_split里的默认随机抽样。随机打乱会破坏时间顺序,模型等于通过未来片段去预测过去。正确做法是按时间顺序切成三段:训练集、验证集、测试集。
# 80%训练,10%验证,10%测试 train_ratio, val_ratio = 0.8, 0.1 train_idx = int(len(df) * train_ratio) val_idx = int(len(df) * (train_ratio + val_ratio)) train_part = df.iloc[:train_idx] val_part = df.iloc[train_idx:val_idx] test_part = df.iloc[val_idx:]这个切分看起来简单,但有两个细节容易被忽略。第一,特征计算要在切分之前完成,这样每条样本的特征窗口是历史的,不依赖切分边界;第二,验证集和测试集的scaler都必须由训练集拟合。如果验证集和测试集用过自己拟合的scaler,虽然误差可能不大,但它违背了“测试环境未知”的原则,严谨性会被扣分。
至于滑动窗口,它有两种作用:一种是给LSTM生成样本序列,一种是在滚动回测中模拟逐步预测。后者我放到最后一章展开,这里只说LSTM的序列生成。常见做法是用过去window天数据预测未来1个点,窗口默认值我一般取30,因为对应一个月的交易日,有足够形态信息。代码在第四章给出,这里先记住结论:窗口太短学不到中期趋势,窗口太长会把远古信息当噪音,30到60是大多数LSTM时间序列预测python实践的起步区间。
3. LR、KNN和ARIMA:先用三个经典模型把基线建立起来
很多人一上来就上LSTM,最后陷入调参泥潭。我的顺序建议是先跑LR、KNN、ARIMA这些基础模型,得出一个可复现的基线,再让深度学习模型去挑战这个基线。如果LSTM连LR都打不过,调参就没有意义,模型选择本身可能就错了。
3.1 逻辑回归LR:标准化、类别权重与基线意义
逻辑回归虽然名字带“回归”,但做的是分类。我们把“明天涨跌”当成一个二分类问题,y等于1表示上涨,0表示下跌。LR训练快、可解释性强,最适合当第一个基线。
from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, confusion_matrix lr_model = LogisticRegression( C=0.1, max_iter=1000, class_weight="balanced", random_state=42 ) lr_model.fit(X_train, y_train) y_pred_lr = lr_model.predict(X_test) print("LR准确率:", accuracy_score(y_test, y_pred_lr)) print("混淆矩阵:\n", confusion_matrix(y_test, y_pred_lr))参数说明:C是正则化强度的倒数,C越小正则化越强。股价特征间相关性高,过拟合风险不小,我一般先把C设为0.1再网格搜索。class_weight="balanced"是因为上涨和下跌样本比例可能接近五五开,但也可能某一阶段上涨天明显多于下跌天,动平衡可以避免模型偷懒全部预测多数类。max_iter设1000是给收敛充裕空间,默认100偶尔会警告未收敛,尤其在特征没标准化的时候。
LR的意义不只在于它的准确率,更在于逻辑回归系数能告诉我们每个特征的贡献方向。比如ret_5d系数为正,说明过去5天涨幅越大,明天上涨概率越高——这不一定符合实际市场规律,但至少模型没有学到反直觉的模式。
3.2 KNN做形态匹配:近邻参数与时序约束
KNN算法常被应用于股票量化分析,因为它没有显式训练过程,思想是“找到历史上最像当前时点的T天,看看次日涨跌”。距离度量默认用欧式距离,这与2.2里做的标准化直接相关——如果不标准化,价格从几百到几千,成交量从几万到几亿,欧氏距离会被大数值特征完全支配。
from sklearn.neighbors import KNeighborsClassifier knn_model = KNeighborsClassifier( n_neighbors=5, weights="distance", p=2, metric="minkowski" ) knn_model.fit(X_train, y_train) y_pred_knn = knn_model.predict(X_test) print("KNN准确率:", accuracy_score(y_test, y_pred_knn))这里n_neighbors=5表示取最近5个历史样本投票;weights="distance"让距离更近的邻居权重更大,而不是简单多数投票;p=2就是欧氏距离,p=1是曼哈顿距离,两者可以都试。
KNN最大的坑在于时间顺序。sklearn的KNN在fit之后,查询X_test时会在整个训练集里找邻居,这本身没有泄露,因为训练集时间都在测试集之前。但如果你做滚动预测,每次预测时训练集必须只包含“当前时刻之前”的样本,否则测试期之后的数据会被当成邻居,这种未来数据泄漏会让回测准确率虚高,实盘一塌糊涂。后面第五章我会给出一个具体的错误范例。
3.3 ARIMA做单变量序列:ADF检验、AIC定阶与滞后问题
ARIMA模型和机器学习分类器不是一个路子,它只吃一条时间序列,比如每天的收盘价,建模目标是序列自身的时间依赖。对价格序列直接建模通常不稳,最常见的诊断工具是ADF检验。
from statsmodels.tsa.stattools import adfuller close_series = df["Close"] adf_stat, p_value = adfuller(close_series)[:2] print("ADF p值:", p_value) if p_value > 0.05: diff_series = close_series.diff().dropna() print("价格序列非平稳,做一阶差分") else: diff_series = close_seriesp值大于0.05说明序列非平稳,必须先差分。ARIMA(p, d, q)中的d就是差分阶数,常见取1就够。定阶我很少纯靠眼睛看ACF/PACF图,而是跑一个小网格用AIC挑选,既省事又相对客观。
from statsmodels.tsa.arima.model import ARIMA train_close = close_series.iloc[:split_idx] test_close = close_series.iloc[split_idx:] best_aic = float("inf") best_order = None for p in range(0, 4): for d in range(0, 2): for q in range(0, 4): try: model = ARIMA(train_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) arima_model = ARIMA(train_close, order=best_order).fit() forecast = arima_model.forecast(steps=len(test_close))这个网格很小,p和q各取0到3,d取0或1,总共32个组合,训练速度快,适合日线数据。AIC越低模型越优,但它只代表样本内拟合与复杂度的折中,不能保证样本外预测好。ARIMA预测有一条近似平移的曲线,学名是“预测均值回归”,直观表现就是它基本把历史走势往右平移,等到序列真正发生趋势变化时反应很慢。这是ARIMA自身的局限,不算bug,决策时不要指望它抓住拐点。
4. LSTM的实现要点:从DataFrame到PyTorch训练闭环
市面上关于LSTM做预测的源码并不少,但多数只是“能跑”,对数据格式、窗口长度、训练参数这些决定成败的细节一带而过。这一章我把从DataFrame到PyTorch训练的完整链路拆开,讲清楚每一步在做什么,以及参数为什么这么设。
4.1 序列张量化与窗口长度:LSTM时间序列预测python第一步
LSTM的输入是三维张量,形状是(batch, seq_len, features)。seq_len就是前面说的window,features是特征数量。和LR不同,LSTM不拿一堆独立样本喂模型,而是每条样本本身就是一条连续的历史片段。
import numpy as np import torch window = 30 feature_data = df[feature_cols].values def create_sequences(data, window): X, y = [], [] for i in range(len(data) - window): X.append(data[i:i+window]) y.append(data[i+window]) # 这里用未来一行的特征做回归目标 return np.array(X), np.array(y) X_seq, y_seq = create_sequences(feature_data, window) print("输入张量形状:", X_seq.shape, "标签形状:", y_seq.shape)这段代码里有一个容易被忽视的问题:y取的是data[i+window],而不是“未来收益”。如果目标是回归,一般会构造未来收益率作为标签;如果目标是分类,则把未来涨跌方向作为标签。上面的示例是为了方便解释张量维度,实际使用时要替换成df对应行的label。窗口长度选30的理由前面提过——月度交易周期恰好能覆盖一轮短期形态,超过60天历史形态的参考价值会衰减。
张量划分与前面保持一致:先切分时间,再生成序列,切分边界要落在样本索引上,而不是落在窗口中间。训练数据有必要转成torch.float32张量,LSTM内部计算不接受float64,这是新手最容易碰到的报错。
4.2 模型结构设计:hidden、layers、dropout怎么配
模型结构不需要复杂,一个LSTM层加一个全连接输出层就能解决大多数单步预测问题。hidden_dim控制记忆容量,num_layers表示堆叠层数,dropout用于防止过拟合,层数多了反而难收敛。
import torch.nn as nn class PriceLSTM(nn.Module): def __init__(self, feature_dim, hidden_dim=64, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=feature_dim, hidden_size=hidden_dim, num_layers=num_layers, batch_first=True, dropout=dropout ) self.fc = nn.Linear(hidden_dim, 1) def forward(self, x): out, _ = self.lstm(x) last_step = out[:, -1, :] return self.fc(last_step) feature_dim = len(feature_cols) model = PriceLSTM(feature_dim=feature_dim, hidden_dim=64, num_layers=2, dropout=0.2)参数说明:batch_first=True表示输入形状是(batch, seq_len, features),这更符合我们直觉,如果不设则默认是(seq_len, batch, features)容易搞混。hidden_dim=64是常用的起始大小,数据量大可以升到128;num_layers=2通常够用,超过3层在小数据集上基本只会过拟合。dropout只在多层lstm的层间生效,单层LSTM设dropout不会有任何作用,这是PyTorch实现细节。
网上能找到的Pytorch LSTM源码非常多,但很多时候换一个数据集就失灵,原因是骨架只是骨架,输入输出维度、标签对齐、归一化策略才是真正的灵魂。这一段代码不追求花哨,就是要把结构稳下来。
4.3 训练闭环:学习率、梯度裁剪与早停
训练LSTM最典型的失败是loss不降反涨,或者训练loss低、验证loss剧烈震荡。除了数据问题,最重要的两个控制手段是梯度裁剪和早停。
criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) X_train_tensor = torch.tensor(X_train_seq, dtype=torch.float32) y_train_tensor = torch.tensor(y_train_seq, dtype=torch.float32).view(-1, 1) X_val_tensor = torch.tensor(X_val_seq, dtype=torch.float32) y_val_tensor = torch.tensor(y_val_seq, dtype=torch.float32).view(-1, 1) epochs = 100 best_val_loss = float("inf") best_model_path = "best_lstm.pth" for epoch in range(epochs): model.train() optimizer.zero_grad() pred = model(X_train_tensor) loss = criterion(pred, y_train_tensor) loss.backward() nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) optimizer.step() if (epoch + 1) % 10 == 0: model.eval() with torch.no_grad(): val_pred = model(X_val_tensor) val_loss = criterion(val_pred, y_val_tensor).item() print(f"Epoch {epoch+1:3d} | train loss: {loss.item():.6f} | val loss: {val_loss:.6f}") if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), best_model_path)这里的关键是clip_grad_norm_。LSTM对梯度爆炸尤其敏感,RNN系模型在长序列上梯度传播容易失控,把梯度范数裁剪到1.0能有效防止训练发散。学习率从1e-3起步,如果训练loss一直在0.1量级反复横跳不下降,可以把lr降到3e-4再跑一遍。早停逻辑很简陋但有效:每10个epoch对比一次验证loss,只保留最好的参数。很多进阶方案会加ReduceLROnPlateau这种学习率调度器,效果更好,但核心仍然是保留最优验证loss。
5. 模型对比与避坑:为什么回测漂亮,一上模拟盘就翻车
特征也构造了,模型也训练了,准确率看着还行,一放到实盘模拟却完全不是那么回事。这一章把最容易踩的坑逐一挑明。不夸张地说,这里每一条我都亲眼见过有人踩进去。
5.1 公平对比的前提:同数据、同窗口、同指标
做过课程设计或者期末作业的人应该理解这种感觉:明明每个模型单独跑的时候都说得过去,写进报告里一对比,却发现LR和KNN的测试集不一样,ARIMA用的区间又比LSTM多了一段。模型对比只要有一个条件不同,结论就不成立。
我的一般做法是先统一训练、验证、测试区间,再分别计算准确率、F1、年化收益率、最大回撤这类指标。准确率只适合初步观察,在涨跌分布不均衡时容易产生误导。年化收益率容易极端,需要配合最大回撤一起看。无论用哪个指标,四条铁律必须遵守:一是同一份CSV,二是同一个切分点,三是同一个预测目标,四是同一个时间粒度。
5.2 常见问题一:九成准确率其实是类别失衡加数据泄露
现象:模型在测试集上准确率超过0.9,你觉得“稳了”,放到模拟盘却连续亏损。
原因通常有两个叠加:第一,未来函数泄漏,常见的是标准化时用了全样本统计量,或者特征构造时用了未来价格;第二,类别极度不平衡,某个时段上涨或下跌样本占比超过90%,模型只要一直预测多数类就能得到漂亮准确率。
解决:检查两个地方。一是scaler是否只在训练集上fit;二是打印测试集标签的分布,如果某类占比超过80%,准确率就没有参考价值,此时要看F1或AUC。如果你想要一个更可信的基准,可以做一个“预测昨天涨跌方向”的哑模型,它的准确率大概等于上涨样本占比,任何模型都必须在这个基准之上才算有效。
5.3 常见问题二:KNN把未来样本当邻居
现象:KNN在回测里准确率特别高,但在实盘模拟中表现平平,甚至差于LR。
原因:很多人用sklearn的KNN时,写的是整个数据集fit再predict。如果训练集时间窗口覆盖到测试集之后的数据,KNN查询时就会把“未来”样本纳入邻居集合,这意味着它偷看了未来。KNN因为没有显式训练参数,这类错误特别隐蔽。
解决:滚动预测,每次只取当前时刻之前的数据重新fit。误区是追求效率用全量数据一次fit,但KNN本身没有训练过程,每次预测前临时构建训练集也不算慢,代价完全可以接受。
# 错误写法:KNN能偷看到未来样本 knn.fit(X_all, y_all) knn.predict(X_test) # 正确写法:只在当前时刻之前的历史样本中找邻居 knn.fit(X_train, y_train) knn.predict(X_test)这两行对照是最典型的时间序列KNN坑,比任何参数调优都重要。
5.4 常见问题三:ARIMA预测出一条“平移曲线”
现象:ARIMA的预测曲线在图上和真实曲线高度相似,但整体向右平移了一段,看起来准确率不错,实际上没有任何可用信号。
原因:ARIMA的预测本质是条件均值,当序列非平稳或模型以低阶AR为主时,预测值会趋向于最后几个观测值的加权重平均。股价序列的随机性很强,条件均值预测天然会倒退到“最近的值就是最佳预测”的逻辑,于是曲线就产生了滞后。
解决:ARIMA适合做数据的基线理解,不适合直接作为股价预测信号。用它验证残差是否为白噪声,比用它预测涨跌更有价值。如果你看到残差序列还有明显自相关,说明模型结构没定对,需要回到d阶数和p、q选择上重新检查。
5.5 常见问题四:LSTM训练loss在测试集上发散
现象:训练loss一路下降,验证loss却从第30个epoch开始剧烈上升,最后彻底发散成NaN。
原因:学习率过高、没有梯度裁剪、没有早停,三者至少中了一个。时间序列预测的LSTM对学习率非常敏感,1e-2都可能直接发散,1e-3是相对安全起点;梯度爆炸在序列长度长、数值范围大时更容易发生;早停则是最后的防火墙。
解决:把学习率降到3e-4或者1e-4重新跑;在backward之后加clip_grad_norm_;只保留验证loss最小的模型参数,不要用最后一个epoch的权重。这里的实现细节已经在第四章给出,关键在于真正用起来,而不是读一遍就过。
6. 把模型组合起来:从多模型投票到实盘纪律
单个模型各有脾气,那就让他们投票。常用的办法是少数服从多数:LR、KNN、LSTM、ARIMA各给一个明日的涨跌观点,当两个或三个以上模型一致时才产生信号。这个做法的逻辑不是每时每刻都要交易,而是过滤掉模型分歧大的日期,只做大家都同意的方向。
import numpy as np # 假设四个模型都输出0或1,1表示看涨 votes = np.array([lr_signal, knn_signal, arima_signal, lstm_signal]) final_signal = 1 if votes.sum() >= 3 else 0这个代码片段简单到不需要解释,真正要理解的是背后的纪律:把“每天都要预测”变成“只在模型共识时做决策”。四个模型经常有分歧,所以信号数量会少,但胜率通常更稳定。
除了投票,我还保持一个习惯:把每次预测的概率和对应的特征日期存到本地,每周复盘一次预测质量,计算IC,也就是预测方向与真实方向的相关性。如果连续几周IC都在零附近徘徊,说明特征失效,需要重新切数据或者更新训练集。这种做法比盯着一两个回测指标更真实。LSTM这类深度学习模型需要定期重训,市场环境变了,训练数据也要滚动更新。我总会先用LR跑通完整流程,再逐步加KNN、ARIMA、LSTM,先在模拟盘上跑三个月再谈实盘。这套流程说不上温柔,但至少能让你少交学费。希望帮到你。
本文还有配套的精品资源,点击获取