简介:一套面向毕业设计、期末大作业与课程设计场景的机器学习股价预测Python仿真项目,完整提供从数据解析、特征数据集构建、LSTM模型定义到训练评估的端到端实现。压缩包共13个文件,其中5个Python脚本为程序主体,涵盖数据读取、数据预处理、模型配置、训练与效果评估等模块;另含csv格式的原始行情数据、pkl格式的已训练模型、png可视化结果图表及md格式说明文档,数据、代码、结果与文档齐备。资源仅356KB,部署简单,代码内附详细注释,能够降低新手理解门槛。该项目源自个人手打高分作品,据称导师认可度较高,适合需要参考完整项目结构、学习时间序列预测建模流程的学生直接使用。目前已有303人学习下载。
1. 机器学习预测股票价格趋势:为什么先预测方向而不是预测价格
第一次拿到“基于机器学习实现预测股票价格趋势的Python仿真”这类题目的人,多半会先去找一份现成的股票价格预测代码,然后发现两个极端:要么跑出来分数虚高,换一段行情就漏气;要么整个工程只有训练和预测,没有说明文档和数据,答辩时根本讲不清楚自己在做什么。这篇笔记想把这套方案拆开——数据从哪来、怎么洗、怎么造特征、用什么模型、简单到什么程度还能出活、哪些坑会让人辛辛苦苦做出一个自欺欺人的结果。适合正在做课程设计、毕业设计,或准备量化比赛的新手;老手可以直接看第4章和第5章,那里是我调这类仿真时翻过车以后才总结出来的边界。
2. 用Python做股票趋势预测前,先把数据从CSV处理成样本
2.1 数据源选型:自带CSV、Tushare还是AkShare
预测股票价格趋势的前提,是把行情数据读成一张能喂进机器学习模型的表。常见做法有三个:直接读题目自带的数据文件、用Tushare按token拉数据、用AkShare免token抓数据。对课程设计和仿真练习来说,我的建议顺序是:自带CSV最优先,其次是AkShare,最后才考虑Tushare。原因很简单,自带数据已经把格式定好了,你不需要在反爬和接口参数上浪费时间;AkShare不用注册就能拉数据,适合临时补数据;Tushare虽然字段规范且稳定,但要先去官网注册拿token,而且它有积分限制,某些接口新账号调用不了。
不管数据从哪个来源来,第一步都是先看数据长什么样。拿一份常见的日线CSV举例,它通常至少包含日期、开盘价、最高价、最低价、收盘价和成交量这六列。先把文件读进来,确认列名和类型,再做后续清洗。
import pandas as pd # 读取日线行情数据,请把路径换成自己手头的文件 df = pd.read_csv("stock_daily.csv", encoding="gbk") print(df.head()) # 先看前几行,确认列名和数值是不是正常 print(df.dtypes) # 检查数据类型,日期列必须是 datetime,价格列必须是数值 print(df.isnull().sum()) # 统计缺失值,决定后续怎么处理代码的逻辑分三步:head看数据长相、dtypes看类型、isnull看缺失。很多新手拿到CSV直接就开始算特征,结果日期列是字符串、价格列里面混着逗号或者空行,后面全部报错。encoding参数也要留意,国产金融数据CSV常常用GBK编码,直接read_csv读会乱码,这时把encoding改成gbk或gb18030。
2.2 时间对齐与缺失值处理:停牌日和字符串日期怎么洗
行情数据的清洗有两个固定动作:把日期规范化,以及把缺失值处理干净。日期列如果不是datetime类型,后面所有按时间切片、对齐合并的操作都会出问题。规范的写法是先转类型、再排序、再设索引。这三步顺手做完,后面按时间切片才不会乱套。
缺失值在股票数据里很常见:停牌日没有交易、某些指标数据上线晚、数据供应商对个别股票的天数不足。处理缺失值没有标准答案,我的习惯是区分对待——如果是构造特征时产生的缺失(比如滞后第10天收益率,前面9天天然没有),直接保留NaN,模型能处理;如果是因为停牌导致的整行缺失,用dropna删掉,因为你无法用一行不存在的行情去预测未来。
df["date"] = pd.to_datetime(df["date"]) # 字符串转日期类型 df = df.sort_values("date").reset_index(drop=True) # 按日期升序排好 df = df.drop_duplicates(subset="date") # 同一个交易日只留一条 df = df.dropna(subset=["close", "volume"]) # 核心行情缺失的直接删除这一步看起来不起眼,但它决定了后面所有特征计算的准确性。drop_duplicates是针对同一个日期出现多行记录的情况,数据源拼接时常出这种问题;dropna只针对核心价格和成交量字段,不是对全表删空,因为后面构造的滞后特征本来就会有NaN。如果你把全表dropna,样本量可能少掉一半,得不偿失。
2.3 把行情变成特征:滞后期、涨跌幅、波动率,避开标签泄露
预测股票价格趋势,本质上是把行情序列转换成“特征-标签”对。特征是用历史已知信息构造的数值表,标签是你想预测的未来结果。最常见的做法是预测未来N天的涨跌方向,也就是分类问题。为什么不直接预测价格?因为价格序列是非平稳的,直接对价格做回归很容易出现“预测值永远等于最近收盘价”这种没有任何交易价值的模型;而预测方向把问题简化成“涨/跌/平”,模型学的是规律,不是死记价格数字。
构造特征要严格遵守一条铁律:特征里只能包含T时刻及T时刻以前的信息,标签必须是T时刻以后的信息。稍微松懈一点,就会造成未来函数泄露——也就是你的模型在训练时偷看到了答案。具体实现上,最核心的手段就是shift。
# 基础特征:过去 N 天的收益率、振幅、成交量变化 df["ret_1"] = df["close"].pct_change(1) # 前一日收益率 df["ret_5"] = df["close"].pct_change(5) # 前5日累计收益率 df["ret_10"] = df["close"].pct_change(10) # 前10日累计收益率 df["amp"] = (df["high"] - df["low"]) / df["close"] # 当日振幅 df["vol_change"] = df["volume"].pct_change(1) # 成交量变化率 # 特征统一滞后一天:用昨天及之前的数据,预测今天的下一个方向 feature_cols = ["ret_1", "ret_5", "ret_10", "amp", "vol_change"] for col in feature_cols: df[col] = df[col].shift(1) # 标签:未来5个交易日涨跌方向,1 表示涨,0 表示跌 df["label"] = (df["close"].shift(-5) > df["close"]).astype(int) df = df.dropna()这里的逻辑是:先把收益率、振幅、量比算出来,这是基于当前及历史行情的信息;随后用shift(1)把所有特征整体往后挪一行,意思是“今天开盘前我能看到的所有信息,截止到昨天收盘”;标签则用shift(-5)取未来第5天的收盘价和今天比较,1表示未来5天是涨的。shift的方向很多人搞反,我当年也在这里栽过——特征用shift是为了防止用当天的信息预测当天,标签用shift(-5)是为了把答案挪到未来。最后统一dropna,把前10个没有完整历史的交易日清理掉。
特征工程的深度可以无限加:MACD、RSI、布林带位置、均线偏离度都有人做,但对课程设计这个体量的仿真来说,收益率加振幅加量比已经能说明完整流程了。真正影响评分的是这段代码有没有讲清楚“为什么特征要滞后、标签为什么要前置”,而不是特征的数量。
3. 在Python里跑通股票趋势预测的第一版仿真:从读数据到出预测
3.1 环境依赖:把Python和必需的库装好,别一上来就上深度学习
趋势预测仿真对机器要求不高,Anaconda自带的那套环境基本够用。需要确认的是pandas、numpy、scikit-learn、matplotlib这四个库已经装好。深度学习在这个场景里不是必须的,像LSTM、Transformer这类模型参数多、训练慢、可解释性差,课程设计里如果用不好,反而会让答辩变成事故现场。scikit-learn里的随机森林和逻辑回归,跑得快、解释清楚、参数少,足够撑起一个完整的高分仿真。
# 在命令行里安装所需依赖 pip install pandas numpy scikit-learn matplotlib如果你是在做比赛或者毕设,强烈建议把环境依赖单独写一个requirements.txt放进项目里,让答辩老师知道你的工程是可复现的。这一条看似不起眼,但评分的“完整度”很大程度体现在这种地方。安装时如果遇到scikit-learn和numpy版本冲突,通常是网络源或Python版本太老导致的,换个Python 3.9以上的环境就好,不要在旧版本上死磕。
3.2 按时间顺序切分训练集和测试集:为什么不能shuffle
做完特征工程后,接下来要面对一个很容易犯错的环节:数据切分。用分类问题的通用写法train_test_split时,shuffle默认是True,也就是先把样本随机打乱再切。这个操作对普通机器学习任务没问题,但对股票时间序列是致命的——它会让训练集里混进未来的样本,测试集里也能看到过去的信息,模型在训练时等于提前读到了未来,测试分数虚高得离谱。
正确做法是按时间顺序切分,前70%或80%做训练,后30%或20%做测试。这只是一种基础切法,真实世界里还有滚动预测、扩窗训练这些进阶玩法,留到第5章再展开。第一版仿真先按时间切出干净的训练和测试集。
3.3 完整训练与预测脚本:随机森林怎么调才不出虚高的分数
随机森林是这里最稳的模型选择。它属于集成学习,对特征尺度不敏感,不需要标准化,容忍噪声和缺失值,而且可以直接输出每个特征的重要程度。周志华《机器学习》里讲集成学习时提到,随机森林通过多棵决策树投票来降低方差,这正是股票这种高噪声数据需要的特性。
import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, classification_report # 假设 df 经过了上一章的特征处理,包含 feature_cols 和 label X = df[feature_cols].values y = df["label"].values # 按时间顺序切分,前80%训练,后20%测试 split = int(len(df) * 0.8) X_train, X_test = X[:split], X[split:] y_train, y_test = y[:split], y[split:] # 随机森林:控制树的数量和深度,固定随机种子保证可复现 model = RandomForestClassifier( n_estimators=200, # 树的数量,越多越稳但越慢 max_depth=6, # 限制深度,防止单棵树记住噪声 min_samples_leaf=10, # 叶子节点最少样本数,太少了容易过拟合 random_state=42, # 固定种子,别人复现结果一致 class_weight="balanced" # 自动平衡涨跌样本数量 ) model.fit(X_train, y_train) # 在测试集上预测,并输出评估结果 y_pred = model.predict(X_test) print("测试集准确率:", accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred))代码的核心在三个参数:max_depth=6限制每棵树的深度,避免树把训练样本死记下来;min_samples_leaf=10强制叶子节点至少覆盖10个样本,进一步抑制过拟合;class_weight="balanced"解决涨跌样本不均衡问题,后面第4章会展开讲。这三个参数是股票预测这种低信噪比场景的合理起点,不要一上来就抄鸢尾花分类的参数,那种默认设置在金融数据上会直接过拟合。
训练完成后,预测方向只是第一步,还要把它转成可展示的结果。很多课程设计只输出一个准确率数字,然后就没了,这不够。至少要把测试集的预测涨跌画成曲线,与真实涨跌放在一起对比,让老师一眼看到模型在哪些时段跟上了行情、哪些时段是完全反向的。
import matplotlib.pyplot as plt test_df = df.iloc[split:].copy() test_df["pred_direction"] = y_pred # 画真实收盘价曲线,再把预测为涨的点标红、预测为跌的点标绿 plt.figure(figsize=(12, 5)) plt.plot(test_df["date"], test_df["close"], color="gray", linewidth=2) plt.scatter(test_df.loc[test_df["pred_direction"]==1, "date"], test_df.loc[test_df["pred_direction"]==1, "close"], color="red", s=8, label="predict up") plt.scatter(test_df.loc[test_df["pred_direction"]==0, "date"], test_df.loc[test_df["pred_direction"]==0, "close"], color="green", s=8, label="predict down") plt.legend() plt.show()这段代码的作用是把预测方向叠加到价格曲线上,红色是模型判断未来5天要涨的交易日,绿色是判断要跌的。如果红色密集出现在上涨区间、绿色密集出现在下跌区间,说明模型学到了行情结构;如果颜色随机散布甚至反向,说明特征和模型都有问题,需要回去检查数据清洗。
完整的仿真还要包含一份说明文档,里面至少写清楚三件事:数据集来源与预处理规则、特征含义、模型选择理由与参数说明。这一套做下来,从数据到特征、从模型到展示,才是标题里“完整代码+说明文档+数据”真正对应的高分结构。
4. 预测股票价格趋势的5个高频翻车点:从未来函数到虚高准确率
4.1 未来函数:特征和标签用了同一天数据
现象:训练集准确率98%,测试集准确率也有90%以上,模型表现好得不像话,但把预测结果画到K线图上,发现模型给出的买点和卖点都完美踩在转折点上,像开了透视。原因:构造特征时没有做滞后,直接用当天的收益率、振幅作为特征去预测当天的涨跌方向。当天涨跌幅本身就能直接决定方向,模型当然“聪明”。解决:所有特征必须shift(1),保证模型用的信息截止到昨天收盘;标签用shift(-n)指向未来第n天。判断有没有踩这个坑,最简单的自查方法就是看特征里有没有当天收盘价直接算出来的收益。
4.2 shuffle毁掉的时间序列:训练集里混进了未来
现象:用train_test_split(X, y, test_size=0.2)跑出来的准确率比按时间切分高出一大截,但同样一组参数放到新数据上就哑火。原因:train_test_split默认shuffle=True,随机打乱后训练集里有未来样本,模型实际上站在未来回顾过去。解决:按时间顺序手动切片,或者用sklearn.model_selection.TimeSeriesSplit做交叉验证。这个坑最隐蔽,因为代码不报错,分数也不难看。
4.3 复权价没处理:除权除息让模型学到一根假K线
现象:某个交易日的收盘价突然从20元跳成15元,看起来像是暴跌,模型据此输出强烈卖出信号,但真实原因是股票除权除息,不是行情崩了。原因:数据源没有做前复权或后复权处理,直接把原始价格拿来算收益率。解决:在做特征工程前,把价格列统一换成后复权价;如果数据源本身已经做了前复权,要确认整个文件口径一致,不要前半段是复权价、后半段是原始价。这类问题在带成交量特征的模型里更隐蔽,因为除权日成交量通常也会异常放大,两个假信号叠加,预测结果完全失真。
4.4 涨跌样本不均衡:90%准确率只是瞎蒙对了方向
现象:测试集准确率60%,看起来还行,但仔细看分类报告,涨类召回率只有20%,跌类召回率90%,也就是说模型大部分时候只会预测“跌”。原因:样本里跌的样本远多于涨的样本,模型学到的策略是“全猜跌也能拿高分”。解决:在分类模型里设置class_weight="balanced",或者对训练集中跌的样本抽样降权;评估时同时看准确率、精确率、召回率和F1分数,而不是只盯着准确率一个指标。对于股票趋势预测,一个合理的基准是:测试集准确率稳定高于55%,且涨跌两类的召回率没有明显偏科。
4.5 训练分数高、持续亏损:过拟合在金融数据上的真实表现
现象:训练集F1在0.7以上,测试集掉到0.5以下,随机抽几段行情验证,预测结果和抛硬币差不多。原因:树模型在训练时把历史上偶尔出现的巧合当成规律,死记了下来;金融数据噪声极大,特征里真正有效的信息占比非常低。解决:先把特征砍到最核心的5个以内,然后调大min_samples_leaf,把叶子节点样本数从1提到10、20甚至30。另一个有效手段是对特征做重要性排序,筛选出模型认为重要的前3个特征重新训练,通常比堆一大把特征更稳。
5. 做一次真正能信的验证:把预测趋势变成回测收益曲线
5.1 先做前向验证:滚动训练模拟真实交易节奏
测试集上一锤子买卖预测完就结束,这在课程设计答辩里够用,但如果你想验证这个模型值不值得继续投入,得做一次前向验证。方法很简单:从测试集的起点开始,每次只用截止当日的数据训练,预测未来5天方向,然后往前走5天,再用扩充后的数据重新训练。这个过程模拟的是真实操作中“每天收盘后更新模型、第二天按信号下单”的节奏。
predictions = [] dates = df["date"].iloc[split:].tolist() for i in range(split, len(df) - 5, 5): # 截至第 i 天为止的历史数据 train_data = df.iloc[:i] X_train = train_data[feature_cols].values y_train = train_data["label"].values # 重新训练一个随机森林,参数与主模型一致 model = RandomForestClassifier( n_estimators=200, max_depth=6, min_samples_leaf=10, random_state=42, class_weight="balanced" ) model.fit(X_train, y_train) # 预测第 i 天之后未来5天的方向 X_new = df.iloc[i][feature_cols].values.reshape(1, -1) pred = model.predict(X_new)[0] predictions.append((dates[i], pred)) print("前向验证样本数:", len(predictions))这段代码的成本比一次性训练高,但它给你的信息量大得多:如果滚动训练出来的预测准确率还稳定保持在高位,说明模型学到的规律是延续的;如果一路下滑,说明特征已经失效,得回炉重造。注意range的步长设成5,和标签的预测周期保持一致,避免同一个样本被重复预测。
5.2 把预测结果变成模拟交易资金曲线
准确率只是过程指标,最终要看的是资金曲线。写一个简单的模拟器,预测“涨”就全仓买入,预测“跌”就空仓等待,每次交易扣除手续费和滑点,画出账户净值曲线。
# 资金曲线模拟:初始资金10万元,每次预测正确买入持有5天 initial_cash = 100000.0 cash = initial_cash hold = 0 # 持仓市值 stats = [] for date, pred in predictions: if pred == 1 and hold == 0: # 买入:按当日收盘价全仓 position = cash / df_close_at(date) cash = 0 hold = position * df_close_at(date) * (1 - 0.001) # 0.1%手续费 elif pred == 0 and hold > 0: # 卖出:按当日收盘价清仓 cash = hold * (1 - 0.001) hold = 0 stats.append(cash + hold)真实的回测引擎还要考虑涨跌停无法买入、滑点、印花税等问题,但作为仿真练习,这个简化版本已经够验证一个朴素策略的盈利潜力。跑完后把净值曲线和基准指数画在同一张图里,如果模型连随机买入持有的收益率都跑不过,那说明它的“预测”没有转化为“策略收益”,这才是判断一个模型能否落地的最硬标准。
5.3 养成一个习惯:把每次预测的置信度存下来
我现在的习惯是,在前向验证时顺手把predict_proba输出的正类概率一起存下来。概率高但方向错误,和概率低但方向正确,背后的含义完全不同;每次只存0/1方向,复盘时什么都看不到。这个习惯帮我在做特征迭代时省掉很多无效劳动——一次建模后回看置信度分布,如果发现高置信度样本的命中率和低置信度没什么差别,那说明模型输出的概率是虚的,比逻辑回归都差。希望这个技巧对你也有用,少走一段我走过的弯路。
本文还有配套的精品资源,点击获取