股票智能预测系统完整实战:数据清洗到LSTM回测

📅 发布时间:2026/10/12 23:38:51
股票智能预测系统完整实战:数据清洗到LSTM回测
简介面向计算机专业毕业设计或课程作业的股票智能预测系统完整项目包聚焦人工智能在金融场景的落地适合需要快速搭建预测系统、理解机器学习与深度学习全流程的学生或初级开发者。压缩包内含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(symbol000001, start20150101, end20240101): df ak.stock_zh_a_hist( symbolsymbol, perioddaily, start_datestart, end_dateend, adjustqfq, # 前复权避免除权跳空造成假信号 ) df.rename(columns{ 日期: date, 开盘: open, 收盘: close, 最高: high, 最低: low, 成交量: volume }, inplaceTrue) 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, start20150101, end20240101, out_dirdata/): 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})参数里最值得关注的是三个地方。adjustqfq 表示前复权意思是把历史价格按最新股本和分红情况重新计算避免除权日突然出现一个 -10% 的假阴线perioddaily 对应日线如果改成 60 就是 60 分钟线但入门阶段我不建议动start 定在 20150101是为了覆盖足够长的市场周期。缓存目录 out_dir 按股票代码命名回测时按需读取不用每次重新拉一次接口。2.3 清洗与对齐停牌、缺失值、重复索引三类脏数据怎么处理真实行情数据拿回来之后至少有三类脏数据要处理。第一是停牌导致的缺失停牌日可能没有成交、没有收盘价第二是接口偶发返回重复行通常是同一交易日出现两条记录第三是不同股票的交易日历不一致有的股票中途退市或长期停牌直接横向拼接会错位。def clean_daily(df): df df.dropna(subset[close]) df df[~df.index.duplicated(keeplast)] df df.sort_index() df df.ffill() # 极少量 NaN 用前值填充 return df我的处理习惯是先删掉没有收盘价的停牌行再用 keeplast 去掉重复索引最后对剩余字段做 ffill。这里要提醒一点ffill 看起来像「偷懒」其实这是时序数据里合法的处理方式模型需要知道「上一交易日收盘价是多少」真正不能做的是用全局均值去填充空缺。对齐交易日时我会先取全市场交易日的并集再对每只股票做 reindex缺的列补 NaN这一步放到特征工程之前统一处理。数据质量检查也有一个简单办法算出每天的收益率如果某天收益率绝对值大于 20%基本可以断定是复权字段出了问题或数据源返回了脏行应该回头检查原始接口。3. 特征工程与模型选型从技术指标到 LSTM 的序列建模数据准备干净之后下一步是让模型看到「能学的东西」。很多人觉得特征越多越好实际做下来特征数量跟模型效果根本不是线性关系。这一章先讲怎么构造特征再给出一份可以直接跑的 PyTorch LSTM 实现最后说明训练时的切分策略。3.1 特征构造先做收益率和滚动窗口技术指标当补充特征工程的第一原则是「不要碰未来数据」。最常见的错误是在算 MACD 或均线时用了当天的收盘价又同时把当天收盘价当作标签结果模型在训练时看到的输入里已经包含了答案。安全做法是先构造标签再构造特征最后统一 dropna确保每一条样本的输入只用到 T 日及之前的信息。def make_features(df, lookback60): 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_size128, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout ) 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_firstTrue 让输入张量形状是 (batch_size, lookback, n_features)这样写数据加载器的时候不容易搞错维度。hidden_size128 在日线数据上已经够用增加到 256 收益不大反而让训练时间翻倍num_layers2 是多数时序任务里比较稳的选择1 层表达力不足3 层以上对几千条样本的数据集很容易过拟合。dropout0.2 放在 LSTM 层之间能在训练时随机丢弃部分信息减弱对训练集的死记硬背。3.3 训练策略时间序切分、早停与模型保存股票数据不能随机切分这是一个容易被忽视的隐患。随机 shuffle 会让训练集里混入未来样本模型在验证集上表现奇好实盘里完全失效。正确的做法是按时间顺序切分比如前 70% 训练、后 30% 验证或者用 TimeSeriesSplit 做多折滚动验证。def train_model(model, train_loader, val_loader, epochs30, lr1e-3): optimizer torch.optim.Adam(model.parameters(), lrlr) 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 不下降再停。参数方面lr1e-3 是 Adam 的常见起点如果训练不稳定降到 3e-4epochs30 对日线级别几千条样本足够配合早停一般 15 轮左右就能收敛。4. 回测与评估用方向准确率、最大回撤验证这套系统的真实水平模型训练完只是第一步。答辩时老师最常问的一句话是「你这个预测模型拿去回测到底赚不赚钱」所以回测框架和评估指标是整个 project 里最容易被看见的部分。这一章先给最小回测实现再讲指标怎么解读最后给 walk-forward 的正确写法。4.1 回测框架的最小实现逐日迭代、按信号持仓回测的核心是模拟「每天看一眼模型预测决定买还是卖」的过程。不要用一次性预测整段行情再计算收益因为那样默认你提前知道了所有未来信息。def run_backtest(df, pred_prob, fee0.001, threshold0.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, indexdf.index)代码逻辑是当预测概率超过 threshold 时全仓买入低于 threshold 时空仓每次交易都扣千分之一手续费。threshold0.55 的意思是模型对「明日上涨」有 55% 以上把握才动手目的是过滤掉模型没信心的预测减少无意义交易。fee0.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, horizon30, train_size250): 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_size250 大约是一个交易年的样本量horizon30 是一个月的预测期。这种写法能让答辩老师看到你理解了「时间顺序」这一核心概念也方便统计多次预测的累计收益。唯一要注意的是窗口大小不要设太极端train_size 太小模型学不到趋势horizon 太长预测误差会快速累积。5. 避坑与常见问题股票预测项目最容易翻车的 5 个血泪点这一章不写理论直接列我在帮人审项目、自己调模型时反复遇到的五类问题。每一条都是按「现象 → 原因 → 解决」的路径来说明。5.1 前视偏差回测曲线很漂亮实盘一跑就废现象是训练好的模型在回测里年化收益率高达 80%看起来稳赚不赔但一放到实盘模拟盘就跑不出那个曲线。原因几乎可以锁定是前视偏差特征构造或标签构造时用了当天收盘后才知道的数据比如用当天收盘价计算的均线去预测当天是否上涨等于把答案泄露给了模型。解决方法是严格区分「T 日收盘后能拿到什么」和「T1 日开盘才能确认什么」。最简单的一条合规准则是所有特征在 T 日计算完毕所有标签指向 T1 日及之后的结果。我在 make_features 里已经用 shift(-1) 做了标签偏移但如果你自己往特征里加了 MACD、布林带等指标一定要再手算一遍指标的最后一个值是否引用了未来数据。5.2 随机切分训练集和测试集验证集指标虚高现象是验证集上的方向准确率超过 65%远高于正常水平。原因大概率是你用了 sklearn 的 train_test_split默认 shuffleTrue导致训练样本和验证样本在时间上互相穿插。股票的某一天行情会被同时分到训练集和验证集模型等于提前见过答案。解决方法是改成按时间顺序切分比如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%。解决方案是统一使用前复权数据并且在拉数时就把 adjustqfq 写死。要注意的是前复权数据在不同时间点拉取历史价格会因为后续分红而变化所以正式训练前要把数据缓存固定住回测和训练必须用同一份缓存否则重新拉一次数特征和标签都会轻微变化结果很难复现。5.4 过拟合训练集准确率高验证集一塌糊涂现象是训练集上的 loss 降到很低方向准确率超过 70%但验证集只有 50%接近瞎猜。原因一般是模型参数量相对于数据量太大或者训练轮数太多模型把历史行情里的噪声当成规律记住了。3000 条日线数据撑不起一个三层 512 维的 LSTM。解决办法是先做减法hidden_size 降到 64 或 128num_layers 保持 2去掉多余的技术指标特征。然后加早停patience 设为 5 到 8验证 loss 连续多轮不降就停止。还可以在数据增强上做文章用多只股票一起训练模型让模型学习共性的价格行为规律而不是单只股票的路径依赖。5.5 预测价格还是预测涨跌损失函数选错导致模型退化现象是模型预测的明天收盘价总是非常接近今天的收盘价误差看着不大但完全没有交易价值。原因是用 MSE 回归去做价格预测时最优解在数学上就是「预测历史均值」模型学到最后相当于在复读昨日价格这在本质上失去了预测意义。更稳的做法是转成二分类预测「明日是否上涨」用 BCEWithLogitsLoss。如果项目方向要求必须输出价格可以改成预测未来 5 日的累计收益率再做秩排序这种任务不会退化成复读机。我的个人习惯是面向答辩和课程作业用二分类加方向准确率来解释模型价值远比扣 MSE 数值更有说服力。6. 把预测系统封装成能存档的交付物目录规范、验证脚本和演示技巧做到这里模型已经能跑通回测也能出结果但很多人最后输在交付物太乱。一个 zip 包打开来全是 Untitled.ipynb答辩老师连跑通的信心都没有。一个成熟的课程作业交付物建议按下面的目录组织。目录/文件作用data/行情缓存数据包含取数脚本生成的 Parquet 文件features/特征工程代码与特征配置说明models/模型定义、训练脚本、最佳权重 best_model.ptbacktest/回测框架、评估指标计算代码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答辩演示时不要从头敲代码而是直接跑这个脚本边跑边指着屏幕说这是数据缓存这是特征窗口这是验证集的方向准确率这是回测最大回撤。演示之前先在干净的虚拟环境里完整跑三遍确保没有因为路径、依赖版本或缓存问题中途报错。给老师展示的结论建议控制在三个方向准确率、回测净值曲线、最大回撤。我最早做这个题目时觉得训练模型是最难的部分后来发现最难的是让结果可复现、让流程可解释。每次提交前我会强制自己问一个问题如果评委明天就要这份代码能不能在一个新环境里十分钟跑出同样的图表这样想之后数据缓存、依赖锁定、脚本顺序这些小事反而变成了整个项目里最值钱的部分。这套思路同样适用于以后的工作交接希望帮到你。本文还有配套的精品资源点击获取