简介:面向金融数据挖掘与深度学习入门人群的实践型资源,演示如何用tushare抓取贵州茅台历史行情,并在TensorFlow 2.0框架下搭建RNN与LSTM模型预测开盘价。压缩包共5个文件,包含数据获取脚本、RNN预测脚本、LSTM预测脚本各一个,另附历史数据CSV和README说明文档,整体仅56KB,轻量便捷,适合快速阅读与二次修改。代码覆盖数据预处理、日期整数编码、开盘价归一化、模型构建、Adam优化训练及误差评估等环节,LSTM部分突出门控机制对长期依赖的捕捉,便于对比两类循环网络在股价序列上的表现。已有1912人学习下载,可直接替换股票代码与时间窗口,迁移到其他品种或指标,是理解时序预测与TensorFlow 2.0实战的实用参考。
1. 用 tushare 喂给 TensorFlow 2.0:RNN 与 LSTM 预测茅台开盘价,到底靠不靠谱
用 tushare 把贵州茅台的日线数据拉下来,再在 TensorFlow 2.0 里分别搭 RNN 和 LSTM 预测下一日开盘价——这个项目我在本地从头到尾跑通过一遍。先说结论:纯历史行情喂给循环模型,对短周期开盘价能学到一定规律,但指望它稳定战胜市场不太现实;这个项目真正的价值在于把「tushare 取数 → 序列预处理 → TensorFlow 2.0 建模 → 训练评估」这一整条链路完整走通了。适合刚开始接触时间序列预测、手头需要一份真实股票数据练手的开发者。项目自带 SH600519.csv 和两个 Python 脚本,照着跑就能出预测曲线。
2. 数据准备:把 tushare 的日线变成模型真正能吃的序列
2.1 拉取与清洗:token 配置、字段筛选和交易日索引
tushare 目前主推的是 pro 版本接口,先要注册拿到 token,再调用ts.pro_api()。日常拉日线用pro.daily()就够了,接口返回的是 DataFrame,字段比老版get_k_data全很多,包括开高低收、昨收、涨跌幅、成交量、成交额。老接口虽然不用积分,但数据字段少、维护状态不明确,不推荐在这个项目里用。
import tushare as ts import pandas as pd ts.set_token('你的tushare_token') pro = ts.pro_api() df = pro.daily(ts_code='600519.SH', start_date='20150101', end_date='20231231') df = df.sort_values('trade_date') # tushare 默认按日期倒序,必须升序 df.to_csv('SH600519.csv', index=False) print(df.head())这段代码做完三件事:配置 token、按 ts_code 拉取贵州茅台日线、按 trade_date 升序排列后落盘。ts_code的格式是「交易所代码.市场后缀」,上交所股票后缀是.SH。trade_date是YYYYMMDD字符串,直接字符串排序碰巧和日期排序一致,但要养成显式排序的习惯,避免后续和 datetime 混用时翻车。
这里有个新手最容易卡住的点:pro.daily()有积分门槛,注册后基础积分低,直接调用会报权限错误。常见做法是先用项目自带的 SH600519.csv 把流程跑通,再去 tushare 官网攒积分开接口权限。如果非要用接口,老版ts.get_k_data('600519')不需要积分,但字段少、接口稳定性一般,只适合拿来做数据下载的练习。
| 字段 | 含义 | 单位/说明 |
|---|---|---|
| open | 开盘价 | 元 |
| high / low | 最高价 / 最低价 | 元 |
| close | 收盘价 | 元 |
| pre_close | 前收盘价 | 元 |
| change / pct_chg | 涨跌额 / 涨跌幅 | 元 / 百分比 |
| vol | 成交量 | 手 |
| amount | 成交额 | 千元 |
vol单位是手,amount单位是千元,这个细节容易在后续做特征工程时算错量纲。tushare 的日线数据是「有交易日才有记录」,停牌日直接没有这一行。这里不要用自然日去补全,否则等于往序列里塞了大量重复价格,模型学到的是「价格不变」的假规律。后面避坑章节会展开讲。
2.2 归一化、滑动窗口与数据划分:为什么开盘价在第 0 列
模型输入不能直接用原始价格。茅台开盘价一千多块,成交量几十万手,数值量级差太多,直接丢进神经网络会让梯度被大数值特征主导。常规做法是 MinMaxScaler 把所有特征压到 0~1 区间。特别注意:scaler 只能 fit 在训练段上,不能先对全量数据 fit 再做切分,否则测试集的 min/max 已经参与了缩放,等价于让模型偷看了未来统计量。
import numpy as np import pandas as pd from sklearn.preprocessing import MinMaxScaler data = pd.read_csv('SH600519.csv') features = data[['open', 'high', 'low', 'close', 'vol', 'amount']].values # 先按时间顺序切分原始数据,再做归一化 split_idx = int(len(features) * 0.8) train_raw = features[:split_idx] test_raw = features[split_idx:] scaler = MinMaxScaler(feature_range=(0, 1)) scaler.fit(train_raw) # 只在训练段上学习 min/max train_scaled = scaler.transform(train_raw) test_scaled = scaler.transform(test_raw) def make_sequences(data, lookback=30): X, y = [], [] for i in range(lookback, len(data)): X.append(data[i-lookback:i]) # 过去 lookback 个交易日 y.append(data[i, 0]) # 预测第 i 日的开盘价,open 在第 0 列 return np.array(X), np.array(y) X_train, y_train = make_sequences(train_scaled) X_test, y_test = make_sequences(test_scaled) print(X_train.shape, y_train.shape)这里有两个关键选择。第一,特征列顺序固定为[open, high, low, close, vol, amount],所以 open 在索引 0,后续取data[i, 0]就是第 i 日的开盘价。第二,lookback=30表示用过去 30 个交易日预测下一个交易日,对应大约一个半月的交易天数,是股票序列预测里比较常见的窗口长度。窗口太短(如 5)模型只能看到短期波动,窗口太长(如 120)样本量急剧下降且容易过拟合。
划分比例 8:2 是按行切分,不是按日期切分。代码里split_idx取总行数的 80%,前 80% 做训练,后 20% 做验证。这里绝对不能随机 shuffle,时间序列一旦打乱,模型等于既看了过去又看了未来,训练出来的 loss 低得没有意义。
提示:做完这一步,检查一下
X_train.shape。正常结果是(样本数, 30, 6),三个维度分别对应样本数、时间步数、特征数。如果看到的是二维,说明滑窗逻辑有问题,后面喂给 LSTM 一定会报维度不匹配。
3. 上手 TensorFlow 2.0:SimpleRNN 与 LSTM 两个模型对比实现
3.1 RNN 基线:一层 SimpleRNN 加 Dropout,先把流程跑通
RNN 循环神经网络的特点是每个时间步的隐藏状态会往后传递,理论上能捕捉序列中的前后依赖。但标准 RNN 对长序列存在梯度消失问题,30 个时间步不算特别长,用它做基线模型是合理的起步选择。项目里的 rnn_stock.py 用 Keras Sequential 搭模型,几行代码就能出来。
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import SimpleRNN, Dense, Dropout model_rnn = Sequential([ SimpleRNN(64, activation='tanh', return_sequences=False, input_shape=(30, 6)), Dropout(0.2), Dense(1) ]) model_rnn.compile(optimizer='adam', loss='mse', metrics=['mae']) model_rnn.summary()SimpleRNN的units=64是隐藏状态维度,64 起步是常见做法:32 偏小、128 训练慢。activation='tanh'是循环层的标准激活函数,不需要额外纠结。return_sequences=False表示只返回最后一个时间步的输出,因为最终目标是预测一个具体的开盘价数值,不需要保存完整的时间步序列。input_shape=(30, 6)对应前面滑窗构造的 30 个时间步、6 个特征。
Dropout(0.2)在循环层输出之后加,用来压过拟合。股票序列噪声大,RNN 参数量不多但同样容易记住训练集上的随机波动。loss='mse'是回归问题的默认选择,metrics=['mae']是为了看平均绝对误差,数值更直观。整个 SimpleRNN 单层 64 单元的参数量不到一万,训练速度非常快,CPU 上几十秒就能跑完一个 epoch。
这个模型最大的意义是作为基线。不要指望它给出多惊艳的预测,而是拿它的 loss 和后面 LSTM 的结果做对比,验证 LSTM 的门控机制到底有没有带来实质提升。如果 RNN 和 LSTM 结果几乎一样,说明问题不在模型结构,而在数据或滑窗构造上。
3.2 LSTM 翻版:双层的门控机制更适合价格序列,但参数量翻倍
LSTM 通过输入门、遗忘门、输出门控制信息保留和丢弃,能有效缓解梯度消失。股票价格序列里既有长期趋势也有短期波动,LSTM 理论上比 SimpleRNN 更适合这类数据。项目里的 LSTM_stock.py 采用了双层 LSTM 结构,第一层返回完整时间步序列给第二层继续提取特征。
from tensorflow.keras.layers import LSTM model_lstm = Sequential([ LSTM(64, return_sequences=True, input_shape=(30, 6)), Dropout(0.2), LSTM(32, return_sequences=False), Dense(1) ]) model_lstm.compile(optimizer='adam', loss='mse', metrics=['mae']) model_lstm.summary()第一层LSTM(64, return_sequences=True)的return_sequences=True是双层 LSTM 的关键:必须输出所有时间步的隐藏状态,第二层才能继续按序列处理。第二层LSTM(32, return_sequences=False)只输出最后一步,直接接全连接层输出预测价格。参数量上,单层 LSTM 因为内部有四个门控结构,大约是同样 units 的 SimpleRNN 的四倍。双层 64+32 的话,总参数量在三万左右,训练速度明显比 RNN 慢,但也没有到不能接受的程度。
训练时加上 EarlyStopping 和模型保存,是这类项目的标准配套。归一化后的 MSE 在 0.001~0.003 区间算是正常水平,对应真实价格误差大约在几十元。如果验证集 loss 始终降不下去,先回去检查数据,别急着调网络结构。
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint callbacks = [ EarlyStopping(monitor='val_loss', patience=10, restore_best_weights=True), ModelCheckpoint('best_lstm.h5', monitor='val_loss', save_best_only=True) ] history = model_lstm.fit( X_train, y_train, epochs=50, batch_size=32, validation_data=(X_test, y_test), callbacks=callbacks, verbose=1 )epochs=50搭配patience=10的意思是:最多训练 50 轮,但连续 10 轮验证集 loss 不下降就提前停,并恢复最优权重。这个组合能在过拟合之前「踩刹车」。batch_size=32是常规选择,数据量不大时不用刻意调。validation_data直接用之前按时间切出来的后 20% 数据,每轮训练结束都能看到验证集指标。
| 模型 | 循环层 | 参数量级 | 训练速度 | 适用判断 |
|---|---|---|---|---|
| SimpleRNN | 64 units × 1 | 约 1 万 | 快 | 流程基线,验证链路通不通 |
| LSTM | 64 + 32 units × 2 | 约 3 万 | 中等 | 正式预测,捕捉中长期依赖 |
4. 避坑:股票时间序列预测里我踩过的五个坑
第一个坑:把日期当成特征喂给模型。现象:把 trade_date 归一化后拼进特征矩阵,训练时 loss 一直居高不下,预测曲线几乎是一条水平线。原因:日期编码和价格之间没有可学习的因果关系,归一化后的日期接近单调递增,模型只学到了一个无意义的时间趋势项。解决:只保留 ohlcv 和成交量特征,日期只做索引不做输入。摘要里提到的「将日期转换为连续的整数编码」在早期版本里常见,但这个思路现在基本被放弃,建议直接丢弃日期列。
第二个坑:Keras 报输入维度不匹配。现象:执行 fit 时抛出expected ndim=3, found ndim=2。原因:DataFrame 是二维结构,直接传进去等于把每个样本当成一个独立时间步,根本没有构造(样本数, 时间步, 特征数)的三维张量。解决:先跑make_sequences滑窗函数,再打印X.shape确认是三维,最后才进模型。
第三个坑:预测曲线整体延迟一天,看起来贴合度极高但其实是假象。现象:把预测值和真实开盘价画在一起,两条曲线几乎重合,但整体向右平移了一天,MAE 还异常低。原因:模型其实在拟合「昨日收盘价 → 今日开盘价」的跳空关系,本质上是拿前一天的信息做无脑预测,并没有学到多日模式。解决:用「上一交易日收盘价当作今日开盘价的预测值」做一个无脑基准线,如果模型跑不赢这条基准线,说明学到的东西没有增量价值。
第四个坑:全样本归一化造成数据泄漏。现象:训练时 loss 很漂亮,但拿模型去预测「只有过去数据」的真实场景,误差明显变大。原因:MinMaxScaler 在全部数据上 fit,测试段的 min/max 参与了缩放,相当于模型间接看到了未来统计量。解决:严格按照「先按时间切分 → 只用训练段 fit scaler → 再 transform 训练和测试段」的顺序走,这也是第 2 章代码里特意先切分再 fit 的原因。
第五个坑:停牌日补全导致序列失真。现象:用 pandas 按自然日resample补全停牌日之后,模型预测值长期不变。原因:补全操作把停牌日填充成前值,序列里出现大量重复价格,模型学到「价格永远不变」的假规律。解决:保持 tushare 返回的交易日序列,不要用自然日对齐。构造滑窗时直接按行索引走,有交易记录才算一个时间步。
注意:
pro.daily()的积分权限报错不属于数据处理问题,但极其常见。现象是调用接口时提示没有权限或访问受限,原因是 tushare 对 daily 接口有积分门槛,解决方式是先用项目自带 CSV 文件跑通流程,再去 tushare 官网攒积分或换旧版get_k_data接口应急。
5. 验证与预测:回测里看模型能不能信,以及怎么预测下一天
5.1 反归一化与滚动预测:补零列的小技巧
模型输出的是 0~1 区间的归一化预测值,直接看没有意义。反归一化时有个常见坑:scaler.inverse_transform需要完整特征维度,但模型只预测了 open 一个字段。方式不是只传一列,而是构造一个 6 列的零矩阵,把预测值塞进第 0 列,再取反变换后的第 0 列。
pred_scaled = model_lstm.predict(X_test) dummy = np.zeros((len(pred_scaled), 6)) dummy[:, 0] = pred_scaled[:, 0] pred_open = scaler.inverse_transform(dummy)[:, 0] true_open = scaler.inverse_transform(test_scaled[30:])[:, 0]这里test_scaled[30:]对应测试集中第一个滑窗的起始位置,要和pred_scaled的行数对齐。更严格的验证方式是滚动预测:每预测完一天,把真实观测值追加到窗口末尾,再预测下一天,模拟真实的逐日预测过程。一次性预测全部测试集会有信息优势,滚动预测的结果更接近实际使用情况。
5.2 误差指标与使用边界
评估时别只盯 MSE,加一个 MAPE 更直观。计算公式是mean(abs((真实值-预测值)/真实值)) * 100%,茅台开盘价在 1500~2000 元区间时,MAPE 在 2% 以内已经算是这个任务里不错的水平。我习惯把预测值、真实值和「上一日收盘价」三条线画在一张图里,肉眼判断模型到底有没有跑赢无脑基准。
预测未来一天只是把最后 30 个交易日的数据滑窗出来,丢进模型拿到归一化结果再反变换,逻辑和测试集完全一致。但有一点要认清:模型只学了历史价格模式,突发事件、政策变化、市场情绪这些信息在行情数据里没有体现,预测结果只能当参考,不能当交易信号。从那以后我每次跑这类预测项目,都强制走一遍「严格时间划分 + 无未来函数 + 无脑基准对比」三步检查,过滤掉了很多看似漂亮实则没用的模型。希望帮到你。
本文还有配套的精品资源,点击获取