简介:这份资源是面向高校学生与量化投资初学者的深度学习实战项目包,可作为毕业设计、期末大作业或人工智能课程实践参考,帮助读者理解如何用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, drop=True) df['turnover_ma5'] = g['turnover'].rolling(5).mean().reset_index(0, drop=True) # 量比:当日成交量 / 过去5日均量 df['vol_ratio'] = df['volume'] / g['volume'].rolling(5).mean().reset_index(0, drop=True) return df.dropna()这段代码的关键点在于所有滚动计算都必须按股票分组,否则会把不同股票的数据混在一起。pct_change(5)算的是 5 日累计收益,rolling(20).std()算 20 日波动,reset_index(0, drop=True)是为了让 rolling 结果和原索引对齐。参数上,5 和 20 是日频常用的短中期窗口,做周频可以换成 4 和 12。注意dropna()会删掉前 20 行,这是必要的,但要在回测时保证不引入未来数据。
2.2 标签设计:回归还是分类,预测几天
标签决定了模型在学什么。常见三种做法:预测未来 N 日收益率做回归、预测未来 N 日涨跌做二分类、预测未来 N 日收益率排序做排序学习。我自己的经验是,日频选 N=5 或 N=10 比较稳,N=1 噪声太大,N=20 以上信号衰减明显。
回归标签直接用close.shift(-N)/close - 1,但要注意极端值处理。A 股经常有涨跌停,未来 5 日收益可能出现 +60% 这种离群点,直接回归会被少数样本带偏。我一般会做 winsorize,把标签截断在 1% 和 99% 分位数。
def build_label(df, n=5): 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-2019 | 2020 | 2021-2023 | 快速验证想法 |
| 滚动切分 | 前 4 年 | 第 5 年 | 第 6 年 | 接近实盘 |
| 扩展切分 | 2015-2019 | 2020 | 2021 | 样本量少时 |
滚动切分更接近实盘,因为实盘中你只能用到当前时点之前的数据。代价是计算量大,每次滚动都要重新训练。我一般先用固定切分快速筛模型结构,确定后再用滚动切分做最终评估。
3. 模型选型与训练:LSTM、GRU 还是 Transformer
3.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_dim=64, num_layers=2, dropout=0.3): super().__init__() self.gru = nn.GRU(input_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout) 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_dim=64是隐藏层维度,num_layers=2是 GRU 层数,dropout=0.3是防过拟合的关键。out[:, -1, :]取序列最后一个时间步的隐藏状态,代表模型对当前时点的总结。如果做的是多步预测,可以改成取多个时间步或加 attention。参数上,hidden_dim 从 32 到 128 都常见,num_layers 超过 3 层在金融数据上基本没有收益,dropout 低于 0.2 容易过拟合。
3.2 训练循环里的三个关键设置
训练循环本身不复杂,但量化场景有三个特殊设置。第一是损失函数,回归任务用 MSE 或 HuberLoss,Huber 对离群值更鲁棒。第二是早停,验证集 loss 连续若干轮不降就停,避免过拟合。第三是学习率调度,用 ReduceLROnPlateau 在验证 loss 停滞时降学习率。
model = GRUModel(input_dim=len(feature_cols)) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, patience=5) 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_decay=1e-4是 L2 正则,金融数据上很重要。HuberLoss默认 delta=1.0,对极端收益更稳。早停 patience 设 10 轮,配合 ReduceLROnPlateau 的 patience=5,能有效防止过拟合。注意保存的是验证集最优的模型,不是最后一轮的模型,这个细节很多人会忽略。
3.3 序列长度和 batch 怎么选
序列长度决定模型看多长的历史。日频数据我一般用 20 到 60 天,太短捕捉不到中期趋势,太长引入噪声且训练慢。batch size 上,金融数据样本量通常几十万到几百万,batch 设 256 或 512 比较合适,太小训练不稳,太大泛化差。
| 参数 | 常用范围 | 我的默认值 | 调整方向 |
|---|---|---|---|
| 序列长度 | 20-60 | 30 | 数据噪声大就缩短 |
| batch size | 128-1024 | 256 | 显存够就加大 |
| hidden_dim | 32-128 | 64 | 因子多就加大 |
| dropout | 0.2-0.5 | 0.3 | 过拟合就加大 |
| 学习率 | 1e-4-1e-2 | 1e-3 | 不收敛就减小 |
这些参数没有绝对最优,要在验证集上试。我一般会做一轮粗调,固定其他参数,单独调序列长度和 dropout,这两个对结果影响最大。
4. 回测与信号生成:把模型输出变成可执行策略
4.1 从预测值到持仓:排序分组还是阈值
模型输出的是每只股票未来 N 日的预期收益。要变成持仓,常见两种做法。一是排序分组,每天按预测值排序,买前 10% 卖后 10%,做多空组合。二是阈值法,预测值超过某个阈值就买,低于就卖。排序分组更稳健,因为它不依赖预测值的绝对大小,只依赖相对排序,而金融预测的绝对值往往不准。
def generate_signal(pred_df, top_pct=0.1): # pred_df: 日期、代码、预测值 pred_df['rank'] = pred_df.groupby('date')['pred'].rank(pct=True) 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(pct=True)把预测值转成 0 到 1 的百分位,top_pct=0.1表示买前 10%、卖后 10%。这样做的好处是每天持仓数量稳定,不会因为预测值整体偏移导致空仓或满仓。参数上,top_pct 常用 0.1 或 0.2,太小换手率高,太大信号弱。
4.2 回测里必须扣掉的成本
很多深度学习量化策略的回测曲线漂亮,实盘一塌糊涂,核心原因就是没扣交易成本。A 股单边成本大约千分之一到千分之三,包括佣金、印花税、冲击成本。日频调仓的策略,如果每天换手 50%,一年 250 个交易日,成本能吃掉大部分收益。
def backtest(signal_df, ret_df, cost=0.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 dailycost=0.002是单边千分之二,双边就是千分之四。turnover用 signal 变化的绝对值算,从 0 到 1 算一次换手。daily是等权组合的日收益。这个回测很粗糙,但能快速判断策略在扣成本后是否还有超额。更严谨的要用实际持仓权重和成交价,但那是下一步的事。
4.3 评价指标:不只看收益
量化策略评价不能只看年化收益。我一般看四个指标:年化收益、夏普比率、最大回撤、换手率。夏普比率衡量单位风险的收益,日频策略年化夏普超过 1 就不错,超过 2 要警惕过拟合。最大回撤反映极端风险,超过 30% 要检查是不是杠杆或集中度过高。换手率决定成本,日频策略年换手超过 50 倍基本不可行。
| 指标 | 计算方式 | 合理范围 | 警戒线 |
|---|---|---|---|
| 年化收益 | 日收益均值 × 250 | 10%-30% | >50% 需查过拟合 |
| 夏普比率 | 年化收益 / 年化波动 | 0.5-2 | >3 需查过拟合 |
| 最大回撤 | 净值峰值到谷值 | <20% | >30% 需查风险 |
| 年换手率 | 日均换手 × 250 | <30 倍 | >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'], method='spearman') ) 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。三关都过才考虑小资金实盘。这个习惯帮我避开了很多看着漂亮实则脆弱的策略。希望帮到你。
本文还有配套的精品资源,点击获取