
简介这是一套基于机器学习的股票预测与分析完整项目面向计算机专业毕业设计、课程设计及需要实战练习的学习者。项目包含可运行的Python源码、算法模型权重及详细文档说明覆盖数据预处理、特征工程、模型训练与结果可视化等环节并内置多家公司股票历史行情CSV数据便于直接复现股票趋势预测流程。压缩包共2000个文件以png图表、csv数据、npz模型数据、pth权重及py脚本为主另有xml工程配置与说明文档整体约693MB结构完整经过严格调试可直接运行。该项目在CSDN已有614人学习代码注释与文档配套清晰适合作为高分毕业设计参考或进一步扩展到量化交易分析方向。1. 股票预测到底在预测什么先别急着写代码很多人拿到基于机器学习的股票预测这个毕设题第一反应就是找一个LSTM模型把过去60天的收盘价扔进去预测明天的收盘价。回测画出来曲线完美贴合导师看了点头答辩时却被人问一句你这预测出来价格敢拿真钱去交易吗就哑火了。这个标题真正的核心不是用Python写一个预测函数而是如何构建一个符合金融逻辑、可回测、可解释的机器学习预测流程。它解决的是把历史行情、技术指标、交易量等信息转化为能训练的特征把未来一段时间的涨跌或收益率转化为标签训练出能在未见过的数据上稳定有效的模型并用科学的方法评估它值不值得信。适合谁一是做本科/硕士毕设的学生需要完整的源码和文档支撑二是想入门量化的开发者先弄清这条流水线的每个环节怎么串起来。这里强调一个反直觉结论预测价格数值几乎注定失败因为价格序列非平稳、噪声大真正能做的是预测方向上概率——明天涨还是跌、未来5日收益率是否超过阈值。想清楚这个整个毕设的立论就站住了。2. 数据是预测的地基用akshare拉日线数据与清洗拼接2.1 为什么选akshare而不是Excel手工数据常见做法是找历史行情数据源免费的国内接口里akshare最省事不用注册 token直接 pip install akshare就能拉A股日线、分钟线、指数成分股列表。比tushare方便在不需要积分比yfinance的优势是A股复权、停牌状态处理得更符合国内习惯。有的同学喜欢从Wind或Choice导出Excel但那只能做静态快照没法做滚动扩展窗口的真实训练流程而且不同格式的表拼接起来特别容易出错。所以这里建议直接用akshare把数据采集写成可复现的脚本毕设文档里也好写数据来源与获取方式。2.2 拉取沪深300成分股日线数据的代码先用akshare拿到沪深300成分股代码列表再遍历拉日线前复权数据。前复权必须做否则除权除息会让价格出现非自然的跳空模型会把这种假信号当真。下面是最小可用代码import akshare as ak import pandas as pd import time # 1. 获取沪深300成分股返回的DataFrame里有 symbol 和 name stock_300 ak.index_stock_cons_csindex(symbol000300) # 有时候返回的列名为 code先用合法性检查兜底 if code not in stock_300.columns: stock_300 stock_300.rename(columns{代码: code, 名称: name}) codes stock_300[code].astype(str).str.zfill(6).tolist() # 2. 拉取每只股票的前复权日线存成dict data_dict {} for code in codes[:50]: # 毕设跑通流程可以先限50只全量300只容易触发限流 try: df ak.stock_zh_a_hist( symbolcode, perioddaily, start_date20150101, end_date20231231, adjustqfq # qfq 前复权hfq 后复权这里必须用 qfq ) if df is None or df.empty: continue # akshare返回的列名是中文统一成英文便于后续特征工程 df df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume, 成交额: amount }) df[date] pd.to_datetime(df[date]) df df[[date, open, high, low, close, volume, amount]].copy() df[code] code data_dict[code] df except Exception as e: # akShare偶发网络错误跳过这只不要中断整个循环 print(f{code} 拉取失败: {e}) time.sleep(0.12) # 控制请求频率避免被源站限流这段代码里最重要的参数是adjustqfqa股除权除息后如果不复权股价会产生向下的跳空缺口机器学习会把这种缺口当成可预测的规律实际是财务事件不是市场行为。start_date和end_date决定了时间跨度跨度太短模型学不到不同行情阶段牛市、熊市、震荡市建议最少覆盖5年以上最好带一轮完整牛熊。codes[:50]是做原型验证的通用技巧先跑通流程再上全量。2.3 清洗与拼接停牌、异常值、缺失值处理拉下来的数据不能直接用必须做三层清洗。第一层是缺失值停牌日会有空行或NaN最简单的做法是直接删除但要注意有的股票停牌一个月删除后日期不连续后面做滚动窗口时窗口内天数变少特征会失真。常见处理是如果连续缺失不超过5天用最近一个交易日的数据向前填充连续缺超过5天的直接剔除这段区间。第二层是异常值比如成交量因为数据源错误出现0或负值开盘价高于涨停价之类的这类数据直接删掉对应行。第三层是拼接把50只股票的数据纵向堆起来但必须在堆之前给每只股票生成内部的交易日序号因为不同股票停牌日期不同用时间索引对齐反而会错位。def clean_stock_df(df, max_gap5): df df.sort_values(date).reset_index(dropTrue) # 删除成交量或价格0的行 df df[(df[volume] 0) (df[close] 0) (df[high] df[low])] # 日期连续性检查生成期望的交易日序列后求差集 full_dates pd.date_range(df[date].min(), df[date].max(), freqB) missing_dates full_dates.difference(df[date]) if len(missing_dates) max_gap: df df.set_index(date).reindex(full_dates).ffill().reset_index() else: # 缺失过多保留实际交易日期 df df.dropna(subset[close]) # 生成股票内部连续序号供后续时间窗口使用 df[day_idx] range(len(df)) return df all_df pd.concat( [clean_stock_df(d) for d in data_dict.values()], ignore_indexTrue )这里freqB是工作日频率A股周一至周五交易节假日会多算缺失但一般只有几天ffill能处理。day_idx是全流程的关键——后面构造过去60天特征时不能用日历日来框必须用这只股票自己的交易日序号。3. 特征与标签构造把K线转成监督学习样本3.1 标签设计的三种做法涨跌分类、收益率回归、未来N日极值标签是整个预测任务的定义做错了一切白搭。最常用的三种第一种是二分类标签未来第T天的收盘价是否高于今天的收盘价高于为1否则为0这对应持股/空仓决策第二种是回归标签未来T日收益率(close[tT]/close[t] - 1)模型输出一个连续值第三种是未来N日最高价是否超过今日收盘价阈值或者最低价是否跌破阈值这更像目标止盈止损。毕设里建议用第一种分类任务评估简单、答辩好解释而且神经网络和树模型都能输出概率很方便做阈值调整。需要注意的是标签必须完全由未来数据构成不能引入任何当时未知的信息。下面这段代码展示了正确的标签构造方法其中shift(-T)保证了标签与特征在时间上对齐。def make_labels(df, horizon5, modeclassify): df df.sort_values(day_idx) # 未来horizon日收盘价 future_close df[close].shift(-horizon) if mode classify: # 用未来收盘价和当前收盘价比大小作为涨跌标签 df[label] (future_close df[close]).astype(int) elif mode regression: df[label] future_close / df[close] - 1.0 # 最后horizon行没有未来数据标签为NaN训练时必须丢弃 df df.dropna(subset[label]).reset_index(dropTrue) return df为什么shift(-horizon)是时序里最容易出错的点如果不做 shift直接用当天的close和当天的feature那标签里就包含了当天的收盘信息特征如果也用了当天收盘就变成用答案预测答案。另一个常见错误是dropna丢掉了没有未来标签的尾部数据这会导致回测时最后horizon天无法出预测所以策略要在标签有效的区间内逐日滚动。3.2 技术指标特征MA、RSI、布林带用ta库还是手写特征工程里技术指标是必选项。常见做法是用ta库或talib生成RSI、MACD、布林带、ATR等指标但talib在Windows上安装麻烦ta库更友好。不过毕设里更推荐手写指标不是造轮子而是手写代码能保证你清楚每个指标的边界——比如RSI计算时是简单移动平均还是指数移动平均不同实现结果差很多。下面给出核心特征的特征表特征族具体特征计算窗口说明动量类收益率1/5/10/20天用close.pct_change()即可均线类MA5/MA10/MA205/10/20天与当日close的比值归一化波动类标准差5/10/20天close收益率的标准差量价类成交量均值/换手率5/10天volume/流通股本可选超买超卖RSI14天用Wilder平滑实现通道类布林带位置20天(close - lower)/(upper - lower)用ta库一行能生成几十个指标但容易生成未来函数——因为很多库的指标默认用了全量数据做中心化或归一化你看起来没什么问题实际上数据被从未来借了信息。比如有人用ta.MACD之后又对整个DataFrame做Z-score标准化标准化用的是全部数据的均值和方差这就是典型的未来函数。所以准则只有一个所有特征计算必须只用当前时刻及之前的数据写在循环里或者用rolling()不要在构造完整个数据集之后再做全局标准化。import numpy as np import pandas as pd def add_features(df): df df.sort_values(day_idx).copy() close df[close] # 收益率保留多窗口树模型能自己选择用哪个 for win in [1, 5, 10, 20]: df[fret_{win}] close.pct_change(win) # 均线偏离当前价/过去N日均价 - 1 for win in [5, 10, 20]: ma close.rolling(win).mean() df[fma_bias_{win}] close / ma - 1.0 # 波动率用收益率标准差 for win in [5, 10, 20]: df[fvol_{win}] df[ret_1].rolling(win).std() # RSI-14 手动实现 delta close.diff() gain delta.clip(lower0) loss -delta.clip(upper0) avg_gain gain.ewm(alpha1/14, adjustFalse).mean() avg_loss loss.ewm(alpha1/14, adjustFalse).mean() rs avg_gain / avg_loss df[rsi_14] 100 - 100 / (1 rs) # 布林带位置20日均线上下2倍标准差 mid close.rolling(20).mean() std20 close.rolling(20).std() upper mid 2 * std20 lower mid - 2 * std20 df[bb_pos] (close - lower) / (upper - lower).replace(0, np.nan) return df这段代码里的ewm(alpha1/14, adjustFalse)是RSI公式中注明的Wilder平滑用adjustFalse是为了让指数权重从最早时刻开始一致如果使用默认的adjustTrue序列前段权重不同指标值会偏移虽然不算错误但会跟主流平台如TradingView的计算结果对不上答辩时容易被动。3.3 构造样本的时序切分防止未来函数浸入特征和标签都构造好之后下一步是把数据切成样本矩阵。这里最容易踩坑的是把所有股票的数据混在一起做随机切分。绝对不能随机切分因为同一天的样本之间高度相关比如2021年3月全市场都在涨随机切分会把上涨样本同时分进训练集和测试集模型记忆了市场状态测试集准确率虚高一到实盘完全失灵。正确做法是按时间切分比如前80%时间做训练后20%做测试。all_features [col for col in df_panel.columns if col not in [date, code, close, label]] X df_panel[all_features].values y df_panel[label].values dates df_panel[date].values # 按时间切分假设按日期排序后取80%临界点 cut_idx int(len(dates) * 0.8) X_train, X_test X[:cut_idx], X[cut_idx:] y_train, y_test y[:cut_idx], y[cut_idx:]这里有个细节df_panel是已经按日期排序的如果之前concat的时候按股票堆叠要重新sort_values(date)。另外如果你用了股票的day_idx特征那么这个特征本身也是从0递增的模型学到的可能不是市场规律而是第1024天以后都是上涨这属于序号泄漏。所以特征集中绝不要放day_idx、code这类与预测目标没有因果关系的ID类变量。处理办法是把它们设为索引而不是特征列。4. 模型选择与参数调优从逻辑回归到XGBoost4.1 基准模型逻辑回归参数少能跑通全流程很多同学一上来就上LSTM结果训练半天不收敛连数据预处理对不对都不知道。我的建议是第一个模型用逻辑回归它解释性强、训练快、不容易过拟合适合作为毕设的baseline。逻辑回归的本质是学习一组线性权重对特征做sigmoid映射在金融这种低信噪比场景下线性模型往往不比非线性模型差太多而且权重绝对值可以解释成特征影响方向。from sklearn.linear_model import LogisticRegression from sklearn.metrics import accuracy_score, precision_score, f1_score model_lr LogisticRegression(max_iter1000, C1.0, random_state42) model_lr.fit(X_train, y_train) y_pred model_lr.predict(X_test) print(Accuracy:, accuracy_score(y_test, y_pred)) print(Precision:, precision_score(y_test, y_pred))这里C1.0是正则化强度的倒数C越小正则化越强防止过拟合。为什么不设C0.1因为如果特征没做标准化逻辑回归收敛可能有问题。先跑一个StandardScaler对特征做Z-score标准化注意必须用训练集的均值和标准差去变换测试集不能在全量数据上拟合Scaler。这是另一个高频翻车点。4.2 随机森林与XGBoost处理特征重要性和过拟合控制随机森林是比逻辑回归更上一步的选择能捕捉非线性关系还能输出特征重要性。XGBoost在结构化数据上通常比随机森林好一点但参数多、调参复杂毕设里不一定需要追求极致精度把随机森林调好足够拿高分。最关键的两个参数是max_depth和n_estimators股票数据噪声大max_depth别超过6树太深必然过拟合。n_estimators用200左右就行太多会让训练变慢且收益递减。from sklearn.ensemble import RandomForestClassifier model_rf RandomForestClassifier( n_estimators200, max_depth6, min_samples_split20, min_samples_leaf10, random_state42, n_jobs-1 ) model_rf.fit(X_train, y_train) importances pd.Series(model_rf.feature_importances_, indexall_features) print(importances.sort_values(ascendingFalse).head(10))min_samples_split20和min_samples_leaf10是控制粒度的关键金融数据里噪声样本很多如果叶子节点样本太少模型会把个别极端行情当成规律。训练结束后看特征重要性前十通常vol_5、bb_pos、ret_20会排前面如果出现某个不合理的特征比如label自己说明前面构造样本时出了问题。训练集和验证集不能随机打乱这对树模型同样成立。验证集的选取要按时间顺序用后面20%时间的数据而且验证集与训练集之间要有间隔——比如训练集截至2022-06验证集从2022-07开始这样可以避免窗口重叠带来的信息泄漏。4.3 模型输出去做策略从预测概率到信号模型输出0/1是分类结果但真正做回测时要的是概率。predict_proba得到的第二列是上涨概率你可以设定阈值0.5、0.55、0.6只有概率超过阈值才买入。这样做的原因是机器学习模型在概率接近0.5时预测价值很低强行按0/1信号交易会频繁换手光手续费就吃掉收益。y_prob model_rf.predict_proba(X_test)[:, 1] # 只选概率0.55的持有其余空仓 position (y_prob 0.55).astype(int) # 模拟收益第二天真实收益 daily_ret df_panel[close].pct_change().shift(-1).iloc[cut_idx:].values strategy_ret position * daily_ret cum_ret np.cumprod(1 strategy_ret)这里shift(-1)是为了对齐——你在第t天收盘后根据概率决定是否持有第t1天不能直接用当天的收益率否则等于拿当天的涨跌去预测当天的信号未来函数又回来了。5. 回测与评估避坑为什么回测很漂亮实盘就翻车5.1 坑一前视偏差——用未来数据做特征现象训练集准确率99%测试集准确率99%回测曲线一路向北觉得马上财务自由了。 原因几乎可以肯定数据泄漏。最常见的泄漏有两处——特征里混入了未来信息比如时间窗口用了shift(-1)把明天的数据带到今天或者全局标准化用了测试集的均值/方差。 解决严格按时间顺序构造特征只允许用shift(正数)或rolling()标准化代码写成在训练集上fit在测试集上transform如下所示from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 绝对不能重新fit另外用一个自动检查函数打印特征的最大值和最小值如果测试集中出现训练集从未见过的极端值就要警惕是泄漏还是真实的分布漂移。对于前视偏差最直接的排查方法是把特征列和未来close放在同一张表里人工检查几行。5.2 坑二涨跌不均衡导致准确率虚高现象测试集准确率60%但实际策略年化收益是负的。再看一列测试集中有70%的样本标签是0下跌模型学成永远预测下跌就能拿70%准确率。 原因样本不平衡。尤其在熊市或震荡市下跌天数占比高模型走捷径。 解决不要只盯准确率要看precision、recall、F1更要看策略收益和最大回撤。训练时可以给少数类加权重逻辑回归里加class_weightbalanced随机森林里同样支持。但在金融里预测上涨本身也是低概率事件所以更实际的做法是用过采样只对上涨样本做SMOTE但要谨慎SMOTE会生成插值的合成样本打破时间顺序性毕设里宁可用class_weight不要合成样本。5.3 坑三没有考虑交易成本与滑点现象回测年化收益30%实盘一跑只有8%扣掉手续费和滑点甚至亏钱。 原因回测里按收盘价无缝成交忽略了佣金、印花税、卖出时费用、买卖价差。A股印花税卖出才收佣金双边万2到万3最低5元如果每日调仓摩擦成本非常高。 解决回测时必须模拟交易成本简化的做法是每次买入扣掉0.1%成本、卖出扣掉0.15%成本含印花税佣金滑点。更准确一点用次日开盘价成交而不是当日收盘价成交因为信号在收盘后才能产生当天成交是幻想。fee_buy 0.001 fee_sell 0.0015 daily_ret_after_cost np.where(position_shifted 1, daily_ret - fee_buy, 0) # 卖出日要先计算持仓收益再扣卖出费 sell_days (position_shifted 0) (position_shifted.shift(1) 1)如果毕设里做的是日频信号建议调仓频率控在一周一次以下否则成本会吞噬大量收益。这个结论可以放进论文的交易成本敏感性分析。5.4 坑四参数过拟合到历史区间现象在2015-2020年调出的max_depth8和min_samples_leaf2在2021年测试集上效果暴跌但换个区间调参又变好说明参数只是记住了特定行情的形状。 原因参数寻优时反复在测试集上试测试集的信息通过人的肉眼泄漏进了模型选择这不是模型主动过拟合而是人工调参过拟合。 解决用三层时间切割——训练集2015-2019、验证集2020-2021、测试集2022-2023。只在验证集上调参测试集只跑一次。如果测试集结果不理想也不要再调了直接如实写进论文该模型在样本外失效说明规律不稳定这反而比强行凑一个好结果更显研究素养。终极做法是滚动时间窗用前5年训练预测下一年每一年滚动一次把多年结果汇总这才是量化里常用的Walk-Forward分析。5.5 坑五幸存者偏差现象用今天沪深300的成分股去拉历史数据回测回测表现好得出奇但实盘你根本不知道未来谁会被剔除。 原因沪深300成分股每年调整表现差的股票被换出表现好的留下来。用当前成分股倒推历史相当于自动避开了后来退市和下跌的股票。 解决最简单的方法是用指数本身的历史点位做回测基准或者用过去的成分股列表akshare能取到不同期的成分股但要慢得多了。毕设里至少要做一个敏感性说明把样本换成某时间点截面前500股市值股票观察结果是否稳健。另外买过的股票退市、停牌、涨跌停无法成交回测都要考虑至少把涨停板买入、跌停板卖出的情况设成当天无法成交顺延到下一天。6. 让毕设拿高分的三个进阶技巧如果前面的流程都跑通了这个毕设已经能拿一个不错的分数。但如果想冲优秀下面三个进阶技巧值得加进去。第一个是Walk-Forward滚动验证。不要只在固定训练集和测试集上跑一次改成每一年滚动一次用2015-2019训练预测2020再用2016-2020训练预测2021以此类推。这样能生成多年连续预测策略评估更有说服力。代码思路很简单外面加一个年份循环里面复用之前的训练和预测逻辑关键点是每次都要重新fit标准化器和模型不要偷懒沿用上一次的参数。第二个是加入风险指标。论文里除了贴准确率一定要算夏普比率年化收益/年化波动、最大回撤、卡玛比率、胜率和盈亏比。这些指标是金融领域通用的评价语言导师和答辩评委一眼就能看出你是不是懂行。用百分比收益序列算def sharpe_ratio(returns, rf0.02, periods252): excess returns - rf / periods return np.sqrt(periods) * excess.mean() / (returns.std() 1e-8) def max_drawdown(equity_curve): running_max np.maximum.accumulate(equity_curve) return (equity_curve / running_max - 1).min()第三个技巧是写文档的时候把一个失败实验单独做一节。比如你试过预测原始收盘价、试过直接把所有特征不做标准化丢进LSTM结果很差然后把失败原因分析清楚——非平稳性、噪声过大、窗口信息不足——这恰恰是本课题最难的地方。毕设文档的价值不在我做出来了完美模型而在我理解了问题边界并能用实验证据说明为什么某些做法行不通。这也是我把高分定义为流程完整、评估科学、结论可信而不是准确率90%的原因。最后说句个人的血泪经验做这类题最容易犯的错就是希望在论文里写准确率90%最后为了这个数字去调标签、改阈值、换区间出来的模型拿去实盘一买就套。真实规律是日频涨跌预测准确率能稳定超过55%就已经是优秀的模型了剩下的收益要靠仓位管理和风险控制。把重点放在预测概率的排序能力上——高概率样本的收益是否显著高于低概率样本的收益——比追求绝对准确率更有学术价值也是答辩时最能讲出深度的地方。希望帮到你。本文还有配套的精品资源点击获取