ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

TensorFlow LSTM预测股票次日开盘价:从数据清洗到Checkpoint加载

TensorFlow LSTM预测股票次日开盘价:从数据清洗到Checkpoint加载 简介基于TensorFlow构建的股票价格走势预测项目面向具备一定Python基础、希望入门量化选股或序列预测的开发者。项目通过tushare模块获取股票历史数据利用pandas完成清洗与特征处理再以TensorFlow搭建模型学习价格规律最终借助matplotlib将真实走势与预测结果可视化当前聚焦预测第二日开盘价流程完整、可直接运行学习。压缩包共5个文件包含两个Python脚本、一个checkpoint文件及配套的data与index数据文件其中.py文件分别负责模型加载与预测测试模型文件用于保存训练好的权重整体仅456KB便于快速下载与本地调试验证。目前已有2142人学习使用适合作为股票价格预测实战的入门参考。通过对照load_model.py与test.py的调用逻辑读者可以理解模型持久化、数据对齐和单步预测的基本思路并在此基础上扩展多步预测或引入更多特征。1. 用TensorFlow做股票价格走势预测解压后直接加载模型预测第二天开盘价拿到这份基于python实现股票价格走势预测的压缩包时我原本没抱太高期望很多号称“预测股票”的项目解压后不是缺权重就是少脚本。这个包反而干净cp-0002.ckpt.index、cp-0002.ckpt.data-00000-of-00001、checkpoint三个模型文件配上load_model.py和test.py两个脚本没有花架子。整个链路是TensorFlow搭LSTM模型tushare拉日线行情pandas清洗matplotlib把真实开盘价和预测开盘价画在同一张图上。它能干的事非常具体用最近60个交易日的开盘价预测下一个交易日的开盘价。适合已经装好Python环境、想跑通一个完整量化预测流程的人也适合手上有checkpoint却不知道该怎么恢复模型做预测的开发者。预测准确率属于“趋势大致对、数值有偏差”的水平把它当参考没问题当提款机还远。2. tushare与pandas数据链路从拉日线到切出60天窗口模型再玄学数据错一步后面全白搭。这一章先把数据侧讲透接口怎么选、字段怎么洗、窗口怎么切这三步决定了模型能不能学到真实规律。2.1 tushare接口选型daily接口的参数细节tushare是A股数据里最常用的python数据源之一。预测脚本里拉日线最典型的写法是调用pro.daily接口下面这段可以被直接复用import tushare as ts import pandas as pd ts.set_token(你的token) # 在tushare官网注册后换成自己的token pro ts.pro_api() df pro.daily( ts_code000001.SZ, # 深市平安银行沪市用600000.SH start_date20200101, end_date20241231 ) df df.sort_values(trade_date).reset_index(dropTrue) print(df.head())pro.daily返回的trade_date是YYYYMMDD格式的字符串而且默认按日期倒序排列。排序这一步不能省不然喂给LSTM的序列是时间倒流的模型会学到“明天等于昨天”的错觉。ts_code的后缀规则是沪市SH、深市SZ指数代码带指数后缀这个参数写错会直接返回空表。但这里有个隐蔽问题pro.daily返回的是未复权价格。上市公司分红送转后历史价格会出现人为跳空LSTM看到这种断层会给出离谱预测。我一般会换成ts.pro_bar接口它直接支持复权参数df ts.pro_bar( ts_code000001.SZ, start_date20200101, end_date20241231, adjqfq # qfq前复权hfq后复权 )对比项ts.pro.dailyts.pro_bar复权处理未复权需自己算支持qfq/hfq日期字段trade_date字符串datedatetime类型排序默认降序需手动排序提示tushare的pro接口有积分门槛daily需要120分以上pro_bar部分参数需要更高积分。注册后完善个人信息能拿到基础分够用了。2.2 pandas清洗拿到手先处理这四件事原始数据不能直接进模型。我习惯按缺失值、重复行、列裁剪、类型检查四步来洗每步都有明确目的print(df.isna().sum()) # 1. 缺失值统计重点看open列 df df.dropna(subset[open]).copy() df df.drop_duplicates(subset[trade_date]) # 2. 按日期去重 df df[[trade_date, open, high, low, close, vol]] df df.rename(columns{vol: volume}) # 3. 只留需要的列 print(df.dtypes) # 4. 确认日期是str、价格是float缺失值最常见的来源是停牌日停牌当天没有成交记录tushare会把该行直接跳过而不是填NaN。如果不做处理窗口切分时会把停牌前后的两个交易日拼成连续序列等于给模型喂了一段“假历史”。重复行则多来自分页拉数据时的边界重叠用drop_duplicates按日期去重解决。vol字段单位是“手”不是“股”计算成交额时需要乘以单价再乘100但这个包只做开盘价预测vol只用来辅助观察不参与训练。2.3 为什么只拿开盘价当特征窗口长度和特征维度的选择这个包的设计思路很有意思只预测第二天的开盘价而输入特征只有开盘价一根序列。很多初学者会问为什么不把最高价、最低价、成交量全塞进去我自己的血泪经验是特征越多数据泄漏和数据漂移的坑越多。成交量在免费数据里的噪声很大high和low本身与open存在同期强相关这些特征加进去训练集loss会降低但换一段行情就翻车。LSTM需要一个固定长度的输入窗口。这个包里checkpoint记录的输入形状是三维张量对应(批次, 60, 1)也就是60个交易日、每个交易日1个特征。60天约等于一个季度的交易日数量是短期信息与中期趋势的折中。窗口太短比如20天模型只盯着最近一个月抓不住平台期窗口太长比如90天样本数量变少训练时间变长收益提升却很有限。值得提醒的是移动平均MA5、MA10这类衍生特征也别急着加它们本质是历史价格的线性组合模型学会“抄均线”后在样本外会迅速失效。3. TensorFlow时序建模把60天开盘价映射成第二天开盘价数据洗好只完成了一半接下来要做的是把“过去60天”和“下一天”的关系用模型拟合出来。这里用到的不是普通全连接网络而是LSTM。3.1 为什么是LSTM序列记忆与梯度消失股票开盘价不是独立随机变量今天的价格往往和过去一段时间的数据存在依赖关系这就是时间序列的自相关性。普通全连接网络把60天的价格当成60个互不相干的特征丢失了交易日之间的先后顺序。RNN能保留顺序但朴素RNN在长序列上梯度消失严重学到后面就忘了前面。LSTM用输入门、遗忘门、输出门三组门控结构来控制信息保留和丢弃在60天这个中等长度的窗口上既不会忘掉一个月前的趋势又能对最近几天的突变做出反应。这个包把序列切到60天正好落在LSTM擅长的区间里。3.2 样本构造与数据集划分索引别写错预测值就是下一天模型训练前要把一维的价格序列转成监督学习格式——X是窗口y是窗口外的下一天。这里最常见的一个错误是把y取成窗口内最后一个值那等于让模型复读输入loss会很低但毫无预测能力。import numpy as np def make_sequences(open_prices, seq_len60): X, y [], [] for i in range(len(open_prices) - seq_len): X.append(open_prices[i: i seq_len]) # 第 i 天到第 i59 天 y.append(open_prices[i seq_len]) # 第 i60 天即窗口后的第一天 return np.array(X).reshape(-1, seq_len, 1), np.array(y).reshape(-1, 1) X_all, y_all make_sequences(scaled_open, seq_len60) print(X_all.shape, y_all.shape)索引规则必须对齐当X取open_prices[0]到open_prices[59]时y是open_prices[60]也就是第61个交易日的开盘价。如果把y写成open_prices[i seq_len - 1]模型学到的就是从60天序列里“摘出最后一个值”实盘预测时立刻失效。归一化要在切窗口之前完成。我一般用MinMaxScaler把价格缩放到0到1之间避免LSTM激活函数在输入端饱和from sklearn.preprocessing import MinMaxScaler scaler MinMaxScaler(feature_range(0, 1)) scaled_open scaler.fit_transform(open_prices.reshape(-1, 1)).flatten()注意这里fit_transform只应该对训练集调用。严谨的做法是先切分训练集和测试集再用训练集的价格拟合scaler最后统一transform全量数据。预测阶段再单独fit一次是后面避坑章里的头号翻车原因。数据集划分遵循时间顺序而不是随机打乱split int(len(X_all) * 0.8) X_train, X_val X_all[:split], X_all[split:] y_train, y_val y_all[:split], y_all[split:]时间序列一旦随机shuffle验证集里会混入未来的样本验证loss虚低实盘时立刻现出原形。前80%做训练、后20%做验证是序列预测的通用做法不用K折交叉验证因为股票行情不满足样本独立同分布假设。3.3 模型结构与训练参数两层LSTM加Dropout的典型配置这个包没有附带训练脚本但根据checkpoint的输入形状训练侧大概率是一个两层LSTM加全连接输出的结构。这也是我处理单变量序列预测时最常用的配置import tensorflow as tf model tf.keras.Sequential([ tf.keras.layers.LSTM(64, return_sequencesTrue, input_shape(60, 1)), tf.keras.layers.Dropout(0.2), tf.keras.layers.LSTM(32), tf.keras.layers.Dense(1) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossmse ) model.summary()网络层参数作用LSTM 第一层64单元return_sequencesTrue提取短期模式输出完整的序列给下一层Dropout0.2随机丢弃20%的连接抑制过拟合LSTM 第二层32单元把60步信息压缩成一个向量Dense1输出归一化后的次日开盘价input_shape(60, 1)里的60是窗口长度1是特征数量。第一层LSTM必须保留return_sequencesTrue否则输出到第二层时只剩最后一个时间步的隐状态中间59天的信息全被丢弃。loss用mse而不是mae原因是mse对大误差更敏感股价序列的极端值更需要被惩罚如果你更关心误差的绝对值换成mae也可以但要注意学习率可能要相应调低。3.4 Checkpoint保存机制cp-0002这个编号是什么训练过程中最怕断电和显存溢出。ModelCheckpoint回调把权重定期落盘这个包里出现的cp-0002.ckpt就是训练到第2轮时保存的一份完整快照cp_callback tf.keras.callbacks.ModelCheckpoint( filepathmodel/cp-{epoch:04d}.ckpt, save_weights_onlyFalse, save_freq5 ) history model.fit( X_train, y_train, batch_size32, epochs50, validation_data(X_val, y_val), callbacks[cp_callback] )save_weights_onlyFalse时每轮保存会生成三个文件.ckpt.index记录变量名和索引.ckpt.data-00000-of-00001存放实际权重数值checkpoint文件记录“最新一次保存的是哪个模型”。cp-0002说明这个快照对应第2轮迭代保存时间点比较早说明训练可能在中途被中断过也可能作者特意选了个早期检查点来避免过拟合。对于这种60天窗口的风险预测第2轮已经能学到基本的均值回归结构预测趋势不会差太远。注意save_freq5表示每5轮保存一次如果训练在中间某轮被手动停止最后一次保存的编号可能不是5的整数倍cp-0002就是这样出现的。理解了这个机制你就能判断checkpoint文件是不是完整。4. 加载模型做单步预测load_model.py与test.py实战解读模型训练完预测环节只剩三件事恢复checkpoint、构造输入窗口、把预测值画出来。这个包里的load_model.py和test.py核心逻辑就是在解决这三件事。4.1 从checkpoint恢复模型结构必须与训练时一致很多人拿到.ckpt文件第一反应是用tf.keras.models.load_model直接加载这是必翻车的写法。Keras的load_model只认.h5或SavedModel格式认不了Saver格式的ckpt。load_model.py里靠谱的恢复逻辑是这样的# 读取checkpoint文件拿到最新的模型路径 with open(model/checkpoint, r) as f: lines f.readlines() for line in lines: if model_checkpoint_path in line: ckpt_path line.strip().split()[1] print(ckpt_path) import tensorflow.compat.v1 as tf tf.disable_v2_behavior() def build_model(): model tf.keras.Sequential([ tf.keras.layers.LSTM(64, return_sequencesTrue, input_shape(60, 1)), tf.keras.layers.Dropout(0.2), tf.keras.layers.LSTM(32), tf.keras.layers.Dense(1) ]) return model tf_model build_model() saver tf.train.Saver() with tf.Session() as sess: saver.restore(sess, ckpt_path) # 到这里 sess 里已经是恢复好权重的完整计算图这段代码里有三个细节值得说。第一checkpoint文件里存的是相对路径如果模型目录被移动过saver.restore会找不到文件这时需要把路径改成绝对路径。第二模型结构必须和训练时完全一致LSTM单元数、层数、input_shape任何一个参数变更restore都会报变量名不匹配。包里没带训练脚本这块结构定义是唯一需要手工对齐的地方。第三如果用TensorFlow 2.x环境import tensorflow.compat.v1并关闭v2行为是为了让Saver机制可用如果环境本来就是TF1.x直接import tensorflow as tf即可。4.2 test.py的预测流程归一化、reshape、反归一化预测阶段最容易出错的是输入数据的预处理。训练时做了MinMax缩放预测时必须用同一个scaler做变换一个完整流程长这样import pickle import tushare as ts df ts.pro_bar( ts_code000001.SZ, start_date20250101, end_date20250314, adjqfq ).sort_values(date).reset_index(dropTrue) open_prices df[open].values dates pd.to_datetime(df[date]) with open(scaler.pkl, rb) as f: scaler pickle.load(f) scaled scaler.transform(open_prices.reshape(-1, 1)).flatten() last_window scaled[-60:].reshape(1, 60, 1) with tf.Session() as sess: saver.restore(sess, ckpt_path) pred_scaled sess.run(tf_model.output, feed_dict{tf_model.input: last_window}) pred_open scaler.inverse_transform(pred_scaled.reshape(-1, 1))[0][0] print(f预测下一交易日开盘价: {pred_open:.2f})这里做了四件事拉数据到预测日的前一天、加载训练时保存的scaler、把最近60天价格缩放到0到1并reshape成三维、预测后反缩放还原成真实价格。关键点是scaler只做transform不做fit_transform以及last_window取的是scaled数组的最后60个值保证窗口紧贴预测日。如果你用的是TensorFlow 2.x的Keras接口最后一行的sess.run等价于model.predict(last_window)数据准备逻辑完全一致。4.3 matplotlib可视化真实价与预测价的同轴呈现预测结果单独一个数字没有说服力要画出来对比。matplotlib的用法不复杂但有一个经典坑是中文乱码import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False real_window open_prices[-60:] plt.figure(figsize(10, 5)) plt.plot(dates[-60:], real_window, label真实开盘价, linewidth1.5) plt.axhline(pred_open, colorred, linestyle--, label预测下一个交易日开盘价) plt.title(股票价格走势预测真实 vs 预测) plt.legend() plt.grid(alpha0.3) plt.show()SimHei是Windows下的中文字体macOS要换成Arial Unicode MSLinux用Noto Sans CJK SC。如果不设置中文字体title和label会变成方框初学者经常在这上面卡半小时。画真实价格时取最近60天窗口就好别把几百天的序列全打出来图会糊成一团。预测值用一条水平虚线标注一眼就能看出模型认为明天会在哪个位置开盘。这里要降低预期第二天开盘价受隔夜外盘、消息面和集合竞价影响模型基于历史形态给出的只是条件期望。它适合做早盘参考不适合单独当交易信号。5. 避坑指南预测第二天开盘价的五个高频翻车现场以下五个问题都是重跑这个类型项目时几乎必然遇到过的我按现象、原因、解决的顺序列清楚相当于给动手复现的人一份后悔药。5.1 维度不匹配与归一化漂移坑一predict时忘了batch维度。现象执行model.predict(last_window)时报错“incompatible shapes”或“expected ndim3, found ndim2”。原因训练输入是三维张量(批次, 60, 1)预测时如果直接取scaled[-60:]得到的是二维数组(60,)或(60, 1)模型无法识别。解决固定用last_window scaled[-60:].reshape(1, 60, 1)把batch维度显式写出来。坑二归一化参数漂移导致预测值系统性偏低。现象预测值形态和真实曲线相似但从某一天起整体低一截差值基本固定。原因预测脚本里对包含测试数据的全量价格又重新调用了fit_transformscaler的min和max被新数据污染训练时的分布和预测时的分布不一致。解决训练结束后用pickle把scaler对象存成文件预测时只调transform不碰fit。这一点在4.2节代码里已经体现但很多人为了省事会在预测脚本里重写一遍scaler结果就是模型预测价值直接报废。5.2 checkpoint恢复失败与tushare数据口径坑三用load_model直接加载ckpt报错。现象tf.keras.models.load_model(model/cp-0002.ckpt)抛出异常提示格式不支持。原因ckpt是TensorFlow Saver的权重快照格式只包含变量值和索引不包含模型结构Keras的load_model要求完整模型文件。解决按4.1节方式先build_model重建结构再用saver.restore恢复权重。如果你想把ckpt转成h5方便后续加载可以在restore后用model.save(model.h5)保存一份以后就少一个坑。坑四训练用前复权、预测用未复权数据预测值彻底失真。现象在同一只股票上换时间段回测预测价格和真实价格相差10%以上并且训练集loss并不高。原因训练时用pro_bar(adjqfq)拿到前复权数据预测时换成pro.daily拿到未复权价格。除权除息造成的价格跳空在序列里形成一个人为的断点LSTM基于前复权的历史形态推断出的窗口跟未复权序列完全不在一个口径上。解决数据源全程统一训练和预测都用ts.pro_bar(adjqfq)并且把这一行写到脚本头部注释里防止过几天自己忘掉。5.3 准确率评估误区坑五拿分类准确率要求回归模型觉得模型“不稳定”。现象把预测值和第二天真实开盘价对比误差有时1%有时5%于是得出结论“模型不靠谱”。原因回归任务没有accuracy的概念“准确率还可以”指的是平均误差水平可控不是每次命中。第二天开盘价里包含大量历史序列之外的信息LSTM能做的只是从过去60天形态推断条件期望而不是预测确定值。解决改用MAE和MAPE衡量。指标公式10元价位股票的合理范围MAEmean(|真实值 - 预测值|)0.1 ~ 0.3 元MAPEMAE / mean(真实值) x 100%1% ~ 3%如果你的MAPE能稳定在3%以内这个模型就已经具备参考价值了。真实交易里1%的误差被手续费和滑点吃掉后剩不了多少这也是为什么这个包定位是学习项目而不是自动交易策略。6. 进阶用法改成滚动预测画出未来五天开盘价趋势线单步预测画出来是条水平虚线只能看到一个点。实际做趋势观察时我更习惯把单步预测扩展成滚动多步预测——每预测出下一天的开盘价就把它拼到窗口末尾丢掉最早的一天再预测下一天。def rolling_predict(model, scaler, last_window, days5): current last_window.copy() # 形状 (1, 60, 1) preds [] for _ in range(days): p model.predict(current)[0, 0] # 归一化后的次日开盘价 preds.append(p) p np.array([[[p]]]) current np.concatenate((current[:, 1:, :], p), axis1) return scaler.inverse_transform(np.array(preds).reshape(-1, 1)).flatten() future_prices rolling_predict(model, scaler, last_window, days5) print(future_prices)滚动预测有两个使用边界要记住。一是误差会沿着窗口累积第一天的预测最可信第三天以后基本只能看方向别当真值用二是它只适合画趋势线比如判断未来一周开盘价是抬升还是下探不适合直接拿来做机械交易信号。这类滚动逻辑在量化交易策略代码里经常作为因子研究的基础工具出现用它验证模型稳定性比单点预测更有说服力。验证方法也很简单拿最近60天数据做回看式验证假设今天是T用T-59到T的窗口预测T1逐日往前滚60天最后统计整段预测的MAE和MAPE。这个指标库要单独建一个脚本存起来每次改窗口长度或模型参数后都跑一遍看MAPE是变好还是变差比盯着某一天的预测值靠谱得多。从那以后我每次拿到别人的模型包第一件事都是打开checkpoint文件确认路径反推训练时的模型结构再写测试脚本这一套流程帮我少翻了很多次车。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表