简介:这是一套基于TensorFlow实现的股票价格走势预测示例,面向对金融数据挖掘、时间序列预测感兴趣的Python开发者和量化分析初学者。项目通过tushare接口获取股票历史行情,并对缺失值、日期索引等做必要处理;利用pandas完成数据清洗、格式转换与特征整理,再以TensorFlow搭建并训练模型,当前版本可预测第二天的开盘价;matplotlib则用于在同一幅图中叠加真实价格曲线与预测价格曲线,便于直观对比模型效果。压缩包内共5个文件,包含2个Python脚本(模型加载与测试)以及训练好的checkpoint、index等模型权重文件,整体仅456KB,轻量易用;资源结构简洁,下载后即可对照演练。目前已有2142人学习浏览,是一份适合入门的小型实战参考;读者可借此熟悉tushare数据获取、pandas数据处理、TensorFlow模型保存与加载、结果可视化等完整流程,即使对深度学习不熟悉,也能通过示例脚本快速上手。
1. 从压缩包到可信预测:python股票价格走势预测到底在预测什么
拿到“基于python实现股票价格走势预测.zip”这类项目,解压后通常是十几个脚本加一段 README。跑通很容易,但把预测曲线和真实行情叠在一起看,你会发现训练集上拟合得越漂亮的东西,实盘往往越没用。这个方向真正的难点从来不是模型,而是数据流水线和评估方式:用什么数据、构造什么标签、怎么切分时间序列、怎么防止未来的信息混进训练集。
这篇笔记的目标是帮你从零搭一套能跑、能回测、能判断“到底有没有用”的股票价格走势预测流程。新手跟着步骤能把环境配好、把代码跑通;熟手重点看第 3、4、5 章,滚动前推的评估方式和常见的未来函数泄漏问题,是这类项目里最容易翻车的地方。
2. 数据准备与特征工程:先把价格序列变成模型能学的东西
2.1 拉取数据:用 yfinance 抓日线并处理复权
股票预测项目的第一步是拿到干净的历史行情。常见做法是用 yfinance 拉 Yahoo Finance 的日线数据,A 股代码带后缀,例如贵州茅台是600519.SS,美股直接写AAPL。先解决环境问题:python 安装完成后,在终端里用pip install yfinance pandas numpy把依赖装上,用 VSCode 配置好 python 环境后直接在集成终端里跑,省去路径和解释器切换的麻烦。
import yfinance as yf import pandas as pd # 拉取贵州茅台最近5年的日线数据 df = yf.download("600519.SS", start="2019-01-01", end="2024-12-31") # 只保留需要的列,并删除缺失值 df = df[["Open", "High", "Low", "Close", "Volume"]].dropna() df.columns = ["open", "high", "low", "close", "volume"] # 复权处理:yfinance 的 auto_adjust=True 默认会返回复权后的价格 df = yf.download("600519.SS", start="2019-01-01", end="2024-12-31", auto_adjust=True) df = df[["Open", "High", "Low", "Close", "Volume"]].dropna() df.columns = ["open", "high", "low", "close", "volume"] print(df.tail())逻辑说明:auto_adjust=True会把历史价格按分红、拆股做后复权调整,否则遇到除权除息日,K 线上会出现一根突兀的“暴跌”,模型会把它当成真实的价格跳变来学习,预测结果自然失真。dropna()删除停牌或数据缺失的行,避免空值进入后续计算。
参数说明:start和end可按需调整,但建议至少取 2000 根以上日线,否则模型没有足够的样本学习不同市场状态。如果只拉一年数据,训练集里基本只覆盖一种行情(单边上涨或单边下跌),模型泛化能力会很差。
2.2 构造标签:用未来 N 日收益率代替“涨/跌”分类
很多入门代码把任务定义成二分类——明天涨输出 1,明天跌输出 0。这个做法有个问题:涨 0.1% 和涨 5% 都被算作“涨”,模型学不到幅度信息,预测结果也没法直接指导仓位。更合理的做法是预测未来 N 日的收益率,让模型输出一个连续值。
import numpy as np df["ret_future"] = df["close"].shift(-5) / df["close"] - 1.0 # 删除最后5行(它们没有未来5日的收益作为标签) df = df.dropna() # 特征列与标签列分离 feature_cols = ["open", "high", "low", "close", "volume"] X = df[feature_cols].values y = df["ret_future"].values print(f"样本数: {len(df)},特征数: {len(feature_cols)}") print(f"标签范围: {y.min():.4f} ~ {y.max():.4f}")逻辑说明:shift(-5)把未来的收盘价往当前行移动,ret_future表示“从今天收盘持有 5 天后的收益率”。用连续收益率做标签,模型既能学到方向也能学到幅度,回测时可以直接按预测收益率排序选股。
参数说明:5代表预测周期,可以改成1(次日)或20(月线级别)。周期越长,标签噪声越小,但训练样本的有效数量也会下降。这里有一个折中经验:做日频策略用 3 到 5 天,做低频配置用 20 天。
2.3 技术指标特征:用 talib 生成 RSI、MACD、ATR
只用 OHLCV 五个原始特征,模型很难学到趋势和波动状态。常见做法是用 TA-Lib 生成技术指标作为额外特征。安装 TA-Lib 前需要先装 C 库,Windows 用户可以直接pip install TA-Lib,Linux 用户建议用apt install libta-lib-dev避免编译报错。
import talib # 在原始数据基础上追加指标特征 df["rsi"] = talib.RSI(df["close"], timeperiod=14) df["macd"], df["macd_signal"], df["macd_hist"] = talib.MACD( df["close"], fastperiod=12, slowperiod=26, signalperiod=9 ) df["atr"] = talib.ATR(df["high"], df["low"], df["close"], timeperiod=14) # 删除指标计算产生的 NaN df = df.dropna() feature_cols = ["open", "high", "low", "close", "volume", "rsi", "macd", "macd_signal", "macd_hist", "atr"] X = df[feature_cols].values y = df["ret_future"].values print(df[feature_cols].head())逻辑说明:RSI 刻画超买超卖状态,MACD 表示趋势动能,ATR 表示波动幅度。这三个指标覆盖了趋势、动量、波动三个维度,比单纯堆二十个指标更不容易过拟合。
参数说明:指标参数使用默认值(14、12/26/9)是常见做法,不需要反复调优。原因在于技术指标本身就是对价格的降维变换,参数微调带来的边际收益远小于数据泄漏带来的破坏。特征加入后,用 python 的np.isnan()检查一遍,防止指标计算在前几行产生缺失值。
3. 模型选型与训练参数:为什么优先用 LSTM 而不是直接上 Transformer
3.1 股票数据适合用 LSTM 的三个理由
股票价格走势预测的建模对象是时间序列,样本之间存在顺序依赖,这正是循环神经网络(RNN)和 LSTM 的设计场景。LSTM 通过门控机制保留长期信息,能学到“连续几天放量上涨后可能回调”这类跨时间步的模式。相比之下,普通全连接网络把每一天当成独立样本,完全丢掉了顺序关系,预测效果通常更差。
Transformer 近年在时间序列领域表现不错,但在股票数据上有一个现实问题:股票日线数据量通常只有几千条,而 Transformer 是数据饥渴型模型,样本不足时训练不稳定,很容易过拟合。常见的做法是先用 LSTM 搭基线,如果后期数据量扩充到分钟级(几十万条),再考虑换 Transformer 做对比实验。
| 模型 | 所需数据量 | 时序依赖建模 | 训练稳定性 | 适用场景 |
|---|---|---|---|---|
| 全连接网络 | 少 | 无 | 高 | 特征工程极强时的基线 |
| LSTM | 中 | 有 | 中 | 日线级别的常规预测任务 |
| Transformer | 多 | 有 | 低 | 分钟级高频、样本量充足时 |
3.2 构造三维输入:把数据切成“时间步 × 特征”
LSTM 的输入形状是三维的:(样本数, 时间步, 特征数)。时间步表示模型每次回溯多少天的历史,特征数对应上一章构造的特征列数量。切片操作是这里的核心,用 python 数组切片把一条长序列切成长度为lookback的窗口序列。
def create_sequences(X, y, lookback=20): X_seq, y_seq = [], [] for i in range(len(X) - lookback): X_seq.append(X[i:i + lookback]) y_seq.append(y[i + lookback]) return np.array(X_seq), np.array(y_seq) lookback = 20 X_seq, y_seq = create_sequences(X, y, lookback) # 按时间顺序切分训练集/验证集/测试集,比例 7:1.5:1.5 split1 = int(len(X_seq) * 0.7) split2 = int(len(X_seq) * 0.85) X_train, X_val, X_test = X_seq[:split1], X_seq[split1:split2], X_seq[split2:] y_train, y_val, y_test = y_seq[:split1], y_seq[split1:split2], y_seq[split2:] print(f"训练集形状: {X_train.shape}, 验证集形状: {X_val.shape}, 测试集形状: {X_test.shape}")逻辑说明:切分时必须按照时间顺序,不能像普通分类任务那样随机打乱,否则未来的数据会混进训练集,造成未来函数泄漏。create_sequences里的X[i:i + lookback]取的是第 i 天到第 i+20 天的数据作为特征,y[i + lookback]对应的是第 i+20 天之后的收益,严格保持“用过去预测未来”。
参数说明:lookback=20对应一个月交易日,覆盖一个相对完整的短期趋势周期。切分比例 7:1.5:1.5 是时间序列任务里常用的分配方式,测试集保留最近 15% 的样本,模拟“用过去训练、在最近行情上验证”的真实场景。
3.3 归一化与模型定义:把特征缩放到同一量级
股票特征里,价格是几十到几千,成交量是几百万,RSI 是 0 到 100,直接喂给模型会让优化器把注意力全放在量级大的特征上。常见做法是用标准缩放(StandardScaler),把每个特征变成均值为 0、方差为 1 的分布。
from sklearn.preprocessing import StandardScaler # 特征归一化:注意只在训练集上 fit,防止验证集信息泄漏到训练过程 scaler = StandardScaler() # X 是三维的,需要先 reshape 成二维再 fit n_samples, n_steps, n_features = X_train.shape X_train_2d = X_train.reshape(-1, n_features) X_train_scaled = scaler.fit_transform(X_train_2d).reshape(n_samples, n_steps, n_features) # 验证集和测试集使用训练集的 scaler 做 transform X_val_scaled = scaler.transform( X_val.reshape(-1, n_features) ).reshape(X_val.shape[0], X_val.shape[1], n_features) X_test_scaled = scaler.transform( X_test.reshape(-1, n_features) ).reshape(X_test.shape[0], X_test.shape[1], n_features)逻辑说明:fit_transform只用在训练集上,验证集和测试集直接调用transform,这一点是股票预测项目里最容易忽略的泄漏源。如果对整个数据集先归一化再切分,训练过程中相当于偷看了未来数据的均值和方法,回测指标会虚高,实盘立刻现出原形。
参数说明:StandardScaler适用于大多数量纲差异大的特征场景。如果特征包含大量离群点,也可以换RobustScaler,它对极端值的抗性更强,但绝大多数日线数据用 StandardScaler 就够了。
3.4 搭建 LSTM 回归模型:输出层不加激活函数
标签是连续收益率,因此输出层应该是一个线性单元,而不是 softmax 或 sigmoid。模型结构用两层 LSTM 加一个全连接层,Dropout 放在 LSTM 层之间,用来抑制过拟合。
import tensorflow as tf model = tf.keras.Sequential([ tf.keras.layers.LSTM(64, return_sequences=True, input_shape=(lookback, n_features)), tf.keras.layers.Dropout(0.2), tf.keras.layers.LSTM(32, return_sequences=False), tf.keras.layers.Dropout(0.2), tf.keras.layers.Dense(16, activation="relu"), tf.keras.layers.Dense(1) # 回归头,输出预测收益率 ]) model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss="mse", metrics=["mae"] ) model.summary()逻辑说明:第一层 LSTM 设置return_sequences=True,把完整序列传给第二层;第二层return_sequences=False只输出最后一个时间步的隐藏状态。Dense 层用 ReLU 提取非线性特征,最后一层不加激活函数,让模型自由输出任意实数,对应预测收益率可正可负的特性。
参数说明:learning_rate=0.001是 Adam 优化器在序列预测任务里的常用起点。如果训练损失震荡不下降,可以降到 0.0003;如果收敛太慢,可以提到 0.003,但要配合早停机制防止发散。batch_size默认 32,数据量小(少于 5000 条)时建议调到 16,让梯度更新更稳定。
4. 滚动预测与回测评估:把模型放到真实交易节奏里检验
4.1 滚动前推:训练集随时间向前移动
静态切分有个问题:模型只训练一次,然后去预测最近几个月的行情。但市场状态会变,模型需要不断吸收新数据。常见的做法是滚动前推(walk-forward validation):每次用最近 3 年的数据训练,预测下一个月,然后整个窗口向前推进一个月,重复这个流程。
def walk_forward_predict(df, feature_cols, lookback=20, train_months=36, test_months=1): all_preds, all_trues = [], [] step = 21 # 约一个月交易天数 start_idx = lookback + 252 * train_months # 一年约252个交易日 i = start_idx while i + step <= len(df): train_df = df.iloc[i - 252 * train_months - lookback:i] test_df = df.iloc[i:i + step] # 构造序列并归一化 X_tr, y_tr = create_sequences(train_df[feature_cols].values, train_df["ret_future"].values, lookback) X_te, y_te = create_sequences(test_df[feature_cols].values, test_df["ret_future"].values, lookback) scaler = StandardScaler() X_tr_2d = X_tr.reshape(-1, len(feature_cols)) X_tr_scaled = scaler.fit_transform(X_tr_2d).reshape(X_tr.shape) X_te_scaled = scaler.transform(X_te.reshape(-1, len(feature_cols))).reshape(X_te.shape) # 训练模型 model = tf.keras.Sequential([ tf.keras.layers.LSTM(32, return_sequences=True, input_shape=(lookback, len(feature_cols))), tf.keras.layers.Dropout(0.2), tf.keras.layers.LSTM(16, return_sequences=False), tf.keras.layers.Dense(1) ]) model.compile(optimizer="adam", loss="mse") # 每个窗口只训练 10 个 epoch,减少验证集过拟合风险 model.fit(X_tr_scaled, y_tr, epochs=10, batch_size=16, verbose=0) preds = model.predict(X_te_scaled, verbose=0).flatten() all_preds.extend(preds) all_trues.extend(y_te) i += step return np.array(all_preds), np.array(all_trues)逻辑说明:循环中的train_df截取是从当前窗口往前 3 年的数据,test_df是接下来 1 个月的行情。每次滚动都重新训练模型,模拟真实迭代节奏。这里有意把模型结构设成单层 32 单元的轻量版本,因为每个窗口只训练 10 个 epoch,模型结构太重反而学不完。
参数说明:train_months=36表示用 3 年数据训练,test_months=1表示每次预测 1 个月,step=21是 1 个月的平均交易日数。这套参数组合在大多数日线数据上运行时间可控(约几分钟),同时保证测试结果覆盖多种市场状态。
4.2 评估指标:方向准确率、IC、累计收益曲线
跑完滚动预测后,需要一组能说明问题的指标。方向准确率是最直观的——预测收益率和真实收益率同号的比例。信息系数(IC)衡量预测值与实际值的秩相关性,比方向准确率更细腻。累计收益曲线能直观展示策略净值走势,但要同时画出基准线对照。
from scipy.stats import spearmanr # 方向准确率 direction_acc = np.mean((all_preds > 0) == (all_trues > 0)) print(f"方向准确率: {direction_acc:.4f}") # IC:预测值与真实值的 Spearman 秩相关系数 ic, p_value = spearmanr(all_preds, all_trues) print(f"IC: {ic:.4f}, p_value: {p_value:.4f}") # 模拟累计收益:按预测值符号做多/空,每期收益等于真实收益率 strategy_returns = np.where(all_preds > 0, all_trues, 0) # 只做多 cumulative = np.cumprod(1 + strategy_returns) print(f"累计净值: {cumulative[-1]:.4f}")逻辑说明:np.where(all_preds > 0, all_trues, 0)模拟的是“预测涨就持有,预测跌就空仓”的最简单策略。真实交易里还要考虑手续费和滑点,但这里的目的只是评估预测信号本身是否有效,不急着加交易成本。
参数说明:IC 的绝对值大于 0.05 就可以认为有一定预测力,大于 0.1 属于较强的信号。注意 p_value 要小于 0.05 才说明相关性显著,否则几千次预测里出现一点正相关可能是噪声。方向准确率在 55% 以上配合正 IC,才有继续优化的价值。
4.3 和朴素基线对比:随机预测与“明天等于今天”
任何预测模型都必须跑赢一个最简单的基线:用当天的收益率作为明天的预测值(动量策略)。如果 LSTM 连这个基线都跑不赢,说明模型没有学到超越“价格延续性”的信息。
# 基线:预测值等于当天收益率 baseline_preds = np.roll(all_trues, 1) baseline_preds[0] = 0 baseline_direction_acc = np.mean((baseline_preds > 0) == (all_trues > 0)) baseline_ic, _ = spearmanr(baseline_preds, all_trues) print(f"基线方向准确率: {baseline_direction_acc:.4f}") print(f"基线IC: {baseline_ic:.4f}") print(f"LSTM方向准确率: {direction_acc:.4f}") print(f"LSTM IC: {ic:.4f}")逻辑说明:np.roll把真实收益率的序列向后移一位,模拟“用今天的涨跌预测明天的涨跌”这个朴素思路。如果 LSTM 的指标和基线接近,大概率是因为模型学到了“价格延续性”这个本来就存在于序列中的统计规律,而不是发现了新的交易信号。
参数说明:这个对比必须在同一个测试集上进行,即使用相同的all_trues。如果基线的方向准确率已经达到 53%,而 LSTM 只有 54%,差距在统计上通常不显著,不能说明模型有效。
5. 避坑:股票价格预测里最常见的 5 个翻车场景
5.1 训练损失很低,实盘回测却稳定亏损
现象:模型在训练集上损失降到极低,验证集曲线拟合得非常漂亮,但放到最近几个月的真实行情里回测,收益曲线一路向下。
原因:数据泄漏。最常见的是归一化时对全样本做了fit_transform,或者构造标签时用了包含未来信息的特征(比如把未来 N 天的均线当特征),模型本质上在“抄答案”。
解决:严格按时间顺序切分,归一化只在训练集上fit,测试集只transform。构造特征时逐个检查是否用到了当前时间点之后的数据。
5.2 换一只股票就完全失效
现象:在贵州茅台数据上训练好的模型,直接拿去预测五粮液或招商银行,方向准确率掉到 50% 以下,和抛硬币一样。
原因:每一只股票的波动特性、交易量分布、趋势特征都不同,模型把个股特有的规律学成了通用规律。全市场统一训练的模型在个股上表现通常也更差。
解决:不要跨股票直接迁移模型。如果目标是多只股票,就把多只股票的数据合并训练,并加入股票代码作为特征;如果是单股票预测,每只股票从头训练,超参数用验证集重选。
5.3 预测值总是比真实行情滞后一天
现象:把预测曲线和真实价格曲线叠在一起看,预测走势整体向右平移了一天,看着非常像,但现实中根本没法交易。
原因:标签构造时可能无意中用了当天的收盘价预测当天的收益率(即未来函数),或者特征里包含当天收盘后才能确定的值却被当成盘前已知信息。滞后看起来像“预测准了”,其实是模型在复现昨天的价格。
解决:检查标签列是否用了shift(-N)做对齐,特征列是否全部来自 T 日及之前。严格测试:把预测时点假设为 T 日收盘,预测 T+1 日到 T+N 日的收益,所有特征都必须是 T 日收盘前能拿到的值。
5.4 训练时损失值剧烈震荡,模型不收敛
现象:训练过程中 loss 忽高忽低,验证集指标波动很大,训练一轮后模型预测结果全是同一个常数。
原因:学习率过大、序列长度过长、数据中极端值未处理。股票数据偶有涨停或跌停,收益率分布尾部极厚,一次极端值就能把梯度的方向带偏。
解决:把学习率降到 0.0001 到 0.0003,lookback降到 20 以内,收益率先做截断(例如把超过 ±20% 的收益率压缩到 ±20%),再检查归一化后的特征是否有极端离群点。
5.5 除权除息日出现异常暴跌
现象:回测净值曲线在某一天突然向下跳空,但真实行情并没有出现这么大的跌幅。
原因:没有使用复权价格。股票分红除息后,历史价格会向下调整,如果直接用原始价格,除息日当天会出现一根人为制造的下跌 K 线,模型会把这种事件当成真实下跌信号。
解决:拉数据时设置auto_adjust=True,或者拉取后手动用复权因子计算。检查复权结果的标准是:复权后价格序列中不应该出现分红日前后的大幅跳空缺口。
6. 从预测到可用:验证预测质量的三个硬标准
预测模型跑完后,先别急着和真实行情对比。我通常会做三个验证,任何一个不合格就退回前面的步骤重新检查。
第一个验证是收益分布分层测试。把预测值按大小分成 5 组(例如按分位数分为 Q1 到 Q5),分别计算每组真实收益率的均值。如果预测有效,Q5(预测收益最高的一组)的真实平均收益应该显著高于 Q1(预测收益最低的一组),并且呈近似单调递增。这个测试比单纯看方向准确率更能反映预测的排序能力,因为交易信号的核心不是猜对涨跌,而是能区分“更可能涨”和“不太可能涨”的股票。
第二个验证是预测残差的自相关检验。对预测值和真实值的差值(残差)做自相关分析,如果残差在某些滞后阶数上表现出显著相关性,说明模型遗漏了重要的时序模式,还有改进空间。这里可以用一个简单的 python 循环计算滞后 1 到 10 天的残差自相关系数,超过 ±0.1 就要检查特征集是否缺了对应的因子。
第三个验证是模拟交易的稳健性测试。把模型预测信号用在不同的标的、不同的起始时间上重复回测,观察收益曲线是否只在某一段特定行情里有效。我把这个测试叫做“退后一步看曲线”——放大到 3 年尺度,如果曲线中的大部分收益来自某一段单边上涨行情,那模型学到的大概率是趋势延续性而不是真正的选股能力。
落地上我还有一个死规矩:每个预测模型必须同时保存训练日期、数据范围和当时的验证指标。因为市场状态会漂移,三个月前的有效模型很可能已经失效,没有这些元数据你就不知道什么时候该重训、该用哪版本的去实盘。我习惯把这个信息直接写到模型文件名里,比如lstm_600519_20250101_acc0.56.h5,下次看到就能判断这个模型的“保质期”是否已过。
这个方向的底线是:预测模型只是信号生成器,不是印钞机。把评估做扎实,比把模型做复杂更重要。希望帮到你。
本文还有配套的精品资源,点击获取