简介:这是一套基于机器学习的股票预测与分析完整项目,面向计算机相关专业毕业生、课程设计学生及需要实战练习的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(symbol="000001", start="2018-01-01", end="2023-12-31", adjust="qfq"): raw = ak.stock_zh_a_hist( symbol=symbol, period="daily", start_date=start, end_date=end, adjust=adjust ) # 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", inplace=True) return raw.reset_index(drop=True) df = load_daily() print(df.shape) print(df.head())adjust="qfq"表示前复权,复权处理会把分红送股造成的价格跳空抹平,避免模型把除权除息当日的大幅下跌误判成趋势反转。period="daily"取日线,数据量对于毕业设计足够;如果你想做分钟级短周期预测,接口返回的数据量和处理复杂度都会明显上升,不建议一开始就碰。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[f"ma{w}"] = df["close"].rolling(w).mean() df[f"std{w}"] = df["close"].rolling(w).std() df[f"ret{w}"] = df["close"].pct_change(w) df[f"vol_w{w}"] = df["volume"].rolling(w).mean() df[f"vol_ratio{w}"] = df["volume"] / df[f"vol_w{w}"] # RSI 简化版:用14日均涨跌幅衡量超买超卖 diff = df["close"].diff() up = diff.clip(lower=0).rolling(14).mean() down = (-diff.clip(upper=0)).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, horizon=5, threshold=0.0): # 预测 T+horizon 日相对 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), horizon=5, threshold=0.0) print(df["label"].value_counts())shift(-horizon)把未来第 N 天的收盘价移动到当前行,和今天收盘价做除法得到未来收益率。threshold=0.0表示“未来 5 天不亏钱就算上涨”,换成 0.02 则要求更高收益才算正样本。阈值越高,正样本越少,模型会偏向预测“不涨”,实际使用时建议从 0 开始,先看类别是否平衡。
有一个概念必须区分清楚:这里的 “未来收益” 只出现在标签里,不进入特征。特征矩阵每一行都只用 T 日及之前的数据,而标签用的是 T+5 日才发生的事,这属于监督学习的标准设定,不是前视偏差。真正要防的是把未来数据误当作特征,比如用 T+5 的成交量去预测 T+5 的涨跌,这是初做股票预测最容易踩的坑。
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_splits=5) 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( objective="binary", n_estimators=500, learning_rate=0.03, num_leaves=31, max_depth=4, 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_metric="auc", callbacks=[lgb.early_stopping(50)] ) auc = roc_auc_score(y_val, model.predict_proba(X_val)[:, 1]) print(f"fold {fold}, val_auc={auc:.4f}")TimeSeriesSplit是这套代码里最关键的一行。普通KFold会把时间顺序打乱,让第 3000 天的样本去训练、第 50 天的样本去验证,模型会偷看到未来信息,验证分数虚高。TimeSeriesSplit保证训练集始终在验证集之前,每一折都模拟“用历史预测未来”的真实场景。
| 参数 | 设定值 | 调参方向 |
|---|---|---|
| learning_rate | 0.03 | 小于 0.01 需要大量树,大于 0.1 容易过拟合 |
| num_leaves | 31 | 过大模型记住噪声,表格数据建议不超过 64 |
| max_depth | 4 | 和 num_leaves 配合限制树复杂度 |
| scale_pos_weight | 负样本数/正样本数 | 类别不平衡时手动加权 |
| early_stopping | 50 轮 | 验证集 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_len=20): 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_len=20) print(X_seq.shape) # (样本数, 20, 特征数)模型定义用 PyTorch,结构保持简单:一层 LSTM 加一个全连接输出。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): def __init__(self, n_features, hidden=64, n_layers=1): super().__init__() self.lstm = nn.LSTM(n_features, hidden, n_layers, batch_first=True) self.head = nn.Linear(hidden, 1) def forward(self, x): out, _ = self.lstm(x) # 取序列最后一个时间步的隐状态 return torch.sigmoid(self.head(out[:, -1, :]))训练时有一条容易被忽略的约束:DataLoader不要开启shuffle=True。
from torch.utils.data import DataLoader, TensorDataset loader = DataLoader( TensorDataset( torch.tensor(X_seq, dtype=torch.float32), torch.tensor(y_seq, dtype=torch.float32).view(-1, 1) ), batch_size=64, shuffle=False # 时间序列按顺序训练 )图像训练里 shuffle 是常规操作,但股票序列一旦打乱,相邻样本的前后依赖关系被破坏,模型学到的是“排列无关”的伪规律。另一个常见问题:LSTM 的验证集也要按时间顺序滑窗切片,不能用随机抽样。
训练循环保持常规写法:
optimizer = torch.optim.Adam(model.parameters(), lr=1e-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_size=1500, step=250): 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_estimators=300, learning_rate=0.05, num_leaves=31, 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_size=1500表示用约 6 年的交易日做训练,step=250表示每年重新训练一次模型并预测下一年。窗口太长会把旧市场的规律反复学进去,太短又让模型没见过足够多行情状态,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(f"strategy_equity={equity.iloc[-1]:.2f}, bench_equity={bench.iloc[-1]:.2f}") print(f"max_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(seed=42): 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_state=42,PyTorch 模型初始化也会变得可复现。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)如果高概率档对应的未来平均收益显著高于低概率档,说明模型输出的是一个有效的排序分数,而不仅仅是分类正确率。答辩演示时,把基准收益曲线和策略收益曲线叠在一张图里先放出来,然后用概率分箱图解释模型在什么区间动摇,评委能快速看到你既做了实验、也看清了模型的边界。预测模型不会永远有效,文档里主动写明回测的时间区间和市场状态,比回避问题更有说服力。
本文还有配套的精品资源,点击获取