
简介这份资源是面向高校学生与量化投资初学者的深度学习实战项目包可作为毕业设计、期末大作业或人工智能课程实践参考帮助读者理解如何用Python将深度学习模型落地到股票投资策略中。压缩包共46个文件约216KB以23个py源码为核心辅以xml工程配置、csv策略与行情数据、sqlite/db数据库文件及md说明文档覆盖数据读取、模型构建、策略执行与模拟交易等环节。项目围绕数据预处理、RNN/LSTM/CNN等模型搭建、训练调优以及历史回测评估展开代码按数据、深度学习网络、策略、模拟运行与测试等模块分层组织结构清晰便于按模块阅读与二次修改。目前已有133人学习下载适合希望打通深度学习与量化投资知识链路、积累完整项目经验的读者参考借鉴。1. 基于深度学习的量化投资策略从因子挖掘到实盘信号的落地拆解很多人第一次听到「基于深度学习的量化投资策略」脑子里浮现的是把 K 线图塞进 CNN、让模型自动吐买卖点然后坐等收益曲线一路向北。我最初也这么想过直到自己用 LSTM 在日频数据上跑出第一条净值曲线才发现真正难的不是搭网络而是让模型学到的东西在样本外还站得住。这个方向解决的核心问题是把传统多因子模型里靠人工设定的非线性关系交给网络去拟合同时用严格的滚动回测约束过拟合。它适合有一定 Python 和 pandas 基础、懂基本金融时序概念、想从规则型策略往机器学习策略迁移的从业者。下面我按自己实际落地的顺序把数据、标签、模型、回测、避坑一条条讲清楚。2. 数据与标签怎么定量化投资里深度学习的第一道分水岭2.1 为什么原始行情不能直接喂给网络量化投资和图像、NLP 最大的区别在于信噪比极低。日频股票收益率里能被预测的部分往往不到千分之几剩下全是噪声。直接把开盘、最高、最低、收盘、成交量五列丢进 LSTM模型大概率会去拟合噪声训练集 loss 降得很漂亮验证集一塌糊涂。常见做法是先做特征工程把原始行情转成有经济含义的因子再让网络去学因子之间的非线性组合。我一般会构造几类基础因子动量类过去 5/10/20 日收益率、波动类过去 20 日收益率标准差、量价类换手率、量比、估值类如果数据源有 PE、PB。这些因子本身在传统多因子框架里就被验证过有一定预测力深度学习的作用是找它们的交互项和时变权重而不是从零发明信号。import pandas as pd import numpy as np def build_features(df): # df 需包含 open/high/low/close/volume按股票代码分组 g df.groupby(code) df[ret_5] g[close].pct_change(5) df[ret_20] g[close].pct_change(20) df[vol_20] g[close].pct_change().rolling(20).std().reset_index(0, dropTrue) df[turnover_ma5] g[turnover].rolling(5).mean().reset_index(0, dropTrue) # 量比当日成交量 / 过去5日均量 df[vol_ratio] df[volume] / g[volume].rolling(5).mean().reset_index(0, dropTrue) return df.dropna()这段代码的关键点在于所有滚动计算都必须按股票分组否则会把不同股票的数据混在一起。pct_change(5)算的是 5 日累计收益rolling(20).std()算 20 日波动reset_index(0, dropTrue)是为了让 rolling 结果和原索引对齐。参数上5 和 20 是日频常用的短中期窗口做周频可以换成 4 和 12。注意dropna()会删掉前 20 行这是必要的但要在回测时保证不引入未来数据。2.2 标签设计回归还是分类预测几天标签决定了模型在学什么。常见三种做法预测未来 N 日收益率做回归、预测未来 N 日涨跌做二分类、预测未来 N 日收益率排序做排序学习。我自己的经验是日频选 N5 或 N10 比较稳N1 噪声太大N20 以上信号衰减明显。回归标签直接用close.shift(-N)/close - 1但要注意极端值处理。A 股经常有涨跌停未来 5 日收益可能出现 60% 这种离群点直接回归会被少数样本带偏。我一般会做 winsorize把标签截断在 1% 和 99% 分位数。def build_label(df, n5): df[label] df.groupby(code)[close].shift(-n) / df[close] - 1 # 截断极端值 low, high df[label].quantile([0.01, 0.99]) df[label] df[label].clip(low, high) return df.dropna(subset[label])shift(-n)是向前看 n 天这是标签构造里唯一允许用未来数据的地方但必须在划分训练测试集之前完成且测试集的标签不能参与任何训练。clip的 1% 和 99% 分位是经验值样本少时可以放宽到 2% 和 98%。这里有个血泪教训如果先划分训练测试再算分位数测试集分布会泄露到训练集回测结果会虚高。2.3 时序切分不能随机打乱量化投资里最忌讳的就是随机划分训练测试集。同一只股票相邻日期的样本高度相关随机打乱会让模型在训练集里「见过」测试集附近的行情。正确做法是按时间切分比如 2015-2019 训练2020 验证2021-2023 测试。更严格的是滚动切分每次用前 T 年训练、后 1 年测试然后窗口向前滚动。切分方式训练集验证集测试集适用场景固定切分2015-201920202021-2023快速验证想法滚动切分前 4 年第 5 年第 6 年接近实盘扩展切分2015-201920202021样本量少时滚动切分更接近实盘因为实盘中你只能用到当前时点之前的数据。代价是计算量大每次滚动都要重新训练。我一般先用固定切分快速筛模型结构确定后再用滚动切分做最终评估。3. 模型选型与训练LSTM、GRU 还是 Transformer3.1 为什么时序任务里 LSTM 仍是基线深度学习里做时序预测LSTM 和 GRU 是最常见的基线。量化投资的数据是典型的多变量时序每只股票每天有几十个因子值LSTM 的门控机制能捕捉因子的时变权重。GRU 参数更少训练更快小样本上往往比 LSTM 更稳。Transformer 理论上能捕捉更长依赖但金融数据信噪比低注意力机制容易过拟合我一般只在因子维度很高、样本量足够大时才尝试。选型上我的建议是先跑 GRU 做基线再试 LSTM最后才考虑 Transformer 或 TCN。不要一上来就堆复杂结构金融数据里简单模型往往更抗过拟合。import torch import torch.nn as nn class GRUModel(nn.Module): def __init__(self, input_dim, hidden_dim64, num_layers2, dropout0.3): super().__init__() self.gru nn.GRU(input_dim, hidden_dim, num_layers, batch_firstTrue, dropoutdropout) self.fc nn.Linear(hidden_dim, 1) self.dropout nn.Dropout(dropout) def forward(self, x): # x: (batch, seq_len, input_dim) out, _ self.gru(x) out out[:, -1, :] # 取最后一个时间步 out self.dropout(out) return self.fc(out)input_dim是因子数量hidden_dim64是隐藏层维度num_layers2是 GRU 层数dropout0.3是防过拟合的关键。out[:, -1, :]取序列最后一个时间步的隐藏状态代表模型对当前时点的总结。如果做的是多步预测可以改成取多个时间步或加 attention。参数上hidden_dim 从 32 到 128 都常见num_layers 超过 3 层在金融数据上基本没有收益dropout 低于 0.2 容易过拟合。3.2 训练循环里的三个关键设置训练循环本身不复杂但量化场景有三个特殊设置。第一是损失函数回归任务用 MSE 或 HuberLossHuber 对离群值更鲁棒。第二是早停验证集 loss 连续若干轮不降就停避免过拟合。第三是学习率调度用 ReduceLROnPlateau 在验证 loss 停滞时降学习率。model GRUModel(input_dimlen(feature_cols)) optimizer torch.optim.Adam(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience5) criterion nn.HuberLoss() best_val float(inf) patience_counter 0 for epoch in range(100): model.train() for xb, yb in train_loader: pred model(xb) loss criterion(pred.squeeze(), yb) optimizer.zero_grad() loss.backward() optimizer.step() model.eval() val_loss 0 with torch.no_grad(): for xb, yb in val_loader: pred model(xb) val_loss criterion(pred.squeeze(), yb).item() val_loss / len(val_loader) scheduler.step(val_loss) if val_loss best_val: best_val val_loss torch.save(model.state_dict(), best.pt) patience_counter 0 else: patience_counter 1 if patience_counter 10: breakweight_decay1e-4是 L2 正则金融数据上很重要。HuberLoss默认 delta1.0对极端收益更稳。早停 patience 设 10 轮配合 ReduceLROnPlateau 的 patience5能有效防止过拟合。注意保存的是验证集最优的模型不是最后一轮的模型这个细节很多人会忽略。3.3 序列长度和 batch 怎么选序列长度决定模型看多长的历史。日频数据我一般用 20 到 60 天太短捕捉不到中期趋势太长引入噪声且训练慢。batch size 上金融数据样本量通常几十万到几百万batch 设 256 或 512 比较合适太小训练不稳太大泛化差。参数常用范围我的默认值调整方向序列长度20-6030数据噪声大就缩短batch size128-1024256显存够就加大hidden_dim32-12864因子多就加大dropout0.2-0.50.3过拟合就加大学习率1e-4-1e-21e-3不收敛就减小这些参数没有绝对最优要在验证集上试。我一般会做一轮粗调固定其他参数单独调序列长度和 dropout这两个对结果影响最大。4. 回测与信号生成把模型输出变成可执行策略4.1 从预测值到持仓排序分组还是阈值模型输出的是每只股票未来 N 日的预期收益。要变成持仓常见两种做法。一是排序分组每天按预测值排序买前 10% 卖后 10%做多空组合。二是阈值法预测值超过某个阈值就买低于就卖。排序分组更稳健因为它不依赖预测值的绝对大小只依赖相对排序而金融预测的绝对值往往不准。def generate_signal(pred_df, top_pct0.1): # pred_df: 日期、代码、预测值 pred_df[rank] pred_df.groupby(date)[pred].rank(pctTrue) pred_df[signal] 0 pred_df.loc[pred_df[rank] 1 - top_pct, signal] 1 pred_df.loc[pred_df[rank] top_pct, signal] -1 return pred_dfrank(pctTrue)把预测值转成 0 到 1 的百分位top_pct0.1表示买前 10%、卖后 10%。这样做的好处是每天持仓数量稳定不会因为预测值整体偏移导致空仓或满仓。参数上top_pct 常用 0.1 或 0.2太小换手率高太大信号弱。4.2 回测里必须扣掉的成本很多深度学习量化策略的回测曲线漂亮实盘一塌糊涂核心原因就是没扣交易成本。A 股单边成本大约千分之一到千分之三包括佣金、印花税、冲击成本。日频调仓的策略如果每天换手 50%一年 250 个交易日成本能吃掉大部分收益。def backtest(signal_df, ret_df, cost0.002): # signal_df: 日期、代码、signal # ret_df: 日期、代码、未来一期收益 df signal_df.merge(ret_df, on[date, code]) df[gross] df[signal] * df[ret] # 换手signal 变化的绝对值 df[turnover] df.groupby(code)[signal].diff().abs().fillna(0) df[net] df[gross] - df[turnover] * cost daily df.groupby(date)[net].mean() return dailycost0.002是单边千分之二双边就是千分之四。turnover用 signal 变化的绝对值算从 0 到 1 算一次换手。daily是等权组合的日收益。这个回测很粗糙但能快速判断策略在扣成本后是否还有超额。更严谨的要用实际持仓权重和成交价但那是下一步的事。4.3 评价指标不只看收益量化策略评价不能只看年化收益。我一般看四个指标年化收益、夏普比率、最大回撤、换手率。夏普比率衡量单位风险的收益日频策略年化夏普超过 1 就不错超过 2 要警惕过拟合。最大回撤反映极端风险超过 30% 要检查是不是杠杆或集中度过高。换手率决定成本日频策略年换手超过 50 倍基本不可行。指标计算方式合理范围警戒线年化收益日收益均值 × 25010%-30%50% 需查过拟合夏普比率年化收益 / 年化波动0.5-23 需查过拟合最大回撤净值峰值到谷值20%30% 需查风险年换手率日均换手 × 25030 倍50 倍成本高这些范围是日频 A 股的经验值不同市场、不同频率要调整。关键是任何指标异常好都要先怀疑数据泄露或过拟合而不是高兴。5. 避坑与排查深度学习量化策略最常见的五个翻车点5.1 未来函数回测漂亮实盘崩现象回测年化 50%实盘第一个月就亏 10%。原因特征或标签里混入了未来数据。比如用当日收盘价算的因子却在当日开盘就用来交易或者标签构造时shift(-n)之后又做了全局标准化把未来分布泄露到训练集。解决所有特征必须用shift(1)确保只用历史数据标准化只能在训练集上 fit再 transform 验证测试集。5.2 过拟合验证集好测试集差现象验证集 loss 一直降测试集 IC 接近零。原因模型太复杂或训练太久把训练集的噪声也学了。解决减小 hidden_dim、加大 dropout、加 L2 正则、早停。我一般还会看训练集和验证集 loss 的差距差距超过 30% 就是过拟合信号。5.3 样本不平衡涨跌停导致标签失真现象模型预测值集中在某个区间排序后多空组合没有区分度。原因A 股涨跌停导致部分样本未来收益被截断标签分布偏斜。解决剔除涨跌停样本或对标签做 winsorize或用排序学习替代回归。我一般会在标签构造后检查分布偏度超过 2 就处理。5.4 数据对齐不同频率因子混用现象回测报错或结果异常。原因日频行情和月频财务数据合并时财务数据的公告日期和报告期没对齐导致用了未来才公告的数据。解决财务数据必须用公告日期而非报告期做 merge且公告日期之后才能使用。这个坑很隐蔽建议单独写测试用例验证。5.5 成本低估换手率被忽略现象回测收益高但实盘不赚钱。原因回测没扣成本或成本设太低。解决单边成本至少设千分之一高频策略设千分之三。同时检查换手率日频策略年换手超过 50 倍就要考虑降频或加持仓约束。6. 进阶技巧用 IC 和分层回测验证信号质量模型训完之后不要急着看净值曲线先看 IC。IC 是预测值和实际收益的截面相关系数日频策略 IC 均值超过 0.03 就算有效超过 0.05 算不错。ICIR 是 IC 均值除以 IC 标准差衡量稳定性超过 0.5 算稳。def calc_ic(pred_df, ret_df): df pred_df.merge(ret_df, on[date, code]) ic df.groupby(date).apply( lambda x: x[pred].corr(x[ret], methodspearman) ) return ic.mean(), ic.std(), ic.mean() / ic.std()spearman秩相关比 pearson 更稳因为金融数据非正态。IC 均值看方向ICIR 看稳定性。如果 IC 均值正但 ICIR 低说明信号时好时坏要检查是不是某些时段失效。分层回测是另一个验证手段。按预测值分 5 层或 10 层看每层收益是否单调。如果第 1 层到第 5 层收益递增说明信号有区分度如果中间层乱序说明信号不稳。分层年化收益夏普说明第 1 层最低-5%-0.3空头层第 2 层2%0.2弱第 3 层8%0.6中性第 4 层15%1.1强第 5 层最高22%1.5多头层单调性越好信号越可靠。如果第 5 层和第 1 层差距大但中间乱可能是少数极端样本贡献了收益实盘不稳定。我自己的习惯是任何策略上线前必须过三关IC 均值 0.03、分层单调、扣成本后夏普 1。三关都过才考虑小资金实盘。这个习惯帮我避开了很多看着漂亮实则脆弱的策略。希望帮到你。本文还有配套的精品资源点击获取