ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

沪深300预测全流程:数据清洗、LSTM训练与实时预测

沪深300预测全流程:数据清洗、LSTM训练与实时预测 简介针对沪深300指数预测的完整深度学习项目覆盖数据下载、清洗、LSTM模型训练与实时预测全流程适合AI、通信、自动化等计算机相关专业学生及科研人员用于毕业设计、课程设计与项目演示。压缩包共11个文件以Python脚本为主含数据预处理、模型构建、主程序及配置文件另附CSV训练/测试数据集、Transformer对照模块、Markdown说明文档和readme整体约3MB目录结构清晰便于按模块复现与二次开发。资源由专业团队测试完善运行稳定并附详细设计文档帮助理解代码逻辑已有50人参与学习下载适合具备一定编程基础、希望快速落地股票预测项目的进阶学习者。可在现有框架上扩展功能例如调整特征工程或替换模型结构以满足个性化研究需求。1. 沪深300股票预测项目下载、清洗、LSTM与实时预测整条链路你在网上搜到“沪深300股票预测项目-数据下载清洗LSTM模型训练实时预测含详细文档.zip”这个标题时大概率已经踩过一轮“代码能跑但预测是条水平线”的坑。这个项目说白了是一条完整链路把沪深300指数或成分股的行情数据下载到本地做掉复权、去重、缺失填充这些脏活再用LSTM学习收盘价的序列规律最后用训练好的模型对下一个交易日做实时滚动预测。它解决的是“从零开始搭一套可复现的预测流程”的问题适合正在学习LSTM时间序列预测、或者想用沪深300做量化策略验证的Python从业者。我按这条链路完整走一遍每一步都给出能直接落地的代码和参数最后一章讲验证方法——别等上线之后才发现模型只是拟合了一个平均值。2. 数据下载把沪深300行情从数据源落成本地CSV2.1 为什么先选tushare的免费接口做数据下载做沪深300预测第一步是拿到干净的日线行情。选择数据源时我一般看三个指标接口是否稳定、是否有历史全量数据、是否容易拿到本地。常见的免费渠道里tushare的pro接口和akshare是两大主力但akshare有时会跟着上游网页改版而突然失效所以我在项目初期更倾向于tushare pro——它有结构化的指数日线接口字段规范返回的是标准的pandas DataFrame适合后续直接做pandas数据清洗。要注意的是下载沪深300指数行情和下载单只股票行情的接口不一样指数用index_daily个股用daily。如果你做的是成分股组合预测还需要先通过index_weight拿到最新一期的成分股权重列表再循环下载每只股票的日线。标题里没有明确说预测对象是指数还是成分股我以下载指数日线为主这也是最常见、最省事的做法——沪深300指数本身自带“一篮子股票”的分散效果单指数序列也更容易跑通LSTM训练流程。初次使用tushare会卡在token上注册账号后在个人主页拿到一串token在代码里设置一次就能调接口。部分接口对账号积分有要求普通新号也能拉到底层日线数据只是单次返回行数有上限。这个限制会在后面的代码里用循环翻页来处理不影响整体流程。2.2 数据下载脚本指数日线一条龙落盘import tushare as ts import pandas as pd # 1. 初始化token 在 tushare 个人主页获取 ts.set_token(你的token) pro ts.pro_api() # 2. 下载沪深300指数日线左闭右闭时间是YYYYMMDD字符串 df pro.index_daily( ts_code000300.SH, start_date20150101, end_date20241231 ) # 3. 只保留建模要用的字段别把无关列带进清洗环节 df df[[trade_date, open, high, low, close, vol, amount]] # 4. 接口返回的数据默认按交易日倒序排序后重置索引 df df.sort_values(trade_date).reset_index(dropTrue) # 5. 落盘后续清洗直接读这份CSV df.to_csv(hs300_daily_raw.csv, indexFalse) print(df.shape) print(df.head())逻辑说明pro.index_daily返回的trade_date是整数形态的日期比如20241231字段open、high、low、close是当天开高低收价格vol是成交量amount是成交额。排序这步很关键因为tushare默认按最近日期优先返回如果不排序后续构造LSTM的60天窗口时序列顺序就是反的训练出来的模型等于在“倒放”行情。参数说明ts_code填000300.SH注意沪深300指数是.SH后缀不要误填成000300.SZstart_date和end_date是字符串不是datetime对象前后都包含在内。时间范围我习惯从2015年牛市前开始取这样样本量大概有2400多个交易日喂给LSTM足够。如果你的tushare积分有限单次返回行数受限可以按年份分段下载再concat这是常见做法。如果你要下载的是成分股数据把pro.index_daily换成pro.daily(ts_code600000.SH, start_date..., end_date...)即可。这里有个重要提醒个股数据必须处理复权否则除权除息日会出现价格跳空LSTM会把这种跳空当成真实规律来学。指数的日线本身不存在复权问题但如果你混合使用个股数据做组合预测请直接用ts.pro_bar(ts_code600000.SH, adjqfq)拉前复权数据这一步不能省。下载完先别急着清洗打开CSV看一眼再动手。我见过不少人在数据下载阶段顺手把列名改了结果清洗脚本里字段对不上来回折腾。数据下载和清洗是两个步骤输出hs300_daily_raw.csv后检查一下行数是否合理、末尾日期是否最新这是进入下一章前的最低自检。3. 数据清洗复权、去重、缺失填充的三步落地3.1 清洗前先看数据长什么样从tushare拉下来的数据看着挺干净但不代表可以直接训练。沪深300指数日线常见的问题有三个一是交易日不连续遇到节假日和停牌日期序列有缺口二是trade_date是整数或字符串格式直接当日期索引用会出问题三是极端情况下接口返回重复行比如重复调用了两次接口。这些问题不处理LSTM的序列窗口就会错位。在写清洗脚本之前先执行一行df.info()和df.isnull().sum()确认字段类型和缺失情况。我遇到的情况里vol和amount偶尔会出现个别缺失值close作为预测目标几乎不会缺但你不能赌它不出问题。pandas数据清洗的核心套路是先看结构、再补缺失、最后去重排序顺序不能反——如果你先去重后补缺失补出来的行可能又插到重复行附近等于白干。3.2 清洗代码从原始CSV到可训练数据集import pandas as pd # 1. 读入原始数据 df pd.read_csv(hs300_daily_raw.csv) # 2. trade_date统一转成datetimeformat必须写否则int会被误读 df[trade_date] pd.to_datetime(df[trade_date], format%Y%m%d) # 3. 按日期排序并去重保留最后一条防止重复调用接口引入重复行 df df.sort_values(trade_date) df df.drop_duplicates(subsettrade_date, keeplast) # 4. 把缺失的交易日补出来用ffill填充收盘价 # 这一步对LSTM很重要模型要求时间步连续不能有空洞 df df.set_index(trade_date).asfreq(D).reset_index() # 5. 填充价格和成交量的缺失值 # 常见做法价格用前值填充成交量用0填充停牌日无成交 df[close] df[close].ffill() df[open] df[open].ffill() df[high] df[high].ffill() df[low] df[low].ffill() df[vol] df[vol].fillna(0) df[amount] df[amount].fillna(0) # 6. 剔除价格异常值收盘价小于等于0的行直接去掉 df df[df[close] 0] # 7. 保存清洗后的数据 df.to_csv(hs300_daily_clean.csv, indexFalse) print(f清洗后共 {len(df)} 行日期范围 {df[trade_date].min()} ~ {df[trade_date].max()})逻辑说明第2步的format%Y%m%d必须写。trade_date是20240101这种数字pd.to_datetime如果不给format会尝试自动解析速度慢且容易在混合格式时报错。第4步是这轮清洗的核心用asfreq(D)把日期扩展到自然日粒度周末和节假日就变成了NaN行再通过ffill把价格延续下去。这样做的目的是让LSTM看到的是一个等间隔的时间序列否则模型内部的时间步长含义会飘。参数说明ffill是前向填充用前一个有效值补后面的NaN停牌日和后一个工作日之间的缺失值用它最合理成交量用0填充是因为停牌日确实没有成交而价格用0填充就是灾难。drop_duplicates里的keeplast保证接口返回多份数据时保留最新一份。如果你后续要做更精细的预测可以把周末的填充从ffill改成interpolate()线性插值但训练趋势类模型时ffill更稳不容易在行权日和长假后造出假K线。清洗后的CSV里会包含周末和节假日这些“实为同一价格”的重复日LSTM训练时可以接受但如果你在实时预测阶段发现窗口长度不够60天原因就在这里——自然日序列里最近60天很可能只包含40个真实交易日预测时要用真实交易日窗口对齐这点在避坑章节再展开。3.3 清洗结果怎么自查清洗完成不等于清洗正确。我每次清洗后都会做三个检查第一df[close].max()和close.min()是否在合理区间沪深300近十年低点在2000多点高点在5900多点超出这个范围就有脏数据第二抽查几个节假日前后日期确认close是延续的而不是跳变第三确认没有全NaN列df.isnull().sum().sum()应该等于0。更严谨的做法是把清洗前后的数据画成收盘价曲线叠加对比肉眼扫一遍有没有异常断崖。不过这属于可视化的验证命令行项目里我用一个更简单的办法打印清洗后各列的describe()看看count是否一致、min是否大于0。数据清洗没有玄学所有规则都能写成断言把它当成正式的质检步骤而不是顺手做的事。这章的核心思路是清洗是为LSTM的序列连续性服务的。数据下载拿到的是“真实交易日”数据清洗后拿到的是“等间隔连续”数据两者用途不同。你可以在训练时用清洗后的连续序列预测时回到真实交易日序列这个差异就是后面避坑章节里窗口错位的根源记住这一点能少翻几次车。4. LSTM模型训练序列窗口、标准化与训练参数4.1 为什么用LSTM而不是ARIMA或普通MLP如果你只是想做沪深300收盘价预测ARIMA也能跑线性回归也能跑但LSTM在“序列长期依赖”上的建模能力确实更强。沪深300指数收盘价不是纯随机游走它带有趋势成分、均值回归特性和波动聚集效应LSTM通过门控机制能记住几十个交易日前的形态这是普通MLP做不到的——MLP把每天的输入当成独立特征彻底丢掉了时间顺序信息。ARIMA则是线性模型对非线性关系拟合能力有限。需要提醒的是LSTM不是银弹。预测金融时间序列的难度在于信噪比极低模型很容易学到“昨天收盘价≈今天收盘价”这个懒惰规律训练出来的模型表面上看loss很低实际输出几乎是一条水平线。这个问题的根源和解决方案我会在训练环节直接讲避免你走冤枉路。还有一个选型细节用PyTorch而不是TensorFlow或Keras。PyTorch的nn.LSTM接口清晰可以在forward里控制返回的hidden state和cell state调试时能看到每一步的张量形状对单序列预测这种小规模任务它比TensorFlow轻量得多。下面的代码全部基于PyTorch版本2.x都能直接跑。4.2 构造训练样本60天窗口与标签设计LSTM的输入不是单条行情而是一段长度为lookback的历史窗口。常见做法是用过去60个交易日的收盘价预测下一日收盘价这个60是经验值太短学不到中期趋势太长则会引入过多噪声而且训练样本数会急剧减少。标签设计是这里最容易埋雷的地方。如果你直接预测下一日收盘价的绝对值模型会被梯度推着去学“输出尽量接近输入序列最后一个值”因为收盘价序列本身高度自相关。更稳健的做法是预测下一日的涨跌值或涨跌幅。我用涨跌值作为标签y close[t1] - close[t]这样模型学习的是变化量而不是绝对价格方向准确率更容易提升也规避了价格水平不同导致的尺度问题。import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler # 读取清洗后的数据 df pd.read_csv(hs300_daily_clean.csv, parse_dates[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) close df[close].values.reshape(-1, 1) # 用涨跌值作为标签避免模型学到“复制上一个价格” diff np.diff(close, axis0) X_raw close[:-1] # 输入第 t 天的收盘价 y_raw diff # 标签第 t1 天相对第 t 天的涨跌值 # 按时间顺序切分前80%训练后20%测试绝对不能随机切 split int(len(X_raw) * 0.8) X_train_raw, X_test_raw X_raw[:split], X_raw[split:] y_train_raw, y_test_raw y_raw[:split], y_raw[split:] # 标准化只在训练集上fit测试集和实时预测复用训练集的scaler scaler MinMaxScaler(feature_range(0, 1)) X_train_scaled scaler.fit_transform(X_train_raw) X_test_scaled scaler.transform(X_test_raw) y_scaler MinMaxScaler(feature_range(0, 1)) y_train_scaled y_scaler.fit_transform(y_train_raw).ravel() y_test_scaled y_scaler.transform(y_test_raw).ravel() # 构造60天滑窗序列 def make_sequences(X, y, lookback60): X_seq, y_seq [], [] for i in range(lookback, len(X)): X_seq.append(X[i - lookback:i, 0]) y_seq.append(y[i]) return np.array(X_seq, dtypenp.float32), np.array(y_seq, dtypenp.float32) X_train, y_train make_sequences(X_train_scaled, y_train_scaled) X_test, y_test make_sequences(X_test_scaled, y_test_scaled) # 增加通道维度LSTM要求 (batch, sequence_length, input_size) X_train X_train[:, :, None] X_test X_test[:, :, None] print(f训练样本 {X_train.shape}, 测试样本 {X_test.shape})逻辑说明np.diff计算相邻两天的差值得到的就是每天相对前一天的涨跌值。构造序列时X用的是close[:-1]y用的是diff这样X[i]对应第i天的价格y[i]对应第i1天的涨跌值时间对齐刚好不偏移。MinMaxScaler分别处理价格和涨跌值因为两者的量纲不同涨跌值本身数值很小混在一起标准化会压低价格维度的影响。参数说明lookback60是序列长度可以调成30或90后面我会讲怎么判断哪个值适合你的数据。切分比例0.8是常见设置时间序列不需要交叉验证那种随机打乱按时间切分就够了。每个scaler都要在训练集上fit测试集和实时预测时只能transform这是防止数据泄漏的底线。4.3 PyTorch LSTM的模型结构与训练代码import torch import torch.nn as nn from torch.utils.data import TensorDataset, DataLoader class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) # 输出层把最后一个时间步的hidden state映射到涨跌值 self.fc nn.Linear(hidden_size, 1) def forward(self, x): # x shape: (batch, seq_len, input_size) out, _ self.lstm(x) # 取最后一个时间步的输出 out out[:, -1, :] return self.fc(out) model LSTMPredictor(input_size1, hidden_size64, num_layers2, dropout0.2) criterion nn.MSELoss() optimizer torch.optim.Adam(model.parameters(), lr0.001) train_dataset TensorDataset(torch.from_numpy(X_train), torch.from_numpy(y_train)) train_loader DataLoader(train_dataset, batch_size64, shuffleFalse) epochs 60 for epoch in range(epochs): model.train() total_loss 0 for X_batch, y_batch in train_loader: optimizer.zero_grad() pred model(X_batch).squeeze() loss criterion(pred, y_batch) loss.backward() optimizer.step() total_loss loss.item() if (epoch 1) % 10 0: avg_loss total_loss / len(train_loader) print(fepoch {epoch 1}, loss {avg_loss:.6f}) # 训练完把模型和scaler都存起来实时预测阶段要加载 import joblib torch.save(model.state_dict(), models/lstm_hs300.pt) joblib.dump(scaler, models/scaler.save) joblib.dump(y_scaler, models/y_scaler.save)逻辑说明模型结构是“LSTM 全连接输出”。输入每个batch的形状是(64, 60, 1)经过两层LSTM后输出(64, 60, 64)我们只取最后一天out[:, -1, :]再过一个全连接层得到预测的涨跌值。这里没有用hidden的初始值默认全零初始化对短序列够用。dropout0.2在两层LSTM之间生效防止过拟合训练集。训练循环里shuffleFalse是故意设置的。时间序列的batch内顺序不能打乱否则模型会学到跨样本的未来信息。如果你发现loss在训练集上降到很低但测试集上完全不行优先检查这里是不是开了shuffle。Adam默认学习率0.001在LSTM这种量级的任务上通常够用不需要手动调。参数说明hidden_size64决定了LSTM的记忆容量序列越长、规律越复杂这个值可以往128提num_layers2是层数层数过深容易过拟合2~3层是常见选择batch_size64影响收敛速度数据量小就用32大了容易震荡epochs60不是固定的我一般看loss是否在最后10轮还在明显下降如果在降就加30轮如果已经平了就停。4.4 训练参数怎么调lr、epoch、hidden_size的边界表格形式直接给参数边界值这是我最常用的调试起点参数常见范围我的调试建议lookback30 ~ 90先试60预测方向不准再试90对比hidden_size32 ~ 128数据量2000条用64起步样本少就32num_layers1 ~ 3先2层loss不稳定就降1层dropout0 ~ 0.3验证集loss回升时加dropoutlr1e-4 ~ 1e-2默认1e-3loss震荡就降到1e-4batch_size32 ~ 128时间序列别太大64以内稳定epochs40 ~ 150用early stopping看loss平台期这些参数不是独立调的lookback增大意味着每个样本覆盖更长的时间段hidden_size也要相应增加因为模型要记住更多天的信息。我见过有人把lookback调到120、hidden_size还保持32结果训练loss下降极慢那是记忆容量和输入长度不匹配。另外用涨跌值做标签后loss值会比直接预测收盘价小一个数量级别看到0.001就以为模型完美了。正确判断标准是在测试集上看方向准确率也就是预测的涨跌方向与真实涨跌方向一致的比例。这个指标后面实时预测章会重点用到。5. 实时预测落地与四个常见坑从模型文件到每日结果5.1 实时预测脚本加载模型与scaler滚动输出训练结束只是完成了70%的工作。实时预测意味着你要在每天收盘后拿到最新行情追加到历史数据里滚动预测下一个交易日的涨跌。这里最容易犯的错是直接复用训练时的scaler去标准化新数据但忘了保存scaler本身。训练代码里已经把scaler和y_scaler都存到了models/目录下面就是实时预测脚本的核心部分。import joblib import numpy as np import pandas as pd import torch import torch.nn as nn # 模型结构必须与训练时一致否则加载权重会报错 class LSTMPredictor(nn.Module): def __init__(self, input_size1, hidden_size64, num_layers2, dropout0.2): super().__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) out out[:, -1, :] return self.fc(out) def predict_next_trading_day(csv_pathhs300_daily_clean.csv, lookback60): # 1. 加载模型和两个scaler model LSTMPredictor() model.load_state_dict(torch.load(models/lstm_hs300.pt)) model.eval() price_scaler joblib.load(models/scaler.save) diff_scaler joblib.load(models/y_scaler.save) # 2. 读取清洗后的数据必须按日期排序 df pd.read_csv(csv_path, parse_dates[trade_date]) df df.sort_values(trade_date).reset_index(dropTrue) close df[close].values # 3. 检查窗口长度是否足量 if len(close) lookback: raise ValueError(f历史数据不足需要{lookback}天现有{len(close)}天) # 4. 取最近lookback天按训练时的方式标准化 window close[-lookback:].reshape(-1, 1) window_scaled price_scaler.transform(window) X torch.tensor(window_scaled, dtypetorch.float32).unsqueeze(0) # (1, 60, 1) # 5. 预测涨跌值再反标准化回原始价格单位 with torch.no_grad(): diff_scaled model(X).item() diff_pred diff_scaler.inverse_transform([[diff_scaled]])[0][0] next_close close[-1] diff_pred return next_close, diff_pred next_close, diff_pred predict_next_trading_day() print(f下一交易日预测收盘价{next_close:.2f}) print(f预测涨跌值{diff_pred:.2f})逻辑说明实时预测的核心是“把最近60天的收盘价做成和训练时完全一样的输入格式”。window_scaled用训练时fit好的price_scaler转换模型输出的涨跌值再用diff_scaler逆变换最后加上当前最新收盘价得到预测值。这里有个关键点close[-1]是最后一个真实收盘价实时预测只能基于它往前推不能把昨天的预测值也当成真实输入。参数说明lookback必须和训练时一致这里是60。如果预测时改了它模型输入维度不变但语义变了预测结果直接失真。model.eval()会关闭dropout保证预测阶段多次运行结果一致。如果你希望每天自动执行把这段脚本存成predict.py用cron在收盘后定时跑这是最常见的方式。5.2 常见坑1到坑4现象、原因与解决这里把我在这个项目上走过的弯路集中整理成四条血泪经验每一条都是能直接对照排查的真实场景。坑1训练loss降得很漂亮预测结果却是一条水平线。现象是测试集上所有预测的涨跌值都接近0画出来几乎是一条直线。原因是模型学会了“用上一个收盘价预测下一个收盘价”在涨跌值标签下等价于预测0。解决方法是检查标签构造确认y是diff而不是close[t1]如果已经是diff把hidden_size调大并加入dropout强迫模型学习更复杂的模式而不是走捷径。坑2测试集方向准确率虚高实时预测却失效。现象是训练和测试时模型表现都不错一旦接入实时数据就完全偏离。原因多半是数据泄漏——比如在构造序列时不小心用scaler.fit_transform作用到了整个数据集或者测试集做了shuffle。解决方法是严格按第4章的流程scaler只在训练集上fit测试集只用transformDataLoader的shuffleFalse。坑3实时预测时报错“历史数据不足”。现象是程序跑到第4步就跳出ValueError明明CSV里有几千行数据。原因是清洗时用asfreq(D)把日期补成了自然日你取最近60天是60个自然日而其中只有约40个真实交易日。解决方法是在实时预测前重新按真实交易日过滤数据df df[df[vol] 0]或者直接不补自然日用真实交易日索引构造窗口。这个坑最容易在春节和国庆长假后出现。坑4第一次训练或者调整参数后loss变成NaN。现象是epoch跑到中途loss突然变成nan之后一直nan。原因是数据里混入了Inf或者NaN值或者学习率太大导致梯度爆炸。解决方法分两步先跑np.isfinite(df[close]).all()检查数据再把lr降到1e-4并在训练循环里加一个clamploss torch.clamp(loss, max10.0)防止梯度异常。这四个坑本质上是同一个问题的四个侧面数据一致性。训练时的数据形态、标签定义、标准化参数、序列窗口在实时预测时都必须原样复现。模型本身不复杂复杂的是一条链路里每个环节的隐含假设。6. 验证模型没有过拟合用滚动回测代替单次测试单次切分训练集和测试集只能说明模型在这一个时间切点上没翻车说明不了它在未来各个时段都稳定。我更推荐用滚动回测来验证在测试区间内从第60个交易日开始每天只用“当天及之前”的数据预测下一天然后滚动推进。这正好模拟实时预测的真实操作结果也更有说服力。def rolling_backtest(model, price_scaler, diff_scaler, close, start_idx60): model.eval() pred_diffs, real_diffs [], [] for i in range(start_idx, len(close) - 1): window close[i - start_idx:i].reshape(-1, 1) window_scaled price_scaler.transform(window) X torch.tensor(window_scaled, dtypetorch.float32).unsqueeze(0) with torch.no_grad(): diff_scaled model(X).item() pred_diff diff_scaler.inverse_transform([[diff_scaled]])[0][0] pred_diffs.append(pred_diff) real_diffs.append(close[i 1] - close[i]) pred_diffs np.array(pred_diffs) real_diffs np.array(real_diffs) direction_acc np.mean(np.sign(pred_diffs) np.sign(real_diffs)) return direction_acc direction_acc rolling_backtest(model, scaler, y_scaler, close[split:]) print(f滚动回测方向准确率{direction_acc:.2%})逻辑说明滚动回测没有重新训练模型只是用训练好的模型在测试段逐日预测。每一步的输入窗口只包含截止当天及之前的数据这严格避免了信息泄漏。方向准确率直接反映“预测涨跌方向对不对”比RMSE更贴近实际使用价值。我在实战里积累的教训是方向准确率稳定在55%以上才值得考虑做进一步的策略低于这个数字模型再复杂也只是在拟合噪声。这个项目做下来最大的体会是——金融时序预测里数据清洗和数据一致性才是决定成败的部分LSTM模型本身反而是最不值得纠结的环节。希望帮到你。本文还有配套的精品资源点击获取
返回列表