Python深度学习股票预测:数据、模型与回测避坑指南
简介基于Python的深度学习与股票分析预测项目面向希望跨领域学习量化选股与深度学习技术的小白及进阶学习者可作为毕设、课程设计、大作业或工程实训选题。资源共20个文件含6个Python脚本、6张预测结果图、3个CSV数据文件及说明文档等压缩包4.25MB目录结构清晰便于按模块拆解学习。目前已有284人学习下载。项目提供数据下载、回测与策略三大核心模块通过baostock获取上证50、沪深300、中证500日线数据回测模块按调仓周期选股并以开盘价买卖、计算收益对比指数绘图策略部分覆盖价值因子BM、动量因子MF、换手率因子TR等传统选股思路同时实现CNN、LSTM、GRU、ResNet18/34/50多种深度学习预测模型并对各因子与模型效果给出对比注释便于理解不同方法在大市值股票上的适用性与局限。1. 这篇标题在讲什么用 Python 深度学习做股票预测真正能落地的边界在哪把“基于 Python 的深度学习与股票分析预测”拆开看其实就是两条技术线的交汇一条是 Python 生态里的数据获取、特征工程、模型训练与回测另一条是深度学习在时间序列上的拟合能力。大多数教程卡在“跑通一个 LSTM 模型”就结束了但真正干过这行的人都知道预测准确率只是入场券特征怎么构造、标签怎么打、数据会不会泄漏、回测为什么看着赚钱实盘却亏钱这些才是决定方案能不能用的关键。这篇笔记面向想用 Python 把深度学习落到股票分析上的工程师和量化初学者不预设你有金融背景但默认你写过 Python、跑过深度学习分类任务。读完你会知道环境怎么搭、数据怎么取、模型怎么选、回测怎么设计以及哪些地方属于“看着有道理、实盘就翻车”的玄学区。2. 数据先行把行情数据变成监督学习的输入2.1 取数先用免费数据源把日线、指数和分钟线拉齐做股票预测的第一步不是搭模型而是搞定数据。常见做法是用akshare或tushare这类免费 Python 库拉日线行情再配合pandas做清洗。下面这段代码以akshare为例拉取某只股票的日线数据并转成模型需要的格式import akshare as ak import pandas as pd # 拉取 A 股日线复权方式用前复权避免除权跳空 df ak.stock_zh_a_hist( symbol000001, perioddaily, start_date20150101, end_date20241231, adjustqfq, ) # 统一列名并保证时间升序 df.columns [date, open, close, high, low, volume, amount, amplitude, pct_change, change, turnover] df[date] pd.to_datetime(df[date]) df df.sort_values(date).reset_index(dropTrue) # 只需要模型真正用到的列降低 IO 和内存压力 df df[[date, open, close, high, low, volume, amount]] print(df.head())这段代码的核心是ak.stock_zh_a_hist这个接口symbol是股票代码adjustqfq表示前复权。前复权的重要性在于如果股票发生分红或送股不复权的价格会产生人为跳空模型会把这个跳空当成真实价格波动去学习属于典型的“数据噪音”。perioddaily拉的是日线如果后续要做更短周期的预测可以改成60、30等分钟线参数但分钟线的数据量和噪声都会明显增大建议新手先从日线开始。如果你更熟悉tushare它的pro_bar接口也能做类似的事但需要 token 权限积分门槛对新手不友好。我的建议是先用akshare把全流程跑通确认模型和回测逻辑没问题再切换到商业数据源。2.2 标签用未来收益还是未来方向有监督学习必须有标签。股票预测里最常见的标签有两种回归型标签未来 N 日收益率和分类型标签未来 N 日涨/跌/平。二选一的核心依据是你要模型输出“涨多少”还是只输出“涨的概率”。实盘中概率比具体数值稳定得多因为价格回归的噪声太大精确预测收益率的难度远高于预测方向。import numpy as np df[future_close] df[close].shift(-5) # 未来第 5 个交易日收盘价 df[ret_future] df[future_close] / df[close] - 1 # 分类标签未来 5 日收益 0.5% 记为 1 -0.5% 记为 0中间丢弃 df[label] np.where(df[ret_future] 0.005, 1, np.nan) df[label] np.where(df[ret_future] -0.005, 0, df[label]) df df.dropna(subset[label]) # 只保留到倒数第 5 行因为最后 5 行没有未来收益 df df.iloc[:-5].reset_index(dropTrue) print(df[label].value_counts())标签设计里最容易踩的坑是阈值的选择。0.5%这个值看起来随意但它关系到类别平衡如果你把阈值设得太低比如0.1%绝大多数样本会被标记为 1模型学到的只是“市场大部分时间在微涨”这个先验设得太高如3%正样本会变得极少模型训练不稳定。我通常先画出ret_future的分布再选择能让正负样本比例接近 1:1 到 1:1.5 的阈值这样模型才能学到真正的区分特征而不是靠样本不平衡蒙混过关。2.3 特征泄漏防线标准化的顺序决定回测真实性特征工程前先要解决数据泄漏问题。最典型的错误是用全部数据计算均值和标准差再标准化这等于让训练阶段看到了验证集和未来数据的分布信息。正确做法是只用训练集的统计量去标准化所有数据测试集和未来数据都必须复用训练集的参数。from sklearn.preprocessing import StandardScaler feature_cols [open, close, high, low, volume, amount] # 先按时间排序再按前 80% 作为训练段 train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] scaler StandardScaler() # 在训练集上 fit在测试集上只 transform scaler.fit(train_df[feature_cols]) df.loc[:train_size - 1, feature_cols] scaler.transform(train_df[feature_cols]) df.loc[train_size:, feature_cols] scaler.transform(test_df[feature_cols])这里必须强调特征工程里的技术指标计算也要遵循同样原则。比如计算过去 20 日的移动平均线这个指标本身只使用历史数据不会泄漏未来信息但如果用未来窗口的均值做平滑就会把未来信息带进来。凡是用到滚动窗口的特征必须确保窗口只向过去看。另外一个被很多人忽略的点是删除 NaN 行的时机要放在切分之后不能先 dropna 再切分否则训练集和测试集的边界会错位。3. 特征工程与模型选型从 LSTM 到梯度提升哪条路线适合你3.1 两类特征体系的差异表格特征 vs 序列特征股票数据可以建模成两种形态。第一种是表格形态每行是一天的特征包括价格、成交量、技术指标、截面排名等模型以 LightGBM、XGBoost 这类梯度提升树为主力第二种是序列形态把连续 N 天的数据叠成一个三维张量交给 LSTM、GRU 这类循环神经网络处理。很多人一看到“深度学习”就默认选 LSTM但实际项目中梯度提升树往往表现不差甚至在中小样本上更稳。原因在于股票数据信噪比极低深度学习模型的容量大容易把噪声也拟合进去树模型对特征交互的建模更直接且不需要精细调整序列长度和网络结构。我通常的做法是两条路线都跑一遍用同一套回测框架对比而不是凭直觉押注某一种。如果最终目标是部署上线可以先看 LightGBM 的结果它训练快、特征重要性可以直接输出对数据要求低。LSTM 的优势在于能处理变长序列和复杂的时序依赖但代价是训练时间长、超参数敏感、复现困难。下面给出两种模型的训练骨架。3.2 用 LightGBM 快速建立大盘预测基线import lightgbm as lgb from sklearn.metrics import accuracy_score # 假设 df 已经包含 label 和原始特征列 features [close, volume, amount, ret_5, ma5, ma20, vol_ratio] # 切分时按时间不打乱顺序 train_x df.iloc[:train_size][features] train_y df.iloc[:train_size][label] test_x df.iloc[train_size:][features] test_y df.iloc[train_size:][label] model lgb.LGBMClassifier( n_estimators500, learning_rate0.05, num_leaves31, max_depth5, subsample0.8, colsample_bytree0.8, random_state42, ) model.fit( train_x, train_y, eval_set[(test_x, test_y)], eval_metricbinary_logloss, callbacks[lgb.early_stopping(50)], ) pred model.predict(test_x) print(accuracy:, accuracy_score(test_y, pred)) print(feature importance:, list(zip(features, model.feature_importances_)))这段代码的价值在于快速验证特征有效性。early_stopping(50)表示验证集损失连续 50 轮不下降就提前终止防止过拟合。subsample和colsample_bytree是随机采样比例能提升模型泛化能力。如果连 LightGBM 都跑不出明显超过 50% 的方向准确率那大概率是特征或标签有问题这时候花更多时间调 LSTM 参数只会浪费时间。特征工程部分ret_5是过去 5 日收益ma5、ma20是均线vol_ratio是当日成交量与 20 日均量的比值。这些特征的计算必须严格只用历史数据实现时用rolling(window).mean()即可注意shift(1)防止用到当日收盘后的信息。3.3 LSTM 训练用滑窗构造三维序列checkpoint 保命LSTM 的输入要求是(样本数, 时间步长, 特征数)。时间步长的选择直接影响模型容量步长太短学不到中期趋势太长容易记住太多噪声。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping def build_sequences(data, label, lookback20): X, y [], [] for i in range(lookback, len(data) - 5): X.append(data[i - lookback:i]) # 过去 lookback 天 y.append(label[i 5]) # 未来第 5 天标签 return np.array(X), np.array(y) X_train, y_train build_sequences(train_df[features].values, train_df[label].values) X_test, y_test build_sequences(test_df[features].values, test_df[label].values) model Sequential([ LSTM(64, return_sequencesTrue, input_shape(X_train.shape[1], X_train.shape[2])), Dropout(0.3), LSTM(32, return_sequencesFalse), Dropout(0.3), Dense(16, activationrelu), Dense(1, activationsigmoid), ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) checkpoint ModelCheckpoint(best_lstm.h5, monitorval_accuracy, save_best_onlyTrue, verbose0) early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) model.fit(X_train, y_train, epochs50, batch_size128, validation_data(X_test, y_test), callbacks[checkpoint, early_stop], verbose1)这里有个容易困惑的地方特征标准化必须在构造序列之前完成否则序列数据里如果混入不同量纲的特征LSTM 前期收敛会非常慢。lookback20大约是 A 股一个交易月是一个合理起点。第一层 LSTM 设置return_sequencesTrue是为了把整个时间步的信息传给第二层 LSTM如果只有一层 LSTM则return_sequencesFalse就行。关于随机种子TensorFlow 的复现需要同时设置random.seed、np.random.seed和tf.random.set_seed缺一个都会导致结果漂移。Keras 的层内部初始化如果用了 GPU还可能受 cuDNN 影响通常需要在安装时禁用tf.config.optimizer.set_experimental_options({auto_mixed_precision: False})或在环境变量里设置TF_DETERMINISTIC_OPS1。我的经验是不要追求 100% 复现只要保证同一个随机种子下主结论稳定就行。4. 训练与验证回测是一项防过拟合工程不是按回车看曲线4.1 时间序列切分随机 KFold 是灾难分类任务常用的train_test_split默认随机打乱样本这在股票预测里是致命的因为它会把 2015 年的数据混进训练集、2023 年的数据混进测试集模型本质上是在做“跨年份的信息泄露”。时间序列问题必须用TimeSeriesSplit或手动按时间边界切分。from sklearn.model_selection import TimeSeriesSplit tscv TimeSeriesSplit(n_splits4) for fold, (train_idx, val_idx) in enumerate(tscv.split(df)): train_fold df.iloc[train_idx] val_fold df.iloc[val_idx] print(ffold {fold}: train {train_fold[date].min()} - {train_fold[date].max()}, fval {val_fold[date].min()} - {val_fold[date].max()})TimeSeriesSplit的特点是每一折的训练集永远是验证集之前的数据不允许未来数据混入。但在做交叉验证时要注意它的默认gap0意味着训练集和验证集之间没有间隔如果特征里包含 N 日移动平均线训练集最后几天的特征窗口实际上延伸到了验证集范围这样一个样本就同时出现在训练集和验证集的“特征上下文”里。解决方法是设置gap至少大于特征最大回看长度。4.2 滚动前推回测模拟实盘中最真实的“模型漂移”静态切分只能证明模型在某个时间节点有效但实盘是滚动着向前走的。你应该用滚动前推walk-forward方式回测即每训练一次预测未来一段然后加入新数据再训练一次。这种方式能暴露模型在不同市场环境下的稳定性代价是训练次数成倍增加。def walk_forward_predict(df, feature_cols, train_days1000, test_days50): preds, dates [], [] for start in range(0, len(df) - train_days - test_days, test_days): tr df.iloc[start: start train_days] te df.iloc[start train_days: start train_days test_days] scaler StandardScaler().fit(tr[feature_cols]) tr_x scaler.transform(tr[feature_cols]) te_x scaler.transform(te[feature_cols]) lgb_model lgb.LGBMClassifier(n_estimators200, learning_rate0.05, random_state42) lgb_model.fit(tr_x, tr[label]) preds.extend(lgb_model.predict_proba(te_x)[:, 1]) dates.extend(te[date].tolist()) return pd.DataFrame({date: dates, prob: preds})这段代码的核心是test_days步长它决定了模型更新的频率。实盘中test_days设 20一个月比较合理因为市场环境变化以月为单位设太小会导致频繁重训练工程成本高收益却很有限。滚动回测最大的陷阱是模型在某个时段表现特别好而在另一个时段大幅亏损平均下来净值曲线“看起来还行”但实际最大回撤已经大到不可接受。4.3 评估指标准确率之外必须看最大回撤和年化波动股票预测中单纯看准确率会被误导。假设模型预测准确率 55%但如果它只在上涨行情中预测准确在下跌行情中连续预测错误实际执行的策略依然可能大幅亏损。建议至少计算四个指标方向准确率、年化收益率、最大回撤、夏普比率。方向准确率可以直接用sklearn.metrics.accuracy_score计算但年化收益率和最大回撤需要结合仓位假设来算。下面给出一个简化版交易模拟假设每天按模型预测信号持有或不持有指数def backtest_returns(df, signal_col, daily_ret_col, fee0.0003): df df.copy() df[strategy_ret] df[signal_col].shift(1) * (df[daily_ret_col] - fee) df[strategy_nav] (1 df[strategy_ret]).cumprod() df[benchmark_nav] (1 df[daily_ret_col]).cumprod() total_ret df[strategy_nav].iloc[-1] - 1 rolling_max df[strategy_nav].cummax() max_drawdown ((df[strategy_nav] - rolling_max) / rolling_max).min() annual_sharpe df[strategy_ret].mean() / (df[strategy_ret].std() 1e-8) * np.sqrt(252) return total_ret, max_drawdown, annual_sharpe这里有个关键细节signal_col.shift(1)表示信号只从第二天开始生效目的是模拟“当天收盘后出信号、次日开盘执行”的实盘节奏。如果不做这个 shift等于你用当天的数据预测当天的涨跌并且当天就交易这在实盘里是做不到的因为预测结果出来时行情已经收盘。这个 shift 是回测框架里最容易忽略却影响巨大的一个步骤。5. 避坑清单这些年我在股票预测上踩过的六个实打实的坑5.1 标签泄漏的隐蔽形式未来序列被标准化吞掉现象模型在验证集上方向准确率高达 70%一上实盘就反向亏损。原因标准化时用了全部数据的均值和方差或者特征工程里的滚动均值用了未来窗口。这类泄漏不会在回测中直接报错而是表现为“训练集的分布被未来的统计量污染”。解决严格在滚动窗口内计算所有特征标准化只用训练集fit。每次构建新训练集时检查标准化器是否只基于该训练集拟合而不是全局拟合后复用。5.2 数据漂移同一种特征规则在不同年份完全不同现象2019 年训练的模型在 2020 年回测表现优秀但在 2021 年突然失效。原因市场的波动结构、成交量特征和因子有效性都在变化。深度学习模型容量越大越容易把某一时间段内的特定规律记住导致跨时间的泛化能力反而更差。解决回测分段展示净值分别统计牛市段和熊市段的准确率。如果模型只在单边行情有效那就应该在策略中加入“环境开关”比如用大盘均线判定市场状态只在特定状态下启用模型。5.3 类别不平衡的“伪高准确率”现象模型准确率 88%看起来碾压基准但预测结果永远是“不变”。原因如果将标签的阈值设得过高正样本极少模型学会全部预测为 0 就能拿到高准确率。这是分类任务最常见的陷阱。解决观察混淆矩阵计算召回率和精确率或者改用predict_proba的输出阈值调参。在类别不平衡明显的情况下给少数类加权重是常见的修正手段。5.4 随机性失控同样的代码两次跑结果不同现象同一个 Jupyter Notebook两次运行结果完全不一致有时候模型差异大到影响结论。原因PyTorch、TensorFlow 的随机种子没有固定GPU 并行计算的浮点运算是非确定性的。解决设置random.seed(42)、np.random.seed(42)、tf.random.set_seed(42)并关闭 TensorFlow 的 GPU 自动调优。对于 PyTorch还需要设置torch.manual_seed和torch.backends.cudnn.deterministicTrue。如果你的训练时间预算紧张建议只固定全局种子不追求逐比特复现。5.5 训练数据泄露到“未来”时间特征被误解现象加入了“年份”或“月份”作为特征后模型在验证集上表现异常好。原因年份特征实际上让模型记住了某个特定年份的行情规律而不是真正的预测信号。模型学到的是“2017 年涨、2018 年跌”这种伪规律。解决删除所有从日期中直接派生的特征比如年、月、星期。如果一定要加入周期性信息使用正弦编码表示“是一年中的第几天”但要注意模型对这种特征极其敏感容易把周期性当成绝对规律。5.6 数据量不足时强行上深度学习现象只用了一只股票 3 年的日线数据约 700 个样本直接训练 LSTM验证集损失一路飙升。原因深度学习在小样本上过拟合极快LSTM 尤其如此。股票逐日数据本身就不是大样本问题。解决优先扩大数据范围例如同时使用多只股票的横截面数据或者从日线升到分钟线。如果只能获得少量日线数据建议直接用 LightGBM 而不是深度学习。先问自己“数据量够不够”再决定模型类别。6. 概率阈值与滑窗集成把模型输出变成可执行仓位训练完模型只是第一步真正让策略稳定的是交易决策层。我一般会额外做两步一是把模型输出的概率转成仓位而不是简单的 0/1二是用滑窗集成多个模型预测减少单一模型在某一段行情上的随机波动。from scipy.stats import mode def ensemble_probs(models, X): # 多个模型分别预测概率然后取均值或众数 prob_list [m.predict_proba(X)[:, 1] for m in models] return np.mean(prob_list, axis0) def signal_to_position(prob, low0.45, high0.55): # 概率低于 low 做空或空仓高于 high 做多 return np.where(prob high, 1.0, np.where(prob low, -1.0, 0.0))滑窗集成的思路很直白每次回测滚动开始时不只训练一个模型而是用最近 3 个不同时间窗口分别训练再对最新预测取平均。这个做法对树模型和 LSTM 都有效代价是训练耗时增加约一倍但胜率稳定性提升明显。low和high是仓位阈值设定为 0.45/0.55 意味着模型只有在信心较强时才下单避免频繁交易被手续费吃掉收益。我自己的习惯是把概率输出落成一个 Excel 格式的每日信号表字段包含日期、收盘价、模型概率、建议仓位、次日涨跌。每月初重新看一眼过去 20 个信号的实际命中率如果连续 10 个信号中少于 4 个正确就把仓位阈值调得更保守而不是重新调模型参数。这种做法听起来不“智能”但实盘里比反复调 LSTM 超参数靠谱得多——模型漂移是常态敬畏不确定性才是常态。希望这些基于真实工程经历的思路能帮你少走几段弯路。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →