
简介这是一套基于机器学习的股票预测与分析完整项目面向计算机相关专业毕业生、课程设计学生及需要实战练习的Python学习者。项目以股票历史行情数据为基础覆盖数据处理、特征构建、模型训练、预测评估与可视化展示等环节包含可直接运行的源码与配套文档说明经严格调试可支撑毕业设计或期末大作业的完整交付。资源包共2000个文件约693MB其中1675张PNG图表便于查看预测结果与走势分析156个CSV文件提供多只股票的历史数据39个PTH为训练好的模型权重另有Python脚本与Markdown说明文档结构清晰、便于二次开发。项目内置多只股票的真实历史行情数据便于横向对比不同标的的预测效果文档说明涵盖环境配置、运行流程与结果解读可帮助读者从零复现并继续扩展自己的交易策略。目前已有611人学习下载适合希望快速搭建金融机器学习项目、提升实战能力并完成高分毕设的读者。1. 把技术指标交给机器学习股票预测才算从玄学变成工程问题把几个技术指标堆进机器学习模型去预测股票涨跌听起来像玄学。可实际做起来你会发现真正让模型翻车的地方不是算法选型而是数据切分、特征构造和回测里的前视偏差。一套基于 Python 的股票预测与分析方案核心工作不是调参而是把“过去的数据”和“未来的收益”严格分开再让模型输出上涨概率。用现成的财经数据接口取行情加工出量价特征用 LightGBM 跑出基线再用 LSTM 验证序列信息最后用滚动回测衡量收益与回撤。这套流程能直接支撑机器学习方向的毕业设计也适合想快速验证一个量化想法、又不想从零造轮子的人。2. 用 Python 搭建股票预测的数据管线与特征集做预测的第一步永远是数据。很多人一上来就写爬虫抓网页结果被反爬、动态渲染和字段变化折腾掉大半时间。常见的做法是用 akshare 这类现成的财经数据接口把 A 股日线数据拉下来先解决“有没有数据”的问题再解决“数据干不干净”的问题。机器学习模型只认结构化表格不认行情软件里的红红绿绿所以最关键的工作是把 K 线转成特征矩阵、把未来收益转成标签。2.1 获取行情用现成接口替代自写爬虫import akshare as ak import pandas as pd def load_daily(symbol000001, start2018-01-01, end2023-12-31, adjustqfq): raw ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart, end_dateend, adjustadjust ) # akshare 返回的是中文列名统一改成英文方便后续处理 raw.columns [ date, open, close, high, low, volume, amount, amplitude, pct_chg, change, turnover ] raw[date] pd.to_datetime(raw[date]) raw.sort_values(date, inplaceTrue) return raw.reset_index(dropTrue) df load_daily() print(df.shape) print(df.head())adjustqfq表示前复权复权处理会把分红送股造成的价格跳空抹平避免模型把除权除息当日的大幅下跌误判成趋势反转。perioddaily取日线数据量对于毕业设计足够如果你想做分钟级短周期预测接口返回的数据量和处理复杂度都会明显上升不建议一开始就碰。symbol填 6 位股票代码start和end建议覆盖 5 年以上太短的数据会让模型在样本外几乎没有稳定性可言。用接口而不是自己写爬虫还有一个好处字段结构固定重复拉取便于缓存。真正做实验时我会先把全量数据存成data/stock.csv后续所有特征工程都从这份本地文件读取避免每跑一次实验就请求一次接口。免费接口普遍有访问频率限制把数据持久化下来既快又稳。2.2 特征工程把 K 线和成交量变成模型认识的字段原始行情只有开高低收量五类信息直接喂给模型很难学到结构。常规做法是把价格和成交量加工成一组技术因子均线反映趋势位置标准差反映波动状态动量反映短周期方向量比反映资金活跃度。这里的思路不是照搬某套“指标买卖信号”而是把这些指标当成特征让模型自己决定它们和未来涨跌之间是什么关系。def make_features(df, windows(5, 10, 20)): df df.copy() for w in windows: df[fma{w}] df[close].rolling(w).mean() df[fstd{w}] df[close].rolling(w).std() df[fret{w}] df[close].pct_change(w) df[fvol_w{w}] df[volume].rolling(w).mean() df[fvol_ratio{w}] df[volume] / df[fvol_w{w}] # RSI 简化版用14日均涨跌幅衡量超买超卖 diff df[close].diff() up diff.clip(lower0).rolling(14).mean() down (-diff.clip(upper0)).rolling(14).mean() df[rsi14] 100 - 100 / (1 up / (down 1e-9)) return dfrolling(w).mean()计算最近w天的均值pct_change(w)计算窗口两端的价格变化率这两类操作是量价特征的基础。vol_ratio是当日成交量除以近期均量数值大于 1 说明放量小于 1 说明缩量。用相对量比而不是绝对量是因为不同股票的股本规模不同绝对成交量没有可比性。滚动计算会产生 NaN前 20 行没有完整窗口训练前需要丢弃。下表是这套特征里最有代表性的几类字段模型侧含义比单看指标名称更值得关注。特征类典型字段模型侧含义均线类ma5 / ma20短期趋势位置均线多头或空头排列被数字化波动类std5 / std20价格波动率分段等价于 ATR 的思路动量类ret5 / ret20短周期涨跌方向反映追涨或超跌量能类vol_ratio5 / vol_ratio10放量缩量状态配合价格变化识别资金行为震荡类rsi14超买超卖区间对极端行情有提示作用2.3 生成标签用未来 N 日收益定义上涨特征描述“过去”标签描述“未来”这是监督学习的核心设定。股票预测通常不直接回归未来的具体价格因为价格序列噪声太大改为预测“未来 5 个交易日是否上涨”二分类问题模型更容易收敛评估也更直观。def make_label(df, horizon5, threshold0.0): # 预测 Thorizon 日相对 T 日收盘价的涨跌方向 future_close df[close].shift(-horizon) df[future_ret] future_close / df[close] - 1 df[label] (df[future_ret] threshold).astype(int) # 样本尾部拿不到未来价格直接删除防止模型看到空标签 df df.iloc[:-horizon] if horizon 0 else df return df df make_label(make_features(df), horizon5, threshold0.0) print(df[label].value_counts())shift(-horizon)把未来第 N 天的收盘价移动到当前行和今天收盘价做除法得到未来收益率。threshold0.0表示“未来 5 天不亏钱就算上涨”换成 0.02 则要求更高收益才算正样本。阈值越高正样本越少模型会偏向预测“不涨”实际使用时建议从 0 开始先看类别是否平衡。有一个概念必须区分清楚这里的 “未来收益” 只出现在标签里不进入特征。特征矩阵每一行都只用 T 日及之前的数据而标签用的是 T5 日才发生的事这属于监督学习的标准设定不是前视偏差。真正要防的是把未来数据误当作特征比如用 T5 的成交量去预测 T5 的涨跌这是初做股票预测最容易踩的坑。3. 机器学习预测模型梯度提升树与 LSTM 的取舍模型选型不需要一开始就追求复杂。表格类特征的股票预测梯度提升树往往是性价比最高的机器学习算法LightGBM 训练快、对小样本鲁棒、特征数值范围不敏感几秒就能出一个可信基线。LSTM 的优势是能建模序列依赖但需要先标准化特征、构造滑窗然后处理训练时间和显存问题。我的做法是先跑通 LightGBM把它当作正确性基准再决定要不要用 LSTM 去替换。3.1 先跑 LightGBM 基线再判断序列模型收益import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit from sklearn.metrics import roc_auc_score features [c for c in df.columns if c not in [date, label, future_ret]] X df[features] y df[label] # 时间序列必须用顺序切分不能用随机 KFold tscv TimeSeriesSplit(n_splits5) for fold, (train_idx, val_idx) in enumerate(tscv.split(df)): 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 lgb.LGBMClassifier( objectivebinary, n_estimators500, learning_rate0.03, num_leaves31, max_depth4, scale_pos_weight(y_train 0).sum() / (y_train 1).sum(), verbose-1 ) model.fit( X_train, y_train, eval_set[(X_val, y_val)], eval_metricauc, callbacks[lgb.early_stopping(50)] ) auc roc_auc_score(y_val, model.predict_proba(X_val)[:, 1]) print(ffold {fold}, val_auc{auc:.4f})TimeSeriesSplit是这套代码里最关键的一行。普通KFold会把时间顺序打乱让第 3000 天的样本去训练、第 50 天的样本去验证模型会偷看到未来信息验证分数虚高。TimeSeriesSplit保证训练集始终在验证集之前每一折都模拟“用历史预测未来”的真实场景。参数设定值调参方向learning_rate0.03小于 0.01 需要大量树大于 0.1 容易过拟合num_leaves31过大模型记住噪声表格数据建议不超过 64max_depth4和 num_leaves 配合限制树复杂度scale_pos_weight负样本数/正样本数类别不平衡时手动加权early_stopping50 轮验证集 AUC 连续 50 轮不涨就停3.2 LSTM 的序列构造与 shuffle 细节LSTM 输入是三维张量形状为(样本数, 时间步数, 特征数)。时间步数对应过去多少个交易日常见取 20每个样本拿到最近 20 天的特征去预测第 21 天开始的未来 5 日方向。和树模型相比LSTM 对特征尺度敏感必须先做标准化而且标准化只能用训练集的均值和方差不能用全集。from sklearn.preprocessing import StandardScaler scaler StandardScaler() # fit 只用训练集transform 再套用到验证集防止未来统计量泄露 X_train_scaled scaler.fit_transform(X_train[features]) X_val_scaled scaler.transform(X_val[features])序列窗口构造函数如下import numpy as np def to_sequences(X, y, seq_len20): xs, ys [], [] for i in range(seq_len, len(X)): xs.append(X[i - seq_len:i]) ys.append(y[i]) return np.array(xs), np.array(ys) X_seq, y_seq to_sequences(X_train_scaled, y_train.values, seq_len20) print(X_seq.shape) # (样本数, 20, 特征数)模型定义用 PyTorch结构保持简单一层 LSTM 加一个全连接输出。import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features, hidden64, n_layers1): super().__init__() self.lstm nn.LSTM(n_features, hidden, n_layers, batch_firstTrue) self.head nn.Linear(hidden, 1) def forward(self, x): out, _ self.lstm(x) # 取序列最后一个时间步的隐状态 return torch.sigmoid(self.head(out[:, -1, :]))训练时有一条容易被忽略的约束DataLoader不要开启shuffleTrue。from torch.utils.data import DataLoader, TensorDataset loader DataLoader( TensorDataset( torch.tensor(X_seq, dtypetorch.float32), torch.tensor(y_seq, dtypetorch.float32).view(-1, 1) ), batch_size64, shuffleFalse # 时间序列按顺序训练 )图像训练里 shuffle 是常规操作但股票序列一旦打乱相邻样本的前后依赖关系被破坏模型学到的是“排列无关”的伪规律。另一个常见问题LSTM 的验证集也要按时间顺序滑窗切片不能用随机抽样。训练循环保持常规写法optimizer torch.optim.Adam(model.parameters(), lr1e-3) loss_fn nn.BCELoss() for epoch in range(30): for xb, yb in loader: optimizer.zero_grad() loss loss_fn(model(xb), yb) loss.backward() optimizer.step()如果训练损失下降、验证 AUC 不涨优先把hidden从 64 降到 32或者把seq_len从 20 降到 10。时间步越长需要的数据量越大股票日线数据本身只有一两千行深层 LSTM 很容易过拟合。4. 回测与结果评估评价预测不能只看准确率很多模型在验证集上 AUC 不错放进真实行情里却一塌糊涂原因通常不在模型而在评估方式。准确率看的是“猜对了多少天”但股票交易关心的是“这些预测能不能转化为稳定的收益”。正确的做法是把模型放进滚动回测让它在每个时间点只用当时已知的数据做预测再模拟持仓和收益。4.1 walk-forward 滑窗回测walk-forward 的核心是“训练一段、预测一段、窗口向前滚动”。每次预测时模型只看得到预测日之前的信息这是比 TimeSeriesSplit 更贴近真实交易的评估方式。def walk_forward(df, features, train_size1500, step250): all_pred [] start train_size while start len(df): train df.iloc[start - train_size:start] test df.iloc[start:start step] if len(test) 30: break model lgb.LGBMClassifier( n_estimators300, learning_rate0.05, num_leaves31, verbose-1 ) model.fit(train[features], train[label]) test test.copy() test[pred_prob] model.predict_proba(test[features])[:, 1] all_pred.append(test) start step return pd.concat(all_pred).sort_values(date) result walk_forward(df, features) print(result[[date, pred_prob, label]].tail())train_size1500表示用约 6 年的交易日做训练step250表示每年重新训练一次模型并预测下一年。窗口太长会把旧市场的规律反复学进去太短又让模型没见过足够多行情状态1500/250 是一组适合日线数据的起点参数。预测阶段没有早停因为每次只训练一次固定n_estimators更简单可控。4.2 模拟收益曲线与最大回撤拿到预测概率后还需要一步关键处理信号滞后。当日收盘才算出概率最早只能次日开盘执行所以收益计算必须把持仓信号向后移一天否则会直接用当天收益计算策略收益产生前视偏差。COST_RATE 0.001 # 交易成本占单边成交金额比例按你自己的券商费率调整 result[signal] (result[pred_prob] 0.6).astype(int) result[position] result[signal].shift(1).fillna(0) # 只有仓位变化时才计算交易成本 result[strategy_ret] ( result[position] * result[pct_chg] / 100 - result[position].diff().abs().fillna(0) * COST_RATE ) result[bench_ret] result[pct_chg] / 100 equity (1 result[strategy_ret]).cumprod() bench (1 result[bench_ret]).cumprod() drawdown (equity - equity.cummax()) / equity.cummax() print(fstrategy_equity{equity.iloc[-1]:.2f}, bench_equity{bench.iloc[-1]:.2f}) print(fmax_drawdown{drawdown.min():.2%})position.diff().abs()在仓位从 0 变 1 或从 1 变 0 时取值为 1乘以费率就得到调仓成本。预测概率阈值为 0.6意味着只有模型比较有把握时才持仓比 0.5 更保守也更容易避开震荡行情里的反复交易。指标层面建议同时看下面四项而不是只盯着准确率指标计算方式怎么看年化收益(equity[-1] ** (252/len(result)) - 1)和基准收益对比超额是否稳定最大回撤(equity - equity.cummax()).min()极端行情下最惨亏多少夏普比率strategy_ret.mean() / strategy_ret.std() * sqrt(252)每承担一单位风险能换多少收益预测胜率持仓期间正收益天数 / 持仓天数胜率低于 50% 也可能赚钱关键是盈亏比回测里还要过滤停牌和没有成交的日子。停牌日pct_chg为 0模型预测概率没有意义持仓却可能被重复计费。常见做法是在特征工程之前把volume 0的行剔除或者在策略收益计算里把停牌日position置为 0。5. 毕业设计交付固定随机种子、实验记录与答辩演示顺序源码和文档说明书是毕业设计的两条腿代码能复现、文档能讲清结论才站得住。这套项目里最容易让答辩翻车的不是模型效果差而是随机种子不固定导致两次运行结果对不上或者文档里说不清训练集、验证集、测试集分别是什么时间范围。5.1 固定随机种子与缓存特征import os import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) os.environ[PYTHONHASHSEED] str(seed)训练前调用set_seed(42)LightGBM 里再显式传random_state42PyTorch 模型初始化也会变得可复现。PYTHONHASHSEED需要在解释器启动前设置才完全生效但写在函数里能覆盖多数日常实验场景。特征工程的结果建议缓存成 Parquet 文件特征计算和模型训练分离每次调参不必重新跑一边所有指标。文档说明里写明“数据来源、特征字段、标签定义、训练/验证/测试时间区间、模型超参数、AUC 和回测指标”比冗长的接口文档有用得多。5.2 用概率分箱图验证模型排序能力回测收益曲线只能说明策略整体表现评委更关心模型是不是真的学到了规律。一个直观的验证方法把预测概率分成几档看每档对应的未来平均收益。# result 里保留 future_ret 列用于评估模型排序能力 result[prob_bin] pd.cut( result[pred_prob], bins[0, 0.4, 0.6, 0.8, 1.0], labels[0-0.4, 0.4-0.6, 0.6-0.8, 0.8-1.0] ) bin_stats result.groupby(prob_bin)[future_ret].mean() print(bin_stats)如果高概率档对应的未来平均收益显著高于低概率档说明模型输出的是一个有效的排序分数而不仅仅是分类正确率。答辩演示时把基准收益曲线和策略收益曲线叠在一张图里先放出来然后用概率分箱图解释模型在什么区间动摇评委能快速看到你既做了实验、也看清了模型的边界。预测模型不会永远有效文档里主动写明回测的时间区间和市场状态比回避问题更有说服力。本文还有配套的精品资源点击获取