ARTICLE DETAIL

资讯详情

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

tushare+TensorFlow实战:LSTM股票开盘价预测全流程解析

tushare+TensorFlow实战:LSTM股票开盘价预测全流程解析 简介一份面向金融时序预测学习者的完整示例整合tushare数据接口与TensorFlow 2.0以贵州茅台历史行情为样本实现RNN和LSTM对开盘价的预测。资源包含数据获取、预处理、建模、训练与评估全流程代码。压缩包共5个文件3个Python脚本tushare数据下载脚本、RNN训练脚本、LSTM训练脚本、1个CSV格式的茅台历史数据文件SH600519.csv和1个README说明文档整体大小56KB结构清晰便于对照学习。已有1912人学习下载适合正在入门深度学习时序预测、或希望用真实金融数据演练TensorFlow 2.0模型的开发者参考。通过这份代码可以快速理解日期编码与开盘价归一化的处理方式掌握构建循环层、设置损失函数和优化器的关键步骤同时README与脚本注释提供了tushare接口调用和RNN/LSTM单元选择的思路方便在此基础上扩展其他股票或预测目标。1. 股票开盘价预测为什么选 tushare TensorFlow 2.0 这套组合股票开盘价预测是时序预测里最常被拿出来验证 RNN 和 LSTM 的“标准练习”也是新手最容易跑出个曲线、却说不清模型到底有没有用的场景。这篇笔记的路径很直接用 tushare 拉贵州茅台的日线行情按交易日顺序把数据切成带时间窗口的序列样本再用 TensorFlow 2.0 分别实现一个 SimpleRNN 和一个 LSTM预测“下一个交易日开盘价”最后把验证方法、结果对比、常见翻车点一次性讲透。它能解决的是“网上代码一堆但没有一份能从头到尾跑通”的困境。适合正在做毕设、刚入门时序预测、以及想在正式上量化之前先用一张真实股票表评估 RNN/LSTM 能力边界的人。2. 用 tushare 拉贵州茅台日线数据token 配置、数据清洗与节奏控制预测开盘价的第一步不是建模而是先把数据拿对、存好、看清字段。tushare 的 pro 接口是目前国内散户和研究者拿 A 股日线最顺手的通道贵州茅台的代码是600519.SH调用一次就能拿到上市以来的日线。这里我会把从 token 配置到数据落盘、检查缺失的完整流程写出来顺手说明几个不写就会踩坑的细节。2.1 注册 token 与首次调用tushare pro 与老版本不同所有接口都需要先持有 token。你在 tushare 网站注册后在个人主页能看到一串 token 字符串把它填到下面的代码里。注意 token 是账号维度的权限标识积分不够时部分接口只能访问最近一段时间的数据这点我们在 2.3 里再展开。import tushare as ts import pandas as pd TOKEN 你的token字符串 ts.set_token(TOKEN) pro ts.pro_api() df pro.daily( ts_code600519.SH, start_date20150101, end_date20231231 ) df df.sort_values(trade_date).reset_index(dropTrue) print(df.head())说明两点。ts.set_token()是全局生效的之后每次ts.pro_api()都会带上该 tokenpro.daily()返回的字段包括ts_code、trade_date、open、high、low、close、pre_close、change、pct_chg、vol、amount。trade_date是形如20230101的字符串必须手动按它排序因为 tushare 不同接口返回顺序并不一致不排序会导致后面的滑窗样本错位。排序后重设索引是为了让 DataFrame 的行与时间顺序严格对应。从 2015 年到 2023 年贵州茅台大概有两千个交易日左右。这个量级对于单变量序列模型偏少但做一次完整的流程验证完全够用。如果你拿到的数据行数明显比预期少很多先不要怀疑代码去看 tushare 官网的积分文档大概率是权限不足。2.2 把日线数据落成本地 CSV数据拉下来之后第一时间落盘。这样后续调模型不必反复请求接口也避免了在实验过程中因为网络波动或限流导致数据丢失。df.to_csv(gzmoutai_daily.csv, indexFalse) df_check pd.read_csv(gzmoutai_daily.csv) print(df_check.shape) print(df_check.dtypes)indexFalse是关键不要把手动生成的RangeIndex写进 CSV否则后面读回来会多一列无意义索引。读回后建议打印shape和dtypestrade_date应为object数值列应为float64。如果你看到amount是整型或trade_date混入了时间格式就先在这里修好不要拖到建模阶段。实际项目里我会再检查一遍股票代码是否对600519.SH中的.SH表示上交所A 股代码与交易所错配是 tushare 新手最高频的报错来源之一。拿到的数据如果只有 K 线没有成交量后面第 3 章的多特征方案就做不了所以落盘时至少确认vol和amount两列非空。2.3 数据检查与缺失处理日线数据最常见的“缺失”不是 NaN而是停牌导致的交易日空缺。茅台的流动性极好极少停牌但遇到重大事项停牌时那一周在 DataFrame 里就是整行消失而不是 NaN。这种缺失不要用fillna去补补出来的价格没有真实交易含义反而会污染滑窗样本。df[trade_date] pd.to_datetime(df[trade_date]) df df[df[close].notna()].reset_index(dropTrue) print(df.isnull().sum()) print(df.groupby(df[trade_date].dt.year).size())这里把trade_date转成datetime是为了后续画图、对齐日期时方便。notna()过滤仅用于处理极少数字段缺失但注意如果你发现某一行close缺失而open有值这行数据就应整行删除因为滑窗样本要求每行特征完整。按年份统计样本数可以快速发现某个年份是否异常少比如某一年只有几十条就要回到 tushare 检查是不是权限只开放了部分区间。提示日线数据只按交易日排列不要用pd.date_range()按自然日补全周末和节假日。RNN/LSTM 的输入序列代表“相邻交易日之间”的关系春节、国庆造成的间隔不需要用日期填充来模拟。做完这一步手里就是一份干净的、按时间升序排列的贵州茅台日线表。下一步把它变成模型能吃的序列。3. 把历史行情变成 RNN / LSTM 能学的序列样本模型不认 DataFrame只认张量。把历史行情喂给 TensorFlow 之前必须完成两件事确定特征集合以及把“过去的 10 个交易日的多个特征”与“下一日开盘价”组成监督学习样本。这章写的每一步错了后面模型再漂亮都白搭。3.1 特征选择预测开盘价看哪些字段预测 T 日开盘价输入只能用 T 日之前已知的信息。也就是说T 日的high、low、close、vol、amount都不能进特征因为它们在开盘之后才发生。很多入门代码把当日全部字段一股脑塞进去训练时 loss 极低一实盘就完蛋因为那是用未来预测当下。我一般保留以下字段作为特征字段含义是否可用于预测 T 日开盘openT-1 日及之前的开盘价可用历史值highT-1 日及之前的最高价可用lowT-1 日及之前的最低价可用closeT-1 日及之前的收盘价可用volT-1 日及之前的成交量可用amountT-1 日及之前的成交额可用pre_closeT-1 日及之前的昨收价可用目标是open列在 T 日的值。这样构造出的每个样本输入窗口的最后一根 K 线一定是 T-1 日而预测目标是 T 日不存在时间穿越。vol和amount有时候被新手忽略但贵州茅台这类高价股的日内波动与成交量、成交额相关性明显加上它们能让模型多一个判断流动性的维度。不要只拿open一个序列做“单变量预测自己”那样模型很容易退化成“上一个值复制”。3.2 归一化为什么不能直接喂原始价格贵州茅台的价格在几百到两千多元之间波动成交量从几万手到几十万手量纲差异巨大。直接把原始数值喂给 LSTM梯度很容易被大数值特征主导出现 loss 不降或 NaN。归一化不是可选步骤是必须步骤。用sklearn.preprocessing.MinMaxScaler把特征压缩到 [0,1] 区间。这里有一个绝对不要踩的坑必须先按时间切分训练集/验证集/测试集再对训练集做fit_transform对验证集和测试集只做transform。如果先对全量数据统一 fit 再做切分等价于让模型在训练时就“偷看”了测试阶段的价格区间和分布测试集评估就失去意义。from sklearn.preprocessing import MinMaxScaler import numpy as np feature_cols [open, high, low, close, vol, amount] target_col open features df[feature_cols].values.astype(np.float32) target df[target_col].values.astype(np.float32) # 按时间顺序切分70% 训练15% 验证15% 测试 train_end int(len(features) * 0.70) val_end int(len(features) * 0.85) train_feat features[:train_end] val_feat features[train_end:val_end] test_feat features[val_end:] train_tar target[:train_end] val_tar target[train_end:val_end] test_tar target[val_end:] scaler_x MinMaxScaler() scaler_y MinMaxScaler() train_feat_scaled scaler_x.fit_transform(train_feat) val_feat_scaled scaler_x.transform(val_feat) test_feat_scaled scaler_x.transform(test_feat) train_tar_scaled scaler_y.fit_transform(train_tar.reshape(-1, 1)).ravel() val_tar_scaled scaler_y.transform(val_tar.reshape(-1, 1)).ravel() test_tar_scaled scaler_y.transform(test_tar.reshape(-1, 1)).ravel()train_tar.reshape(-1, 1)必须做MinMaxScaler要求输入是二维。ravel()则是把预测目标还原成一维数组方便后面喂给model.fit。验证集和测试集的目标值也用训练集拟合出的scaler_y做变换这里只 transform不重新 fit否则预测结果与训练目标不在同一尺度上逆变换会出错。注意测试集的价格一旦超出训练集的最高价transform后会出现大于 1 的值这是正常现象。它说明测试段出现了训练期内没见过的价格位置模型被迫外推这正是金融时序最难的部分后面第 5 章会专门讲这个坑。3.3 滑动窗口构造监督学习样本RNN/LSTM 的输入形状是(样本数, 时间步长, 特征数)。时间步长就是窗口大小window我默认取 10代表用最近 10 个交易日的数据预测下一交易日开盘。窗口大小是模型超参数里最值得试的窗口太短模型记不住一周以上的形态窗口太长样本数量减少且老数据可能对当前行情不再有参考价值。def create_sequences(features, target, dates, window10): X, y, d [], [], [] for i in range(window, len(features)): X.append(features[i - window:i]) y.append(target[i]) d.append(dates[i]) return np.array(X), np.array(y), d train_dates df[trade_date].values[:train_end] val_dates df[trade_date].values[train_end:val_end] test_dates df[trade_date].values[val_end:] X_train, y_train, d_train create_sequences(train_feat_scaled, train_tar_scaled, train_dates) X_val, y_val, d_val create_sequences(val_feat_scaled, val_tar_scaled, val_dates) X_test, y_test, d_test create_sequences(test_feat_scaled, test_tar_scaled, test_dates) print(X_train.shape, y_train.shape)create_sequences的循环从window开始保证每个样本都有完整的 10 行历史特征。features[i - window:i]取的是第i-10到i-1行的数据target[i]是第i行的开盘价所以样本的时间边界是干净的输入截止于 T-1 日输出是 T 日没有跨样本信息泄漏。返回的d保存对应预测目标的交易日后面第 6 章做日期对齐评估时会用到。如果看到X_train.shape输出为(样本数, 10, 6)就对了。第一个维度是样本数第二个是窗口长度第三个是特征维度。如果你的 shape 是二维说明漏了窗口维度模型 fit 时会报错。窗口大小 10 只是起点。实操中我会把 5、10、20 都跑一遍观察验证集 loss 变化。窗口 20 会让样本数量减少约 10%但能覆盖一个月内的价格形态对茅台这种慢牛票未必比 10 差。总之窗口、特征集、归一化方式三者组合起来就构成“数据处理方案”每改一个都要重新走一遍训练别只调模型不调数据。4. TensorFlow 2.0 实现 RNN 和 LSTM模型定义、训练与结果对比数据处理完毕进入模型部分。TensorFlow 2.0 的 Keras 接口把 RNN 和 LSTM 封装得足够简单但简单不等于不会翻车。这章给两个可以直接跑的模型一个SimpleRNN做基线一个LSTM做主力然后讲清楚训练参数为什么这么设、失败时看哪个指标。4.1 SimpleRNN 基线模型SimpleRNN结构最简单t 时刻的隐层只依赖当前输入和上一个隐层没有门控机制。它的问题在于难以捕捉长期依赖理论上跑茅台这种有长期趋势的序列容易衰减。但正因为它简单非常适合当基线如果 LSTM 连 SimpleRNN 都打不过说明问题大概率出在数据侧而不是模型侧。import tensorflow as tf def build_rnn(window, n_features): model tf.keras.Sequential([ tf.keras.layers.SimpleRNN( units32, activationtanh, return_sequencesFalse, input_shape(window, n_features) ), tf.keras.layers.Dense(1) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossmse, metrics[mae] ) return model model_rnn build_rnn(X_train.shape[1], X_train.shape[2]) model_rnn.summary()units32是隐层维度对 6 个特征的输入来说 32 是安全的起步值。activationtanh是 SimpleRNN 的默认首选换成 relu 更容易梯度爆炸。return_sequencesFalse表示只在最后一个时间步输出一个隐状态因为我们最终只需要一个标量预测值不需要输出全序列。Dense(1)把隐状态压成开盘价预测。学习率 0.001 是 Adam 的常规默认值金融序列数据量小不建议一上来就用 0.01。model.summary()能帮你确认参数量。一个SimpleRNN(32)输入维度 6参数量约(6 32) * 32 32 1248非常轻量。如果看到参数量是好几万说明 units 设大了对小样本数据很容易过拟合。4.2 LSTM 模型与参数调整LSTM 引入输入门、遗忘门、输出门能在更长的时间跨度里保留信息是处理股票日线这类数据更合理的选择。同样窗口下LSTM 参数大约是 SimpleRNN 的 4 倍所以需要一点正则化手段防止过拟合。def build_lstm(window, n_features): model tf.keras.Sequential([ tf.keras.layers.LSTM( units64, return_sequencesFalse, input_shape(window, n_features) ), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(16, activationrelu), tf.keras.layers.Dense(1) ]) model.compile( optimizertf.keras.optimizers.Adam(learning_rate0.001), lossmse, metrics[mae] ) return model model_lstm build_lstm(X_train.shape[1], X_train.shape[2])这里的units64比 SimpleRNN 的 32 大一倍因为 LSTM 内部结构更复杂需要更多容量来拟合波动。Dropout(0.2)作用于 LSTM 输出后的隐状态随机丢弃部分神经元响应防止模型把训练集的噪声背下来。Dense(16)加 relu 是一个隐层给模型一点非线性变换能力再接Dense(1)输出。如果你的样本量只有一千出头Dense(16)不是必须的删掉后模型更稳健可以对比跑。训练时我会加EarlyStopping这是时序小样本里最重要的防过拟合手段。它监控验证集 loss连续多轮不下降就提前结束训练并恢复到最佳权重。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) history model_lstm.fit( X_train, y_train, validation_data(X_val, y_val), epochs60, batch_size32, callbacks[early_stop], verbose1 )epochs60是上限有了patience10实际不会傻傻跑满 60。batch_size32对茅台这个数据量是合适的样本只有一千多batch 太大容易让每个 epoch 的参数更新次数太少。restore_best_weightsTrue是后悔药训练结束时模型自动回到验证集 loss 最低的那一轮权重避免 last epoch 已经过拟合的脏模型被保存下来。训练完先看history.history[val_loss]是上升还是下降。如果验证 loss 从头到尾都在涨要么学习率太大要么数据没归一化回到第 3 章检查。如果训练 loss 很低、验证 loss 高出一大截Dropout 还没起作用把epochs降到 40 或增大patience对应的轮数阈值再试。4.3 训练后评估RMSE、方向准确率训练后不能只看 loss还要把预测值逆变换回真实价格用人能看懂的指标评估。from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate_model(model, X_test, y_test, scaler_y): y_pred model.predict(X_test) y_pred_real scaler_y.inverse_transform(y_pred.reshape(-1, 1)).ravel() y_test_real scaler_y.inverse_transform(y_test.reshape(-1, 1)).ravel() rmse np.sqrt(mean_squared_error(y_test_real, y_pred_real)) mae mean_absolute_error(y_test_real, y_pred_real) return y_pred_real, rmse, mae y_pred_rnn, rmse_rnn, mae_rnn evaluate_model(model_rnn, X_test, y_test, scaler_y) y_pred_lstm, rmse_lstm, mae_lstm evaluate_model(model_lstm, X_test, y_test, scaler_y) print(fRNN RMSE{rmse_rnn:.2f} MAE{mae_rnn:.2f}) print(fLSTM RMSE{rmse_lstm:.2f} MAE{mae_lstm:.2f})inverse_transform是必须的。如果直接拿 [0,1] 区间的预测值与真实价格比较RMSE 会小得离谱但它没有实际含义。RMSE受大误差样本影响大MAE反映平均误差水平两个一起看。对茅台这种单价 2000 元左右的股票MAE 在 30 元以内算模型学到了基本价格位置如果 MAE 超过 100 元基本等同于瞎猜去看预测曲线多半是整体偏差一个身位。只看 RMSE/MAE 还不够。开盘价预测的实际意义在于判断“今天开盘比昨天高还是低”所以要算方向准确率它的计算方式放在第 6 章那里才是判断模型是否有用的真正关卡。5. 避坑股票时序预测里最容易翻车的 5 个细节时序预测的坑大多数不在模型而在数据组织方式。下面 5 个问题是我跑股票数据时踩过的按“现象 → 原因 → 解决”来写每一条都值得你在跑通之后回头对照检查一遍。5.1 归一化放在切分之前测试集信息提前泄露现象训练集 loss 和测试集 loss 都很低RMSE 好到让人怀疑人生但拿到真实行情走势里预测曲线明显偏乐观或偏悲观。原因对全量数据先做了MinMaxScaler.fit_transform()再切分训练/测试。这一步让 scaler 在计算最小值和最大值时已经把测试集的价格范围纳入了训练数据的归一化尺度。模型在训练阶段的输入分布里就隐含着未来价格的位置测试集评估自然失真。解决严格按“先切分再 fit_transform 训练集对验证集和测试集只 transform”的顺序。这个顺序写在第 3 章代码里实战中要用一个统一的预处理函数封装避免临时改数据处理时手滑。5.2 shuffle 打乱顺序未来数据混进训练集现象验证集 loss 一路走低模型表现得“过于完美”但把训练好的模型拿到新的一段行情上预测误差立刻飙升。原因用train_test_split()之类的工具对时序样本做了随机划分。时序样本之间存在强时间相关性随机切分使得训练集里混入了比验证集时间更晚的样本验证集里也混入了训练集时段内的样本。模型在做预测时其实已经“见过”同一段行情的不同侧写评估变成开卷考试。解决一律按时间比例切分并且切分位置要在生成滑窗样本之前确定。代码里用前 70%、中间 15%、最后 15% 的切分方式就是为此设计禁止对序列样本做随机 shuffle 划分。5.3 没做前复权除权日前后出现假跳空现象模型在除权除息日附近预测误差突然拉大比如某一天股价从 2100 “跌”到 1600模型以为暴跌下一日开盘价预测也跟随大幅下挫。原因股票分红送股会导致价格在除权日被“人为”压低这个跳空不是市场行为而是公司财务行为。贵州茅台近年每年都有分红如果不做复权处理模型会把除权造成的价格缺口当作真实下跌信号学习。长周期数据里没做前复权的模型本质上是拿着一个被财务事件污染的价格序列在预测。解决使用前复权价格。在拉取数据时用pro_bar接口并指定adjqfq或获取复权因子后自行换算。前复权会让历史价格整体下调使价格序列在除权点前后连续模型看到的是一个干净的价格走势。5.4 只用 loss 判断好坏模型变成“照抄昨天”的移动复制现象测试集 RMSE 很好看预测曲线与真实曲线高度重合但仔细观察预测值比真实值晚一天——今天的预测价约等于昨天的真实开盘价。原因开盘价是一个连续变化不剧烈的序列当窗口内的价格基本稳定时模型的最优策略是“预测值等于最后一个已知值”因为这样能让均方误差最小。这是时序回归的固有陷阱模型没有学会涨跌逻辑只学会了平移复制但 loss 指标无法区分这两种情况。解决跑一个 naive 基线直接用“昨天的开盘价作为今天的预测值”计算它的 RMSE然后与 RNN/LSTM 的 RMSE 对比。如果模型只比 naive 好一点点说明模型没有抓住真正的价格变化规律还需要回到特征和窗口设计去找原因。5.5 测试集价格超出训练集范围归一化后出现大于 1 的值现象训练正常测试集预测却出现极端值比如预测开盘价涨到 3000 元逆变换后远超训练样本区间。原因MinMaxScaler的上下界是训练集里学到的。测试期一旦出现比训练期最高价更高的价格transform后该值就会超过 1进入模型从未见过的输入域。模型在边界外只能靠外推输出不稳定非常正常。解决评估时留意X_test的最大值是否大于 1如果大于 1说明行情已经脱离训练分布。此时要么扩大训练集覆盖更长周期要么改用StandardScaler做标准化后者不会把数值钳制在固定区间外推时稍微稳健一点。更重要的心态是预测创新高的价格段本身就是时序模型最难处理的部分不要指望模型能准确预判“突破”。6. 验证模型是否真的可用滚动回测与方向准确率这一章的定位是验收。做完模型并得到 RMSE 之后还需要回答一个问题这个模型对“开盘价涨跌方向”到底有没有判断力。我将方向准确率定义为今天开盘价相对昨天开盘价的真正涨跌方向与模型预测开盘价相对昨天开盘价的方向是否一致。def direction_accuracy(y_true_real, y_pred_real): y_true_diff np.diff(y_true_real, prependy_true_real[0]) y_pred_diff np.diff(y_pred_real, prependy_true_real[0]) return np.mean(np.sign(y_true_diff) np.sign(y_pred_diff)) acc_rnn direction_accuracy( scaler_y.inverse_transform(y_test.reshape(-1, 1)).ravel(), y_pred_rnn ) acc_lstm direction_accuracy( scaler_y.inverse_transform(y_test.reshape(-1, 1)).ravel(), y_pred_lstm ) print(fRNN 方向准确率: {acc_rnn:.2%}) print(fLSTM 方向准确率: {acc_lstm:.2%})算法里用prepend首日真实值来生成“前一日开盘价”序列这样第一天的方向差也能参与统计。方向准确率达到 55% 以上才说明模型的预测有初步参考价值如果只有 50% 上下和抛硬币没有区别那前面跑的模型只能当作习作不能用来指导交易。另一个更严格的验证是滚动回测从测试期第一个样本开始每次用窗口内真实数据预测次日开盘价预测完把次日真实值加入窗口再预测下一天。这和训练时的滑窗策略一致是模拟真实使用场景最接近的做法。茅台这类日线数据波动较慢滚动验证能暴露模型在连续多日预测中的误差积累情况。做这类实验多了我养成了一个习惯任何时序预测结果出炉后第一件事不是画图发朋友圈而是先跑 naive 基线和方向准确率这两项不过关直接回炉查数据处理。股票开盘价预测在模型层面很难玩出花真正拉开差距的永远是那几件不起眼的琐事复权、切分、归一化以及不对未来作弊。希望这个流程能帮你把 RNN/LSTM 这条路走顺。本文还有配套的精品资源点击获取
返回列表