简介:面向计算机专业毕业设计或课程作业的股票智能预测系统完整项目包,聚焦人工智能在金融场景的落地,适合需要快速搭建预测系统、理解机器学习与深度学习全流程的学生或初级开发者。压缩包内含2000个文件,以HTML页面、JavaScript脚本、图片、CSS样式为主,并包含Python、Java源码与SQLite数据库等,覆盖前端展示、后端逻辑、模型训练和数据存储等模块;整体约23.82MB,目录结构清晰,便于按功能区块检索。已有187人学习下载。通过阅读源码及配置文件,可掌握LSTM等时序模型在股价预测中的实际应用,理解数据采集、预处理、训练、预测与结果展示的完整实现思路;其中还涉及UEditor处理脚本、Web配置及多种格式附件,为后续功能扩展和二次开发提供参考。
1. 股票智能预测系统的真实边界:它不是印钞机,是一套能跑通的时序预测工程
每年到了毕设季,我总能看到同一个问题:「股票智能预测系统到底怎么做才不会被答辩老师挑刺?」这个 zip 标题代表的,不是一份能让你躺着赚钱的代码,而是一条完整工程链路:从行情数据获取、特征构造、模型训练,到回测评估与可视化。它最适合两类人:一类是赶毕设或课程作业的学生,需要把「预测模型怎么落地」讲清楚;另一类是准备转量化方向的工程师,拿它当练手骨架。这套系统能解决的关键问题,是让你在答辩或面试时拿出可复现的数据、可解释的特征、可量化的回测结果,而不是只交一个谁也说不清的黑匣子。下面按我实际做这类项目的流程,把每一步的关键决策、参数设置和踩坑点拆开讲。
2. 数据模块先行:行情获取、清洗与前复权处理的三个必调参数
预测系统的地基是数据。课程作业里最常见的翻车场景是模型代码写得很完整,换一个数据源、换一只股票、换一段行情之后就全崩。这一章把数据链路讲透,你做完之后,换股票、换周期、换时间段都能顺手接上。
2.1 数据源选型:为什么默认用 A 股日线行情而不是分钟线
数据源我一般分三类:akshare 的免费日线接口、tushare 需要积分的历史行情、本地手动下载的 CSV。对学生项目来说,akshare 是最稳的起点,不需要注册 token、不用配环境变量,pip install 之后就能用;tushare 虽然字段更规整,但很多历史接口需要积分门槛,入门阶段容易卡在权限上。CSV 适合做数据备份,不适合作为主数据源,因为每天手动更新一次,回测就没法复现。
选日线而不是分钟线,理由很实际。分钟级数据单只股票一年就有几千条,十只股票组合起来就是几万条,预处理和训练时间会翻好几倍;更重要的是分钟线的微观噪声非常大,开盘半小时和尾盘半小时的价格行为跟盘中完全不同,课程作业阶段很难把这些规则讲清楚。日线数据一天一根 K 线,信息密度适中,LSTM 能学到周级别、月级别的趋势,答辩时也更方便解释特征和预测结果。数据区间我一般取 2015 年 1 月 1 日到现在,这个起点包含了 A 股 2015 年牛熊切换、2016 年熔断、2018 年阴跌和 2019 年后的结构行情,覆盖多种市场状态,模型不会只在单边上涨里自嗨。
2.2 用 akshare 批量拉取行情:最小可用的缓存式取数脚本
下面这段代码是一个最小可用的取数函数,它把 akshare 返回的中文字段名统一转成英文,并落成 Parquet 格式缓存。Parquet 比 CSV 快得多,回测阶段反复读取时能省下大量时间。
import akshare as ak import pandas as pd def fetch_daily(symbol="000001", start="20150101", end="20240101"): df = ak.stock_zh_a_hist( symbol=symbol, period="daily", start_date=start, end_date=end, adjust="qfq", # 前复权,避免除权跳空造成假信号 ) df.rename(columns={ "日期": "date", "开盘": "open", "收盘": "close", "最高": "high", "最低": "low", "成交量": "volume" }, inplace=True) df["date"] = pd.to_datetime(df["date"]) df = df.set_index("date").sort_index() return df[["open", "high", "low", "close", "volume"]]这段代码的逻辑是先用 ak.stock_zh_a_hist 拉取单只 A 股的历史行情,再把中文字段名映射成英文,最后按时间索引排序。注意排序这一步不能省,akshare 返回的数据默认按日期正序,但一旦你多线程拉取或者做增量更新,顺序就有可能是乱的,模型训练前的数据必须严格按时间升序排列。
def cache_all(symbols, start="20150101", end="20240101", out_dir="data/"): for code in symbols: try: df = fetch_daily(code, start, end) df.to_parquet(f"{out_dir}{code}.parquet") except Exception as exc: print(f"{code} 拉取失败: {exc}")参数里最值得关注的是三个地方。adjust="qfq" 表示前复权,意思是把历史价格按最新股本和分红情况重新计算,避免除权日突然出现一个 -10% 的假阴线;period="daily" 对应日线,如果改成 "60" 就是 60 分钟线,但入门阶段我不建议动;start 定在 20150101,是为了覆盖足够长的市场周期。缓存目录 out_dir 按股票代码命名,回测时按需读取,不用每次重新拉一次接口。
2.3 清洗与对齐:停牌、缺失值、重复索引三类脏数据怎么处理
真实行情数据拿回来之后,至少有三类脏数据要处理。第一是停牌导致的缺失,停牌日可能没有成交、没有收盘价;第二是接口偶发返回重复行,通常是同一交易日出现两条记录;第三是不同股票的交易日历不一致,有的股票中途退市或长期停牌,直接横向拼接会错位。
def clean_daily(df): df = df.dropna(subset=["close"]) df = df[~df.index.duplicated(keep="last")] df = df.sort_index() df = df.ffill() # 极少量 NaN 用前值填充 return df我的处理习惯是:先删掉没有收盘价的停牌行,再用 keep="last" 去掉重复索引,最后对剩余字段做 ffill。这里要提醒一点,ffill 看起来像「偷懒」,其实这是时序数据里合法的处理方式,模型需要知道「上一交易日收盘价是多少」;真正不能做的,是用全局均值去填充空缺。对齐交易日时,我会先取全市场交易日的并集,再对每只股票做 reindex,缺的列补 NaN,这一步放到特征工程之前统一处理。数据质量检查也有一个简单办法:算出每天的收益率,如果某天收益率绝对值大于 20%,基本可以断定是复权字段出了问题或数据源返回了脏行,应该回头检查原始接口。
3. 特征工程与模型选型:从技术指标到 LSTM 的序列建模
数据准备干净之后,下一步是让模型看到「能学的东西」。很多人觉得特征越多越好,实际做下来,特征数量跟模型效果根本不是线性关系。这一章先讲怎么构造特征,再给出一份可以直接跑的 PyTorch LSTM 实现,最后说明训练时的切分策略。
3.1 特征构造:先做收益率和滚动窗口,技术指标当补充
特征工程的第一原则是「不要碰未来数据」。最常见的错误是在算 MACD 或均线时用了当天的收盘价,又同时把当天收盘价当作标签,结果模型在训练时看到的输入里已经包含了答案。安全做法是先构造标签,再构造特征,最后统一 dropna,确保每一条样本的输入只用到 T 日及之前的信息。
def make_features(df, lookback=60): df = df.copy() df["ret"] = df["close"].pct_change() # 当日收益率 df["label"] = (df["close"].shift(-1) > df["close"]).astype(int) # 明日是否上涨 for col in ["close", "volume", "ret"]: for w in [5, 10, 20]: df[f"{col}_ma{w}"] = df[col].rolling(w).mean() # 滚动均值窗口 df = df.dropna() return df这段代码里 label 用的是 close.shift(-1),意思是拿「明天的收盘价」和「今天的收盘价」比较,作为预测目标。构造完 label 后再加滚动窗口特征,此时 dropna 会顺带把开头没有足够历史数据的行删掉。为什么窗口选 5、10、20?对应一周、两周、一个月的交易日数量,这是用日线做短中期预测时比较自然的周期;你也可以加 60 日的窗口,但不要一口气堆三十个特征,否则训练慢不说,还容易过拟合。
技术指标不是必须的。如果想让答辩更有内容,可以加 RSI 或 MACD 的衍生特征,但一定要用现成的 ta 库或自己按公式严格错位计算。我见过不少项目把 MACD 的 DIF 值算出来后才发现用了当天收盘价,这类 bug 在答辩时被老师一问就会露馅。特征优先级我用一个简单排序:原始价量、收益率、滚动均值、波动率,技术指标放最后。
3.2 搭建 LSTM 预测模块:PyTorch 实现与超参说明
LSTM 是这类项目最稳妥的模型选择之一。Transformer 在长序列上更强,但数据量不够时反而容易欠拟合;LightGBM 也能做时序预测,但它天生不是序列模型,需要手动构造大量滞后特征。对毕设和课程作业,LSTM 性价比最高,既能讲清楚时序建模的思想,又不需要超大算力。
import torch import torch.nn as nn class StockLSTM(nn.Module): def __init__(self, n_features, hidden_size=128, num_layers=2, dropout=0.2): super().__init__() self.lstm = nn.LSTM( input_size=n_features, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout ) self.head = nn.Sequential( nn.Linear(hidden_size, 64), nn.ReLU(), nn.Linear(64, 1) ) def forward(self, x): out, _ = self.lstm(x) return self.head(out[:, -1, :]) # 取最后一个时间步的输出forward 里的关键点是 out[:, -1, :],它表示只取序列最后一个时间步的隐状态,再送到全连接层输出一个 logit。batch_first=True 让输入张量形状是 (batch_size, lookback, n_features),这样写数据加载器的时候不容易搞错维度。hidden_size=128 在日线数据上已经够用,增加到 256 收益不大,反而让训练时间翻倍;num_layers=2 是多数时序任务里比较稳的选择,1 层表达力不足,3 层以上对几千条样本的数据集很容易过拟合。dropout=0.2 放在 LSTM 层之间,能在训练时随机丢弃部分信息,减弱对训练集的死记硬背。
3.3 训练策略:时间序切分、早停与模型保存
股票数据不能随机切分,这是一个容易被忽视的隐患。随机 shuffle 会让训练集里混入未来样本,模型在验证集上表现奇好,实盘里完全失效。正确的做法是按时间顺序切分,比如前 70% 训练、后 30% 验证,或者用 TimeSeriesSplit 做多折滚动验证。
def train_model(model, train_loader, val_loader, epochs=30, lr=1e-3): optimizer = torch.optim.Adam(model.parameters(), lr=lr) loss_fn = nn.BCEWithLogitsLoss() best_loss = float("inf") for epoch in range(epochs): model.train() for x, y in train_loader: optimizer.zero_grad() loss = loss_fn(model(x).squeeze(-1), y.float()) loss.backward() optimizer.step() model.eval() val_loss = 0.0 with torch.no_grad(): for x, y in val_loader: pred = model(x).squeeze(-1) val_loss += loss_fn(pred, y.float()).item() val_loss /= len(val_loader) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), "best_model.pt") if epoch > 10 and val_loss > best_loss: break return model这里用 BCEWithLogitsLoss,是因为我们把任务定义为「明日是否上涨」的二分类,模型输出的是 logit,而不是直接预测价格。预测价格看起来更直观,但价格序列不稳定,MSE 损失会把模型带偏到「预测明天和今天差不多」的状态——后面章节会专门讲这个坑。早停的判断是连续多个 epoch 验证损失不再下降就终止,我先写了一个简单的 if 版本,真正用的时候建议维护一个 patience 计数器,比如连续 5 个 epoch 不下降再停。参数方面,lr=1e-3 是 Adam 的常见起点,如果训练不稳定,降到 3e-4;epochs=30 对日线级别几千条样本足够,配合早停一般 15 轮左右就能收敛。
4. 回测与评估:用方向准确率、最大回撤验证这套系统的真实水平
模型训练完只是第一步。答辩时老师最常问的一句话是:「你这个预测模型,拿去回测到底赚不赚钱?」所以回测框架和评估指标,是整个 project 里最容易被看见的部分。这一章先给最小回测实现,再讲指标怎么解读,最后给 walk-forward 的正确写法。
4.1 回测框架的最小实现:逐日迭代、按信号持仓
回测的核心是模拟「每天看一眼模型预测,决定买还是卖」的过程。不要用一次性预测整段行情再计算收益,因为那样默认你提前知道了所有未来信息。
def run_backtest(df, pred_prob, fee=0.001, threshold=0.55): cash = 1.0 shares = 0.0 positions = [] for i in range(len(df)): price = df["close"].iloc[i] if pred_prob.iloc[i] >= threshold: if shares == 0: shares = cash / price * (1 - fee) # 全仓买入并扣手续费 cash = 0.0 else: if shares > 0: cash = shares * price * (1 - fee) # 清仓并扣手续费 shares = 0.0 positions.append(cash + shares * price) return pd.Series(positions, index=df.index)代码逻辑是:当预测概率超过 threshold 时全仓买入,低于 threshold 时空仓,每次交易都扣千分之一手续费。threshold=0.55 的意思是模型对「明日上涨」有 55% 以上把握才动手,目的是过滤掉模型没信心的预测,减少无意义交易。fee=0.001 是 A 股双边手续费的近似值,实际费率可能更低,但回测里按 0.1% 留出余量是安全做法。真实项目里 threshold 应该通过验证集搜索,别用固定值。
4.2 评估指标:方向准确率比 MSE 更能说明问题
很多课程作业只贴一张 loss 下降曲线,这在答辩里是远远不够的。loss 是训练过程的指标,回测结果需要一套可解释的绩效指标。
| 指标 | 计算方式 | 合理参考 |
|---|---|---|
| 方向准确率 | 预测上涨与实际上涨的样本占比 | 日线任务 52% 以上算可用 |
| 年化收益率 | 回测期末净值按年化 | 跑赢同期沪深300 或中证500 |
| 最大回撤 | 净值曲线峰顶到谷底的最大跌幅 | 控制在 15% 以内 |
| 夏普比率 | 超额收益 / 波动率 | 大于 1 算健康 |
| 交易次数 | 买卖信号总次数 | 日线策略建议 40 次以上再谈统计意义 |
方向准确率是最直观的「模型有没有用」的指标,52% 听起来不高,但在日线级别已经很不容易,因为明天的涨跌本来接近随机。最大回撤比年化收益更重要,一个年化 40% 但回撤 30% 的策略,实战体验接近爆仓。夏普比率用来衡量风险调整后的收益,计算时可以用每日净值收益率的标准差做分母。
4.3 滚动训练与 walk-forward 验证:避免数据泄露的正确回测写法
静态切分只能验证一次,而滚动训练能在多段时间上验证模型的稳定性。做法是每次只用过去 N 天训练,预测未来 M 天,然后窗口向前滚动。
def walk_forward(df, features, horizon=30, train_size=250): preds = [] for start in range(0, len(df) - train_size - horizon, horizon): train = df.iloc[start:start + train_size] test = df.iloc[start + train_size:start + train_size + horizon] model = build_model() train_model(model, train) pred = predict(model, test) preds.append(pred) return pd.concat(preds)walk-forward 的关键是每次训练只用 start 到 start + train_size 这段过去数据,预测其后的 horizon 天。train_size=250 大约是一个交易年的样本量,horizon=30 是一个月的预测期。这种写法能让答辩老师看到你理解了「时间顺序」这一核心概念,也方便统计多次预测的累计收益。唯一要注意的是窗口大小不要设太极端:train_size 太小,模型学不到趋势;horizon 太长,预测误差会快速累积。
5. 避坑与常见问题:股票预测项目最容易翻车的 5 个血泪点
这一章不写理论,直接列我在帮人审项目、自己调模型时反复遇到的五类问题。每一条都是按「现象 → 原因 → 解决」的路径来说明。
5.1 前视偏差:回测曲线很漂亮,实盘一跑就废
现象是训练好的模型在回测里年化收益率高达 80%,看起来稳赚不赔,但一放到实盘模拟盘就跑不出那个曲线。原因几乎可以锁定是前视偏差:特征构造或标签构造时用了当天收盘后才知道的数据,比如用当天收盘价计算的均线去预测当天是否上涨,等于把答案泄露给了模型。
解决方法是严格区分「T 日收盘后能拿到什么」和「T+1 日开盘才能确认什么」。最简单的一条合规准则是:所有特征在 T 日计算完毕,所有标签指向 T+1 日及之后的结果。我在 make_features 里已经用 shift(-1) 做了标签偏移,但如果你自己往特征里加了 MACD、布林带等指标,一定要再手算一遍指标的最后一个值是否引用了未来数据。
5.2 随机切分训练集和测试集:验证集指标虚高
现象是验证集上的方向准确率超过 65%,远高于正常水平。原因大概率是你用了 sklearn 的 train_test_split,默认 shuffle=True,导致训练样本和验证样本在时间上互相穿插。股票的某一天行情会被同时分到训练集和验证集,模型等于提前见过答案。
解决方法是改成按时间顺序切分,比如:
train_size = int(len(df) * 0.7) train, test = df.iloc[:train_size], df.iloc[train_size:]如果要用交叉验证,就选 TimeSeriesSplit,它会保证每次训练集都严格位于验证集之前。答辩时这个点最容易被老师一眼看穿,因为如果你用了随机切分,打印出训练集和测试集的时间范围就会发现重叠。
5.3 未复权数据:除权除息被模型当成暴跌
现象是模型在某只股票除权日给出强烈的卖出信号,但实际这天股价并没有真正下跌,只是除权导致的价格跳空。原因是从某些免费数据源拿到的价格是原始价(不复权),除权日会有一个人为的价格断层,比如 50 块的股票 10 送 10,第二天变成 25 块,看起来像跌了 50%。
解决方案是统一使用前复权数据,并且在拉数时就把 adjust="qfq" 写死。要注意的是,前复权数据在不同时间点拉取,历史价格会因为后续分红而变化,所以正式训练前要把数据缓存固定住,回测和训练必须用同一份缓存,否则重新拉一次数,特征和标签都会轻微变化,结果很难复现。
5.4 过拟合:训练集准确率高,验证集一塌糊涂
现象是训练集上的 loss 降到很低,方向准确率超过 70%,但验证集只有 50%,接近瞎猜。原因一般是模型参数量相对于数据量太大,或者训练轮数太多,模型把历史行情里的噪声当成规律记住了。3000 条日线数据撑不起一个三层 512 维的 LSTM。
解决办法是先做减法:hidden_size 降到 64 或 128,num_layers 保持 2,去掉多余的技术指标特征。然后加早停,patience 设为 5 到 8,验证 loss 连续多轮不降就停止。还可以在数据增强上做文章,用多只股票一起训练模型,让模型学习共性的价格行为规律,而不是单只股票的路径依赖。
5.5 预测价格还是预测涨跌:损失函数选错导致模型退化
现象是模型预测的明天收盘价总是非常接近今天的收盘价,误差看着不大,但完全没有交易价值。原因是用 MSE 回归去做价格预测时,最优解在数学上就是「预测历史均值」,模型学到最后相当于在复读昨日价格,这在本质上失去了预测意义。
更稳的做法是转成二分类,预测「明日是否上涨」,用 BCEWithLogitsLoss。如果项目方向要求必须输出价格,可以改成预测未来 5 日的累计收益率,再做秩排序,这种任务不会退化成复读机。我的个人习惯是:面向答辩和课程作业,用二分类加方向准确率来解释模型价值,远比扣 MSE 数值更有说服力。
6. 把预测系统封装成能存档的交付物:目录规范、验证脚本和演示技巧
做到这里,模型已经能跑通,回测也能出结果,但很多人最后输在交付物太乱。一个 zip 包打开来全是 Untitled.ipynb,答辩老师连跑通的信心都没有。一个成熟的课程作业交付物,建议按下面的目录组织。
| 目录/文件 | 作用 |
|---|---|
| data/ | 行情缓存数据,包含取数脚本生成的 Parquet 文件 |
| features/ | 特征工程代码与特征配置说明 |
| models/ | 模型定义、训练脚本、最佳权重 best_model.pt |
| backtest/ | 回测框架、评估指标计算代码 |
| app/ | 可视化或简单 Web 演示脚本 |
| requirements.txt | 环境依赖清单 |
| README.md | 运行步骤、参数说明、结果复现方法 |
我一般会在项目根目录放一个 run_all.sh,按顺序执行取数、特征生成、训练、回测,这样任何人拿到 zip 都能一键复现结果。
#!/bin/bash python 1_fetch_data.py python 2_make_features.py python 3_train_model.py python 4_run_backtest.py python 5_summary_report.py答辩演示时,不要从头敲代码,而是直接跑这个脚本,边跑边指着屏幕说:这是数据缓存,这是特征窗口,这是验证集的方向准确率,这是回测最大回撤。演示之前先在干净的虚拟环境里完整跑三遍,确保没有因为路径、依赖版本或缓存问题中途报错。给老师展示的结论建议控制在三个:方向准确率、回测净值曲线、最大回撤。
我最早做这个题目时,觉得训练模型是最难的部分,后来发现最难的是让结果可复现、让流程可解释。每次提交前我会强制自己问一个问题:如果评委明天就要这份代码,能不能在一个新环境里十分钟跑出同样的图表?这样想之后,数据缓存、依赖锁定、脚本顺序这些小事反而变成了整个项目里最值钱的部分。这套思路同样适用于以后的工作交接,希望帮到你。
本文还有配套的精品资源,点击获取