简介一份基于机器学习预测股票价格趋势的Python仿真项目面向毕业设计、期末大作业及课程设计场景适合希望快速搭建完整预测流程的本专科学生和机器学习初学者。项目以LSTM神经网络为核心覆盖数据读取、特征处理、模型构建、训练评估和结果可视化等环节代码注释细致新手也能看懂。压缩包内含13个文件包括5个Python脚本、1个上证指数CSV数据集、1个训练好的stock.pkl模型、2张结果可视化图片及README说明文档整体仅356KB轻量而完整。其中dataset.py负责数据预处理LSTMModel.py定义网络结构train.py和evaluate.py分别完成训练与评估parser_my.py辅助解析目录结构清晰便于按需学习。已有303人学习下载是作者独立完成并获评98分的高分项目得到导师认可直接下载部署即可使用也可作为毕业设计或课程设计的重要参考。1. 机器学习预测股票价格趋势先搞清楚它到底在预测什么做股票趋势预测的Python仿真最常栽的跟头不是模型不够强而是从一开始就把问题定义错了。基于机器学习预测股票价格趋势本质上是拿历史行情数据训练一个分类或回归模型去预测未来一段时间的方向——涨、跌还是横盘——而不是去预测那个精确到小数点后两位的收盘价。这个区别决定了你后面所有代码怎么写、指标怎么选、结果怎么解读。标题里提到的“完整代码说明文档数据”对应的是工程化的落地交付物数据怎么清洗、特征怎么构造、模型怎么训练、结果怎么评估每一步都有章可循。这篇笔记适合两类人正在做课程设计或毕业设计的学生以及想验证“机器学习到底能不能用在股票上”的Python开发者。我会按一份可复现的仿真工程顺序来讲最后再把我踩过的坑按现象、原因、解决方式列给你。2. 数据是第一个分水岭行情接口选型与特征构造2.1 拿到干净行情数据三个常见数据源怎么选股票趋势预测的第一步不是写模型是把行情数据拿到手且拿对。常见做法是直接用Python第三方库拉数据目前免费且还算稳定的方案有三个baostock、akshare、tushare pro。baostock胜在免费、无需注册token、返回的字段干净适合做仿真akshare数据覆盖面广但接口结构变动频繁今天能跑的代码下周可能就报错tushare pro做复权因子比较专业但积分门槛导致部分接口要付费学生党用它容易被额度卡住。我一般会推荐baostock做主力数据源原因很实际它的日线接口返回前收盘、开盘、收盘、最高、最低、成交量等基础字段且自带前复权和后复权选项省去自己算复权的步骤。拉取一只股票五年日线数据的代码大致如下import baostock as bs import pandas as pd # 登录 baostock本地会话不影响其他数据源 lg bs.login() # 拉取浦发银行 2019-01-01 至 2024-01-01 的日线数据 rs bs.query_history_k_data_plus( sh.600000, date,open,high,low,close,volume,amount,turn,pctChg, start_date2019-01-01, end_date2024-01-01, frequencyd, adjustflag2 # 2代表前复权 ) data_list [] while (rs.error_code 0) and rs.next(): data_list.append(rs.get_row_data()) # 转 DataFrame 并统一类型 df pd.DataFrame(data_list, columnsrs.fields) df[date] pd.to_datetime(df[date]) df[close] df[close].astype(float) df[volume] df[volume].astype(float) bs.logout()这段代码的逻辑很直接先建立baostock本地会话再按股票代码和时间区间拉取日线数据最后把字符串字段转成数值类型。注意adjustflag2表示前复权做趋势预测时建议用前复权数据因为它消除了除权除息带来的价格跳空让模型看到的连续价格曲线更接近真实交易中投资者能观察到的形态。如果你用的是akshare接口名通常是stock_zh_a_hist参数里同样要指定复权方式。无论选哪个源字段统一换成英文名date、open、high、low、close、volume再做后续处理能少踩很多坑。2.2 数据清洗与对齐复权、停牌、缺失值的处理顺序拿到数据后别急着算特征先做三件事检查缺失值、剔除停牌日、检查有无异常值。停牌日不会返回K线记录所以缺失值主要表现为某天没有数据这本身不是问题问题是你后续构造技术指标时滚动窗口的长度是连续的一旦中间缺了日期算出来的均线就是错的。处理顺序上我建议先按日期排序再判断是否有某只股票存在“交易日跳空”。A股交易日往往有节假日调休直接用自然日历判断不靠谱更稳妥的办法是用交易日历做对齐。baostock有query_trade_dates接口可以拿到交易日历把数据表与交易日历做左连接缺失的日期用前值填充或直接丢弃——对趋势预测来说直接丢弃比填充更安全因为股票数据不像传感器数据天然连续你无法合理构造一个“假想收盘价”。# 用交易日历对齐并过滤非交易日的空行 trade_dates bs.query_trade_dates(start_date2019-01-01, end_date2024-01-01) date_list [d[0] for d in trade_dates.get_data()] df[date] df[date].astype(str) df df[df[date].isin(date_list)].reset_index(dropTrue) # 检查显式缺失同一日期出现多条记录或NaN assert df[date].duplicated().sum() 0, 存在重复交易日 assert df[close].isna().sum() 0, 收盘价存在缺失以上代码先拿交易日历过滤原始数据再用断言快速检查两条硬性规则日期不能重复、收盘价不能有NaN。断言在这里不是为了摆样子而是仿真代码里非常实用的“体检”手段——数据问题一旦带上特征工程就会被放大到模型结果里届时再排查成本高得多。异常值方面A股有涨跌停制度单日涨幅超过10%的股票通常意味着ST或特殊板块这类样本要不要保留取决于你的预测目标我在后面的避坑章节会单独讲。2.3 特征构造不只是收盘价技术指标与统计特征的取舍很多人在机器学习预测股票上翻车根本原因是特征只有“过去N天的收盘价”。价格序列本身的平稳性很差直接把原始价格喂给模型模型学到的往往是“昨天的价格是多少”这类无意义的记忆而不是趋势规律。特征构造要分三层基础行情特征、技术指标特征、统计截面特征。基础行情特征最简单包括过去5日、10日、20日的收益率、振幅、成交量变化率。技术指标特征常用的是MA5/MA10/MA20、MACD的DIF与DEA、RSI、布林带位置。统计截面特征指的是把同一时间截面上所有股票的数据拿来一起算的指标比如个股收益率在全市场中的排名分位。对单股票仿真来说前两层就够用了。需要特别提醒的是技术指标存在着高度的多重共线性MA5和MA10本身就是从收盘价算出来的MACD又是从MA算出来的。把它们全部堆进去模型的训练时间变长还容易让树模型对某一类特征过度敏感。我的做法是分步验证——先跑一版只有基础行情的模型作为baseline再加技术指标再看是否有效而不是一次性铺开几十列特征。# 构造基础特征过去N日收益率与波动率 df[ret_1] df[close].pct_change(1) df[ret_5] df[close].pct_change(5) df[ret_10] df[close].pct_change(10) # 构造技术指标MA5/MA10/MA20 与价格偏离度 df[ma5] df[close].rolling(5).mean() df[ma10] df[close].rolling(10).mean() df[ma20] df[close].rolling(20).mean() df[bias] (df[close] - df[ma20]) / df[ma20] # 价格偏离20日均线的幅度 # 构造动量特征 df[momentum] df[close] / df[close].shift(10) - 1 # 这些窗口值在序列头部会产生NaN直接丢弃 df_model df.dropna().reset_index(dropTrue)这段代码的核心思想是所有特征都只使用过去的数据计算不掺入未来信息。pct_change(5)的含义是“当前收盘价相对于5天前的变化率”它在计算时只用到了当前及更早的行情不会偷看未来。shift(10)同理是向前平移10天取价格。你只要在整个工程里守住“特征计算不碰未来数据”这条底线后面在回测环节就能少掉80%的坑。3. 标签设计与模型选型预测趋势不是预测价格3.1 为什么把回归问题改成分类问题直接用机器学习回归预测“明天的收盘价是多少”听起来更刺激实际做下来会发现模型输出基本就是一条滞后的价格曲线——它学不到涨跌规律只是把昨天的价格搬过来。原因在于价格序列的噪声远大于信号MSE损失函数会让模型趋向于预测均值区间也就是价格的中位水平。更务实的目标是趋势方向未来N天收益率是正还是负。把回归改成二分类有几层好处一是标签更稳健不会因为某一天的微小波动导致目标值剧变二是评价指标更直观准确率、精确率、召回率可以直接读三是分类模型的输出可以转化为置信度用于后续的仓位决策。还有人在二分类基础上做三分类涨/跌/横盘横盘阈值通常设在±0.5%以内。三分类更贴近交易真实感受但类别不平衡问题会更明显——A股大部分时间其实在横盘。如果你坚持做回归也不要预测绝对价格而是预测未来N日收益率。这样至少目标变量是平稳的模型学起来比直接预测价格靠谱得多。我在仿真项目里通常把回归版跑一遍当作baseline然后主模型用分类。3.2 标签滑窗未来N日收益率的阈值切分标签是怎么从历史数据里长出来的核心是滑窗。假设要预测“未来5日是否上涨”那么第T天的标签就是close[T5] / close[T] - 1是否大于某个阈值。这个计算必须当场完成并缓存不能等训练时临时算否则排列对齐很容易出乱子。# 生成未来5日收益作为标签基础 df[future_ret] df[close].shift(-5) / df[close] - 1 # 二分类标签未来5日收益大于0.5%视为上涨小于-0.5%视为下跌中间丢弃 label_threshold 0.005 df[label] 0 df.loc[df[future_ret] label_threshold, label] 1 df.loc[df[future_ret] -label_threshold, label] 0 df df.dropna(subset[label]).reset_index(dropTrue)shift(-5)就是未来函数的高危区。这段代码里它被用来构造标签属于“标注阶段允许使用未来信息”的特例但特征绝不能这样做。很多初学代码把future_ret也当特征喂进模型然后训练集准确率高达98%一到实盘就原形毕露——不是在预测市场是在偷看答案。阈值0.5%的含义是未来5天涨不到0.5%以上就不算“上涨”以此过滤掉噪声样本。阈值可以调但切记只能依据训练集分布来调测试集不能参与阈值选择否则又会过拟合。3.3 模型选型树模型与线性模型的边界XGBoost与LightGBM的适用位置股票趋势预测这种表格型数据首选是梯度提升树模型XGBoost和LightGBM都行。原因是特征之间高度非线性、特征尺度不一致、存在大量缺失值或者无效区间树模型对这些问题天然不敏感而线性模型需要做标准化、特征选择、处理共线性这一套流程下来工作量加倍效果还不一定好。XGBoost在中小规模数据集上表现稳定收敛慢一点但不容易出幺蛾子LightGBM训练更快但一旦数据量不足或特征噪声太大更容易过拟合。我的习惯是数据量在5万行以下用XGBoost以上再考虑LightGBM。神经网络不是不能用LSTM、Transformer等模型确实有做时间序列预测的论文支撑但复现成本极高对超参数敏感在一份仿真工程里反而容易把“机器学习”做成了“深度学习调参”偏离标题主线。from xgboost import XGBClassifier from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import accuracy_score, precision_score, recall_score # 特征列与标签列分离 feature_cols [ret_1, ret_5, ret_10, ma5, ma10, ma20, bias, momentum] X df_model[feature_cols] y df_model[label] # 时序交叉验证按时间顺序切分防止未来信息渗入训练集 tscv TimeSeriesSplit(n_splits5) for train_idx, val_idx in tscv.split(X): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model XGBClassifier( n_estimators200, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, eval_metriclogloss, use_label_encoderFalse ) model.fit(X_train, y_train, eval_set[(X_val, y_val)], verboseFalse)关键参数说明一下。max_depth4控制每棵树的深度对金融噪声数据深度超过6基本就会开始记忆噪声subsample0.8让每棵树只采样80%样本增加随机性防止过拟合learning_rate0.05配合200棵树相当于用更多弱学习器逐步逼近目标。TimeSeriesSplit是这里最重要的组件它不像普通K折那样随机打乱数据而是严格按时间顺序前训练后验证这也是金融时序建模与普通机器学习分类任务最本质的差异。4. 跑通完整仿真训练流程与评测代码4.1 项目文件结构与主流程完整仿真工程的组织方式直接决定了这个项目是“代码能跑”还是“可复现、可解释、可答辩”。我常用的结构是四个目录加一个主脚本data/放原始数据缓存的CSVfeatures/放特征工程代码models/放训练与评测代码output/放评估结果和图表。主脚本run_pipeline.py按“取数→清洗→特征→标签→训练→评测”的顺序串联起来保证任何一步都可以单独重跑。这个结构看起来简单但对仿真项目非常关键原因在于数据缓存。免费接口都有频率限制如果你的特征工程改了某个参数重跑时直接读本地CSV就行不必再调接口拉一次数据。另一方面课程设计或答辩场景里老师几乎一定会问“数据哪来的”“每个文件是干嘛的”这套结构正好能清晰地回答这些问题。4.2 训练与验证时间序列分割是重点一个完整的训练流程分三步全量特征数据准备、时序切分、多轮交叉验证取平均值。第3章里已经演示了单次切分的训练写法这里要补上每一折的指标汇总避免只看一次运气结果。import numpy as np cv_results {accuracy: [], precision: [], recall: []} for train_idx, val_idx in tscv.split(X): X_train, X_val X.iloc[train_idx], X.iloc[val_idx] y_train, y_val y.iloc[train_idx], y.iloc[val_idx] model XGBClassifier( n_estimators200, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, eval_metriclogloss, use_label_encoderFalse ) model.fit(X_train, y_train, verboseFalse) y_pred model.predict(X_val) cv_results[accuracy].append(accuracy_score(y_val, y_pred)) cv_results[precision].append(precision_score(y_val, y_pred)) cv_results[recall].append(recall_score(y_val, y_pred)) # 打印每一折的关键指标 print(fFold acc{cv_results[accuracy][-1]:.4f}, fprec{cv_results[precision][-1]:.4f}, frec{cv_results[recall][-1]:.4f}) # 汇总五折均值 print(fMean accuracy{np.mean(cv_results[accuracy]):.4f}, fMean precision{np.mean(cv_results[precision]):.4f}, fMean recall{np.mean(cv_results[recall]):.4f})这段代码的逻辑在于把每一折的预测结果分别计算指标最后再取平均而不是把所有折的预测结果混在一起算一个总分。后者会掩盖不同时间段上的性能波动——模型可能在某些年份有效、某些年份失效混合评测会把这种波动平均掉不利于定位问题。打印每一折的指标还有一个好处你能直观看到模型性能在时间轴上是如何变化的。如果前面几年准确率55%左右、后面突然变65%那大概率是你的特征在后段时间碰巧捕捉到了某种市场风格而不是模型泛化能力变强了。4.3 评测指标与第一次结果解读股票趋势预测的评测指标不能只看准确率。准确率在类别不平衡时会骗人假设上涨样本占60%模型全预测上涨就有60%准确率。所以要看精确率和召回率的组合。精确率表示“模型说涨、实际也涨”的比例召回率表示“实际涨了、模型也捕捉到”的比例。做趋势预测时我更关注精确率因为误判一次的代价是实打实的亏损而漏掉一次上涨最多是少赚。首次跑通时指标多少算正常说实话日频预测能稳定做到准确率53%-55%、精确率55%以上已经是相当不错的结果足以说明特征里有真实信号。如果有人跟你说准确率能做到80%以上那几乎可以肯定存在数据泄漏或用了未来函数。拿到第一次结果时别急着调参先去检查预测错误的样本长什么样——是被极端行情带崩的还是在横盘震荡中频繁误判。这个检查过程往往比换模型更有效。# 将最后一折的预测结果按实际日期归位观察错误样本的时间分布 import matplotlib.pyplot as plt X_val_with_date df_model.iloc[val_idx][date].reset_index(dropTrue) pred_df pd.DataFrame({ date: X_val_with_date, actual: y_val.values, pred: y_pred }) pred_df[hit] (pred_df[actual] pred_df[pred]).astype(int) # 按月统计命中率定位失效时间段 pred_df[ym] pred_df[date].astype(str).str[:7] monthly pred_df.groupby(ym)[hit].mean() print(monthly)这段代码把预测结果按月份聚合输出每个月的命中率序列。你会发现某些月份命中率只有40%紧接着某个月份又到60%。这种波动是正常的因为股票市场存在风格切换——你的特征可能在振荡市中表现好在单边趋势中表现差。把逐月命中率打印出来是对仿真结果做“诊断”而非“欣赏”的第一步也便于在说明文档中给出诚实的结论。5. 避坑为什么你的模型训练时95分、回测却亏钱5.1 坑一标准化偷跑了未来数据现象是模型在训练集上准确率极高但换一段时间的验证集就崩盘而且怎么调参都没用。原因对特征做了Z-score标准化或MinMax缩放时用了全量数据的均值与方差。标准化这个操作本身看着无害但它会把未来信息编码进当前样本的取值里——你的训练集和验证集新闻了同一个“全局统计量”验证集里的信息就这样被泄露给了模型。解决标准化必须在每次时序切分之后做且只能使用训练集的统计量。树模型其实不需要特征标准化所以最简单的做法就是直接用树模型压根不做归一化。如果你非要试逻辑回归或SVM用sklearn.pipeline.Pipeline把标准化器放到切分循环内部并保证fit只在训练子集上调用。5.2 坑二shift方向搞反把未来当成了历史现象是模型在验证集上准确率异常高高到接近80%甚至90%但实盘想法完全找不到这种水平。原因标签构造时用了shift(-N)取未来数据这没错但某些特征在清洗时也顺手用了shift(-N)等于把未来N天的价格信息搬到了今天。最常见的情况是计算未来收益率时原来写的是close.shift(-N) / close - 1后面整理代码时误改成close / close.shift(N) - 1语义完全变了。解决把特征和标签的构造代码分开放在两个不同函数里特征函数禁止出现负数shift标签函数单独注释“此处允许未来数据”。另外在评测时做一个leakage检查——随机打乱标签后重新训练如果模型在新数据集上准确率依然超过55%说明特征里混入了与标签直接相关的信息回到代码里逐列审查。5.3 坑三用随机K折替代时间序列切分现象是模型交叉验证分数比时间序列切分结果高出3到5个百分点让你对模型真实能力产生误判。原因普通K折随机打乱样本后训练集里包含未来时间段的样本。股票数据具有很强的时序相关性——今天的价格和明天的价格高度相关干脆说这不是特征的问题是评测方式的问题。随机切分让模型在训练时见识过“未来”验证分数天然虚高。解决统一用TimeSeriesSplit或者更严格的实现——训练集结束日期必须在验证集开始日期之前至少留出N个交易日的空隙避免因MA20这类窗口特征把相邻样本“粘”在一起。空隙大小建议与特征最长窗口一致用20个交易日作为缓冲。5.4 坑四ST股与涨跌停样本没有剔除现象是模型预测结果里频繁出现连续涨停股票被预测为“下跌”导致精确率大幅下滑。原因A股有涨跌停制度涨停时买不进、跌停时卖不出。模型从历史数据学到的是“涨停后继续上涨”的模式但真正决策时你无法在涨停板买入所以这种预测没有实操意义。另外ST股涨跌幅限制只有5%序列波动特征与普通股票完全不同混在一起训练会干扰模型。解决在数据清洗阶段就过滤掉代码前缀为ST的股票剔除单日涨幅超过9.5%非ST科创板创业板阈值不同或跌幅低于-9.5%的极端交易日或者给这些日子打标签时单独处理。如果你的目标是做可交易的预测而不是纯学术实验这一步不能省。5.5 坑五特征窗口和标签窗口重叠现象是模型指标表现良好但策略模拟交易时几乎每一笔都被手续费吃光。原因特征用了过去20日均线标签预测的是未来5日涨幅两者时间窗口没有重叠这是没问题的但如果你把特征拓宽到未来10日、而标签也是未来5日间接引入了未来数据。更隐蔽的情况是回测时在T日收盘后计算特征但回测撮合用的是T日开盘价等于用收盘后的信号去交易开盘前的价格。解决统一时间约定——特征全部基于T日及之前的数据交易信号在T日收盘后生成最快在T1日开盘执行。在回测代码里用一个signal.shift(1)把信号整体后移一天就能模拟这种延迟。手续费按双边万三加滑点万五估算趋势预测的边际收益本来就不高交易成本模型必须加进仿真。6. 进阶把仿真结果用到真实交易前的最后一道校验仿真模型在历史数据上表现稳定距离真实可用还有一道关键校验——滚动前推验证也就是walk-forward validation。这个方法的核心纪律是模型每预测完一段时间就把这段时间的真实数据并入训练集重新训练模型再预测下一段。它模拟的是真实交易中“边学边做”的过程比一次性训练后固定模型更贴近实战。# 滚动前推验证每月重新训练一次模型 train_size 1200 # 用过去1200个交易日做训练 step_size 30 # 每个自然月新增30个交易日样本 preds [] for start in range(train_size, len(X) - 30, step_size): X_train X.iloc[start - train_size:start] y_train y.iloc[start - train_size:start] X_test X.iloc[start:start step_size] model XGBClassifier( n_estimators150, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, eval_metriclogloss, use_label_encoderFalse ) model.fit(X_train, y_train, verboseFalse) preds.extend(model.predict(X_test)) # 对齐预测与真实标签计算滚动区间内的性能 y_aligned y.iloc[train_size:train_size len(preds)] print(fRolling accuracy{accuracy_score(y_aligned, preds):.4f})滚动窗口有两个参数影响结果train_size不能太小树模型至少需要上千个样本才能学出稳定模式A股单只股票5年日线数据大约1200个交易日step_size30表示每月重训一次太频繁会过拟合最近行情太稀疏会跟不上市场风格变化。做这个验证的另一个价值是观察“模型退化”时点——如果滚动准确率在最近半年持续低于50%说明市场结构变了你的特征集已经失效而不是模型参数出了问题。说句实在话我做了几次这样的仿真项目之后最大的感受是股票趋势预测的价值不在“靠模型发财”而在“用一套严谨流程检验一个想法是否真的有效”。滚动前推验证教会我的不是调参技巧而是克制——每次想放大阈值、加深树深度来提升历史分数时我都会想起之前用随机K折测出70%准确率、实盘却连续止损的经历。一套能区分“真信号”和“数据泄漏”的仿真流程比一个漂亮的准确率数字值钱得多。把这份代码跑通、把误判样本逐月拆开看明白你就已经超过了绝大多数只会调库的仿真项目。希望帮到你。本文还有配套的精品资源点击获取