
简介基于 Python 的深度学习与股票分析预测项目面向金融科技方向的初学者与进阶学习者适合用作毕业设计、课程设计或大作业。资源完整覆盖数据获取、策略编写、模型训练与回测评估等环节可帮助读者理解机器学习在量化选股中的应用。压缩包共 20 个文件包含 6 个 Python 源码文件、6 张预测结果图、3 个 CSV 行情数据文件以及 Markdown 说明文档和依赖清单整体仅 4.25MB目录结构便于按模块查阅。目前已有 284 人学习/下载可作为入门量化投资的参考工程。项目中利用 baostock 下载上证50、沪深300、中证500日线数据实现了基于 CNN、LSTM、ResNet 等深度模型的选股策略并借助回测模块按调仓周期、开盘价买卖计算收益与指数对比绘图此外还包含价值、动量、换手率等多种传统因子选股思路方便横向比较不同方法的优劣。1. 股票预测不是玄学但也不是跑通一个 LSTM 就能躺赚做股票分析预测的 Python 项目最容易掉进去的认知陷阱是把它当成“训练一个深度学习模型、看 loss 下降、然后拿预测结果去买股票”这么简单。真实情况是深度学习在这个领域扮演的是从历史行情中提取非线性模式的工具而不是能精准预言涨跌的“水晶球”。我见过太多人用 TensorFlow 跑通一个 LSTM回测曲线漂亮得离谱实盘一上来就连续止损——根源几乎都是数据泄露、未来函数和过拟合。这篇内容会把“基于 Python 的深度学习与股票分析预测”这件事拆成五个部分数据怎么准备、模型怎么选、回测怎么做、哪些坑必踩以及一个让模型真正有点用的验证技巧。适合手里有 Python 基础、想从零搭一套可复现的股票预测实验的工程师和量化爱好者我默认你熟悉 pandas、numpy能用 Keras 或者 PyTorch 跑通一个简单网络。2. 数据准备与特征工程先让历史行情变成模型能学会的样本2.1 行情数据的获取本地 CSV 与 Python 量化数据接口的取舍构建股票预测系统的第一步不是写模型而是解决数据来源问题。常见做法有三种用免费接口直接拉取、从财经网站下载 CSV、或自己用 Python 爬虫抓取。我建议团队协作或个人实验首选免费 Python 量化数据接口理由很简单复权处理、停牌过滤和板块分类这些脏活累活接口已经帮你处理掉大部分自己爬虫维护成本极高。以国内股票为例常见的数据源有 baostock、tushare、akshare。我主要在实验阶段用 baostock因为它不需要 token、注册即用这对快速验证特征工程逻辑非常重要。下面是拉取日线数据并保存为 CSV 的标准脚本import baostock as bs import pandas as pd # 登录 baostock免费接口无需 token lg bs.login() # 拉取 600519贵州茅台的日线数据 rs bs.query_history_k_data_plus( sh.600519, date,code,open,high,low,close,volume,amount,turn,pctChg, start_date2020-01-01, end_date2024-12-31, frequencyd, adjustflag2 # adjustflag2 表示前复权 ) rows [] while (rs.error_code 0) rs.next(): rows.append(rs.get_row_data()) df pd.DataFrame(rows, columnsrs.fields) bs.logout() # 转数值类型date 列转 datetime df[date] pd.to_datetime(df[date]) for col in [open, high, low, close, volume]: df[col] pd.to_numeric(df[col], errorscoerce) df.dropna(inplaceTrue) df.to_csv(stock_600519_daily.csv, indexFalse)这段脚本的逻辑是把接口返回的数据逐行装进列表再转成 DataFrame。adjustflag2是前复权参数必须设置否则遇到除权除息时价格会出现向下跳空模型会把这种“假摔”当成真实下跌信号预测结果参考价值大跌。turn是换手率它在后面的特征工程里比成交量更能反映资金活跃度。参数说明frequencyd指定日线pctChg是涨跌幅百分比amount是成交额。如果做分钟级预测可以把frequency改成m但分钟数据文件体积大、噪声强非必要不建议新手碰。数据落到本地 CSV 后后续特征工程就完全依赖 pandas 处理不再依赖网络连接。2.2 特征窗口与标签设计预测涨跌方向而不是预测价格很多初学者直接拿“明天收盘价 - 今天收盘价”当标签然后让模型回归预测价格这其实是让模型去拟合一个非平稳的随机游走过程基本等于让模型背圆周率。更合理的做法是用未来 N 日的收益率作为标签并且把任务定义成分类问题——预测未来 5 日是涨还是跌。这样既避免了价格序列的绝对数值带来的尺度问题又契合交易场景中“方向比幅度更重要”的经验。标签和特征窗口的构造代码import numpy as np import pandas as pd df pd.read_csv(stock_600519_daily.csv, parse_dates[date]) df.sort_values(date, inplaceTrue) df[ret_5d] df[close].shift(-5) / df[close] - 1 # 未来5日收益率 df[label] (df[ret_5d] 0).astype(int) # 涨1跌0 # 特征列包含动量、波动率、量价关系 df[mom_5] df[close] / df[close].shift(5) - 1 # 5日动量 df[mom_10] df[close] / df[close].shift(10) - 1 # 10日动量 df[vol_5] df[close].pct_change().rolling(5).std() # 5日波动率 df[vol_ratio] df[volume] / df[volume].rolling(20).mean() # 量比 df[amt_turn] df[turn] # 换手率原始值 # 删除含缺失值的行注意 shift 造成的头部 NaN feature_cols [mom_5, mom_10, vol_5, vol_ratio, amt_turn] data df.dropna(subsetfeature_cols [ret_5d]).reset_index(dropTrue)代码逻辑说明shift(-5)是把未来第 5 天的数据拉到今天的位置从而构造“未来 5 日收益率”。label用(ret_5d 0)转成 0/1这就是二分类任务的标签。特征方面动量因子mom_5和mom_10捕捉短中期趋势强度vol_5捕捉波动率聚集效应vol_ratio反映放量缩量状态。参数说明窗口期选择 5 日和 20 日对应一周和一个月的交易周期这是短线模型常用的组合。你也可以换成 10 日和 60 日做中长线版本但窗口越长有效样本越少训练集和测试集的时间跨度就要相应拉大。dropna(subset...)必须同时包含特征列和标签列否则模型会拿到NaN标签。这里有一个新手容易忽略的问题标签里包含了使用未来数据构造的收益所以切分训练集和测试集时绝对不能随机打乱必须按时间顺序切分。下面的章节专门讲这个问题。2.3 数据集划分按时间切分不要让未来数据穿越到训练集股票数据是典型的时间序列它的样本之间存在先后依赖关系。如果用train_test_split默认的随机划分相当于把未来某 5 天的数据混进训练集模型在评估时会“作弊”——它已经见过测试区间附近的价格走势了。正确做法是按日期排序后用前 80% 的时间段做训练后 20% 做测试并且测试集的起始日期要避开近期数据中尚未完整形成未来 5 日标签的尾部区间。train_end_date 2023-12-31 train_data data[data[date] train_end_date].reset_index(dropTrue) test_data data[data[date] train_end_date].reset_index(dropTrue) # 特征标准化用训练集的均值/标准差禁止用全量数据计算 from sklearn.preprocessing import StandardScaler scaler StandardScaler() X_train scaler.fit_transform(train_data[feature_cols]) X_test scaler.transform(test_data[feature_cols]) y_train train_data[label].values y_test test_data[label].values print(f训练样本数: {len(X_train)}, 测试样本数: {len(X_test)})这段代码最关键的地方在scaler.fit_transform(train_data[feature_cols])之后测试集只用transform也就是复用训练集的均值和标准差。如果对全量数据先做标准化再切分均值和标准差里就掺杂了未来信息这属于一种隐蔽的未来函数。股市数据的均值漂移本来就快一旦标准化参数包含未来区间测试集被训练集污染的问题会直接放大模型在测试集上的虚假表现。关于特征标准化的边界条件StandardScaler假设特征近似正态分布动量因子和换手率基本满足但波动率序列偏态较明显如果效果不理想可以改用RobustScaler用中位数和四分位距做尺度化对极端行情如暴涨暴跌日更稳健。3. 深度学习模型选型与训练为什么 LSTM 只是基线不是终点3.1 模型对比LSTM、GRU 与 MLP 在股票序列上的适用边界选模型之前先明确一点股票日线数据每个交易日样本数量通常只有几百到几千条这点数据量喂给大规模 Transformer效果往往还不如一个结构简单的 LSTM。我做过的对比实验结果里在 3000 条样本规模下LSTM 的测试集方向准确率大约 52%55%而一个三层 MLP 也能到 50%53%两者差异并没有想象中大。GRU 和 LSTM 表现基本持平但 GRU 参数量更少、训练更快。为什么还是推荐 LSTM因为它能显式建模序列依赖关系特别是“连续上涨后的缩量回调往往预示短期见顶”这类需要结合多日上下文才能表达的模式。MLP 把每天的特征向量独立看待天然丢失了相邻交易日之间的先后关系。如果非要上 Transformer我建议把输入改成 60 天的窗口序列而不是单日特征否则自注意力机制没有发挥空间。下面是使用 TensorFlow Keras 构建 LSTM 基线模型的完整脚本import tensorflow as tf from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau # 把单日特征整理成 10 天的滑动窗口序列 def make_sequences(features, labels, seq_len10): X, y [], [] for i in range(seq_len, len(features)): X.append(features[i - seq_len:i]) y.append(labels[i]) return np.array(X), np.array(y) X_train_seq, y_train_seq make_sequences(X_train, y_train, seq_len10) X_test_seq, y_test_seq make_sequences(X_test, y_test, seq_len10) model Sequential([ LSTM(64, return_sequencesTrue, input_shape(10, X_train.shape[1])), Dropout(0.3), LSTM(32, return_sequencesFalse), Dropout(0.3), Dense(16, activationrelu), Dense(1, activationsigmoid) ]) model.compile(optimizeradam, lossbinary_crossentropy, metrics[accuracy]) early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) reduce_lr ReduceLROnPlateau(monitorval_loss, factor0.5, patience5, min_lr1e-5) history model.fit( X_train_seq, y_train_seq, validation_split0.1, epochs100, batch_size32, callbacks[early_stop, reduce_lr], verbose1 )这个脚本的序列化逻辑在make_sequences函数中features[i - seq_len:i]取当前样本之前的 10 天特征labels[i]取第 11 天的未来 5 日涨跌方向。注意这里窗口滑动的步长是 1意味着相邻窗口之间有 9 天的重叠这会造成严重的样本自相关性后面会专门讲这个问题。参数说明第一个 LSTM 层设置return_sequencesTrue让输出保持时间维度方便第二层 LSTM 继续处理序列第二层用return_sequencesFalse只输出最后一个时间步的状态再接全连接层。Dropout(0.3)是 LSTM 序列模型里比较关键的正则化参数值太低抑制不了过拟合太高又会把模型压死导致欠拟合一般从 0.3 起调。EarlyStopping(patience10)表示验证损失连续 10 个 epoch 不下降就停配合restore_best_weightsTrue把权重回滚到验证集最优的状态。3.2 训练细节验证集比例、批大小与学习率衰减的取舍训练股票模型时validation_split0.1表示从训练集的末尾切出 10% 作为验证集注意这个切分同样按时间顺序进行——Keras 默认从尾部取恰好符合时间序列要求。批大小batch_size32对日线数据来说不算大因为样本总量本来就少太大容易让梯度更新方向被局部噪声主导。回调函数里ReduceLROnPlateau的factor0.5表示验证损失进入平台期时学习率折半min_lr1e-5是下限。LSTM 的 loss 面比较崎岖固定学习率很容易在某个局部最小值附近震荡上不去学习率衰减是这类问题的后悔药。训练完成后记得在测试集上做一次严谨评估用混淆矩阵和 AUC 看整体效果不要只盯着 accuracy。from sklearn.metrics import accuracy_score, roc_auc_score, confusion_matrix y_pred_prob model.predict(X_test_seq).flatten() y_pred (y_pred_prob 0.5).astype(int) acc accuracy_score(y_test_seq, y_pred) auc roc_auc_score(y_test_seq, y_pred_prob) print(f方向准确率: {acc:.4f}, AUC: {auc:.4f}) print(confusion_matrix(y_test_seq, y_pred))方向准确率 50% 是随机水平的基准线55% 以上已经说明模型提取到了微弱的市场规律60% 以上在日线级别几乎不可能稳定实现——如果有人跟你保证 80% 以上的准确率大概率是回测用了未来函数。AUC 比准确率更值得关注因为它不受阈值选择影响能反映模型把上涨样本和下跌样本分开的能力。如果accuracy有 55% 但AUC只有 0.52说明模型靠调阈值取巧信号本身没有区分度。3.3 特征窗口与标签构造的联动效应为什么序列长度不建议超过 20窗口长度seq_len10是我比较推荐的默认值。窗口太短模型看不到足够的中期趋势背景窗口太长比如 60 天样本数直接减少到原来的六分之一左右日线数据本来就少训练集可能只剩几百条LSTM 学什么都不够。另外长窗口在时间序列预测里有一个隐含问题距离当前时刻越远的特征对“最近”的市场状态的预测价值越低模型反而会被陈旧信息干扰。标签的预测周期与窗口长度之间也存在联动效应预测未来 5 日特征窗口取 515 日都可以预测未来 20 日窗口至少取 20 日起否则模型只能从一周的量价信息推断一个月后的走势等于让模型做超出信息边界的猜测。直观原则是标签周期不要超过特征窗口长度的两倍这是我试过多种组合后觉得比较安全的边界。4. 回测框架与信号转换把模型输出变成可执行的策略4.1 模型概率到交易信号的平滑直接二值化是让回测曲线翻车的元凶模型输出的y_pred_prob是一个 0 到 1 的概率值新手最常见的做法是以 0.5 为阈值直接转成“买入/不买”。实际回测里这样的硬切会在震荡行情里频繁切换仓位交易成本把收益吃掉一大截。我一般会做一道平滑处理对概率序列取滚动均值再按置信区间划分信号。# 对预测概率做 3 日滚动平均平滑掉单次预测的噪声 prob_smooth pd.Series(y_pred_prob).rolling(3, min_periods1).mean().values # 信号生成高置信买入(0.65)高置信卖出(0.35)其余持仓不变 position np.zeros(len(prob_smooth)) position[prob_smooth 0.65] 1 position[prob_smooth 0.35] -1 position pd.Series(position).replace(0, methodffill).fillna(0).values这里用滚动平均把 10 天窗口模型输出的概率进行平滑避免单日噪声触发仓位反转。阈值 0.65/0.35 是典型的置信带宽带宽越宽交易次数越少。replace(0, methodffill)的作用是当模型出现“不明确”信号概率在 0.350.65 之间时保持上一交易日的仓位不变这极大减少了无效换手。参数说明min_periods1保证序列前两个数据点也能参与平滑不会因为窗口不足产生 NaN。如果你测试的标的波动率特别大可以把阈值带宽扩大比如 0.7/0.3波动率小的蓝筹股可以缩到 0.6/0.4让信号更灵敏。这个参数的设置没有绝对标准需要结合个股的回测结果调整。4.2 简单可复现的回测手续费、滑点与资金曲线的计算回测的严谨程度直接决定你对策略真实水平的判断。很多新手回测时不计算手续费和滑点看资金曲线觉得“稳了”实盘时交易软件里的盈亏立刻缩水。A 股的手续费由佣金、印花税和过户费组成印花税卖出时单边收 0.05%佣金按万 2.5 到万 3最低 5 元。自己搭回测框架时我统一按单边千分之一计算成本包含佣金和滑点这是比较保守也接近真实的估计。# 以收盘价成交按单边千分之一扣除交易成本 def run_backtest(close, position, cost_rate0.001): capital 1.0 equity [] prev_pos 0 for i in range(len(close)): cur_pos position[i] if cur_pos ! prev_pos and i 0: # 建仓或平仓扣除单边手续费和滑点 capital capital * (1 - cost_rate) # 当日持仓盈亏做多赚涨跌幅做空亏涨跌幅 daily_ret close[i] / close[i - 1] - 1 if i 0 else 0 capital capital * (1 daily_ret * cur_pos) equity.append(capital) prev_pos cur_pos return np.array(equity) equity run_backtest(test_data[close].values, position)这段回测代码的假设是每次以收盘价成交daily_ret * cur_pos反映多头仓位赚取正收益和空头仓位赚取负收益。注意cur_pos 1表示满仓做多-1表示满仓做空0 表示空仓。实际交易中很少有人始终满仓但作为模型评估基准这种简化能让资金曲线与预测准确率之间的因果关系更清晰。关于回测还有一个容易被忽略的细节close序列必须和position序列长度一致而且position从测试集第一个样本开始生成。因为make_sequences会丢弃前 10 个样本回测起始日期应该对齐到测试集中第 10 个数据点之后。否则资金曲线前面几天的position值为 0相当于白占几天时间收益计算基准不对。回测指标里我最看重的不是总收益率而是最大回撤和夏普比率。下面给一段计算这两个指标的基础代码def max_drawdown(equity): peak np.maximum.accumulate(equity) drawdown (equity - peak) / peak return drawdown.min() def sharpe_ratio(equity, risk_free0.0, periods252): rets np.diff(equity) / equity[:-1] return np.sqrt(periods) * (rets.mean() - risk_free) / rets.std() mdd max_drawdown(equity) sharpe sharpe_ratio(equity) print(f最大回撤: {mdd:.2%}, 夏普比率: {sharpe:.2f})最大回撤反映策略在某个连续下跌区间里最惨的亏损幅度这是量化策略最核心的风险指标回撤超过 20% 的策略基本拿不住实盘执行会变形。夏普比率衡量每承受一单位波动能换来多少超额收益大于 1 才算勉强合格1.5 以上是优秀水平。如果模型预测方向准确率只有 52%回测夏普却超过 3几乎可以断定回测框架里有漏洞优先检查是否使用了未来数据或者成本设置过低。4.3 基准对比跑赢“买入并持有”才算有意义的模型一个残酷的事实是A 股长期持有沪深 300 指数在绝大多数年份是正收益的如果你的策略回测收益还不如“买入并持有”那这个模型没有实际部署价值。所以在回测最后一定要把策略资金曲线和基准指数叠加比较。# 基准满仓买入并持有测试区间 benchmark np.cumprod(1 test_data[close].pct_change().fillna(0).values) strategy_ratio equity / equity[0] benchmark_ratio benchmark / benchmark[0] # 超额收益 策略期间收益 - 基准期间收益 strategy_ret strategy_ratio[-1] - 1 benchmark_ret benchmark_ratio[-1] - 1 excess strategy_ret - benchmark_ret print(f策略收益: {strategy_ret:.2%}, 基准收益: {benchmark_ret:.2%}, 超额: {excess:.2%})这段代码用cumprod计算基准的累计资金曲线然后和策略资金曲线统一到起始净值 1。超额收益为正模型才有继续调参的意义超额收益为负说明深度学习模型不仅没捕捉到规律反而被市场噪声带偏了。这也是区分“模型拟合”和“模型有效”最直接的标尺。5. 股票预测模型避坑指南数据泄露、过拟合与静默故障排查5.1 未来函数标签与特征错位导致的方向准确率虚高现象回测方向准确率高达 70%资金曲线近乎直线上升但实盘完全失效。原因shift(-5)构造标签时如果特征里也用了未来数据比如用第 T2 天的成交量填充了第 T 天的缺失值模型就相当于在预测时看到了“答案”。另一种常见错位是标准化时用了全量数据导致测试集信息混进训练过程。解决每次构造特征和标签后用一个简单的验证脚本检查——把特征列中最大值对应的日期打印出来确认它不晚于标签日期。更稳妥的做法是写一个assert断言确保训练集最大日期不晚于测试集最小日期且所有shift计算完成后才切分数据。5.2 样本高度重叠导致验证集失真现象训练集和验证集的 AUC 都很高但测试集 AUC 跌到 0.5模型形同虚设。原因滑动窗口步长为 1相邻样本之间 9 天重叠训练集与验证集交界处的前后样本实际上高度相似验证集的“最优”很可能是对近期行情的记忆而不是泛化能力。解决构造训练集时把窗口步长从 1 改成 5 或 10即每隔 5 天取一个样本窗口。数据量允许的条件下这是最直接的去重手段。如果数据量本来就少可以改用 K 折交叉验证但必须采用 Purged K-Fold 这类考虑样本重叠的变种普通 KFold 不适用。5.3 非平稳行情带来的特征分布漂移现象模型在 2022 年数据上训练效果不错换到 2024 年牛市环境后预测方向准确率暴跌。原因股票收益率序列虽然近似平稳但波动率存在明显的聚集效应。牛市里波动率高、趋势性强熊市里波动率低、以震荡为主模型学到的“涨跌模式”在分布漂移后实效。解决特征里加入波动率状态标记比如把vol_5按历史分位数转成高/中/低三档的独热编码训练时加入近一年的数据做滚动训练而不是固定用历史区间。滚动窗口长度建议取 250 个交易日左右刚好覆盖一年既保留足够样本量又不让过期模式占主导。5.4 库版本不匹配导致的复现结果不一致现象同一份代码在自己的机器上跑 AUC 0.55换台机器变成 0.51甚至报错AttributeError: module tensorflow has no attribute keras。原因TensorFlow 2.x 各小版本的 API 差异较大tf.keras在不同版本里的导入路径和默认行为不完全一致baostock 接口偶尔变更返回字段类型也会导致解析失败。解决把环境依赖写入requirements.txt固定 TensorFlow 主版本号。下面是一个实用的环境配置方式python -m venv stock_env source stock_env/bin/activate # Windows 用 stock_env\Scripts\activate pip install tensorflow2.13.0 pandas numpy scikit-learn baostock固定tensorflow2.13.0是考虑到这个版本对 LSTM 的 CPU 支持和 Keras API 兼容性都处于比较稳定的状态。如果你用的是 PyTorch 路线就固定torch2.1.0加numpy2.0因为 numpy 2.0 对旧版 pandas 和 TensorFlow 的 C 扩展存在二进制不兼容这是近两年常见的环境配置翻车现场。5.5 除权除息与复权数据惹的祸现象回测中某一天收益突然出现 -30% 的异常值模型预测却显示大涨信号。原因数据源返回的是不复权价格遇到除权日股价从 100 元瞬间“跌”到 50 元模型把它当成了真实的崩盘信号回测的收益也被错误计算。解决在拉数据时统一使用前复权价格也就是adjustflag2。前复权保持当前价格不变对历史价格做缩放调整适合做策略回测后复权保持历史价格不变适合看长期走势但当前价格会偏离真实值。策略回测一律用前复权并且不要在除权日前后做任何特殊处理这是最简单也最可靠的做法。6. 一个让模型真正“可信”的进阶验证残差分析与置信度过滤当方向准确率和回测超额收益都过线之后先别急着部署实盘花点时间做残差分析和置信度过滤——这套组合能帮你区分“模型学到了规律”和“模型只是记住了训练集”。具体做法是把测试集的预测概率按从高到低排序然后分组统计每组的实际上涨概率如果预测概率 0.8 以上的分组实际上涨率也显著高于 0.5说明模型的概率输出包含真实信息如果各组的实际上涨率都徘徊在 0.5 附近说明模型输出的概率只是校准得漂亮没有实际区分度。置信度过滤是把低置信度的预测结果直接丢弃只交易模型“最有把握”的行情。实盘中最常见的用法是只有当预测概率超过 0.7 时才建仓低于 0.3 时才平仓其余时间维持空仓或低仓位。这相当于把模型的信号从“全时段预测”降级为“关键时刻出手”虽然交易次数减少但每笔交易的胜率和盈亏比通常会更好。我自己的习惯是用置信度过滤后的策略跟原始策略做收益对比如果过滤后夏普显著提升说明模型的信号质量确实集中在高分段。残差分析的另一个用途是辅助调参把模型预测错误的样本单独提出来查看它们的特征分布和日期分布。如果错误集中在某几个特定月份大概率是遇到了政策冲击或行业突发事件如果错误集中在某个特征区间比如换手率极低的日子可以考虑给该特征增加权重或构造交互特征。这些判断没有统一答案但每一次排查都会让你对数据、模型和市场之间的关系理解更深一层——这个领域没有标准答案但每一步严谨验证都在帮助你降低不确定性。这套流程走完你的模型至少是一个“诚实”的模型它知道自己知道什么也知道自己不知道什么。希望这篇文章能帮你把股票预测从玄学拉回到可复现的工程轨道上少踩几个我踩过的坑。本文还有配套的精品资源点击获取