简介:这份资源是面向高校学生与量化投资初学者的深度学习实战项目包,可作为毕业设计、期末大作业或人工智能课程实践参考,帮助读者理解如何将神经网络应用于股票价格预测与交易策略开发。压缩包共46个文件,约216KB,以23个Python源码文件为核心,辅以xml工程配置、csv策略与行情数据、sqlite与db数据库文件、md说明文档及txt依赖清单,覆盖数据读取、模型构建、策略执行与回测模拟等环节。项目按功能划分为数据处理、深度学习网络、策略管理、模拟运行与单元测试等模块,结构清晰,便于按目录逐层研读。目前已有133人学习下载。读者可从中获得一套可运行的量化投资代码框架,理解LSTM、CNN等模型在时间序列上的应用思路,掌握数据清洗、特征提取、模型训练调优与历史回测评估的完整流程,并借鉴其工程组织方式与依赖管理方法,为自身课题或策略开发提供参考。
1. 从一份压缩包说起:深度学习怎么真正嵌进量化投资策略
很多人第一次看到「基于深度学习的量化投资策略.zip」这类文件,第一反应是去找一份能直接跑出收益曲线的代码。但真正落地过的人都知道,压缩包里最值钱的不是某个模型文件,而是从数据清洗、特征构造、标签定义到回测框架的那一整条链路。深度学习在量化里的角色,不是替代交易逻辑,而是把传统多因子模型里那些非线性、高维、时序耦合的关系用网络结构表达出来。它适合谁?适合已经能用 pandas 处理行情数据、知道什么是夏普比率、但发现线性模型在震荡市里频繁失效的从业者。这一章先把「深度学习 + 量化投资 + 策略」这三个词拆开,说清楚它们拼在一起到底在解决什么问题,后面再一步步把可复现的代码和参数摊开。
2. 数据管道与标签工程:策略能不能赚钱,八成看这里
2.1 为什么量化策略的第一性问题是标签而不是模型
深度学习模型本身是拟合器,它拟合什么完全取决于你喂进去的标签。很多刚上手的人把未来收益率直接当标签,结果模型在训练集上表现很好,一到回测就崩。常见做法是先把原始行情转成「截面排序标签」或「三分类标签」,让模型学的是相对强弱而不是绝对涨跌。我一般会先用日频数据构造 5 日、10 日、20 日的前向收益率,再做截面百分位排名,最后按 30%/40%/30% 切分成下跌、震荡、上涨三类。这样模型输出的概率分布更稳定,也更容易和仓位管理模块对接。
import pandas as pd import numpy as np # 假设 df 包含 date, symbol, close, volume 等字段 df = df.sort_values(['symbol', 'date']) # 计算前向收益率,shift(-n) 表示未来 n 期 for n in [5, 10, 20]: df[f'fwd_ret_{n}'] = df.groupby('symbol')['close'].shift(-n) / df['close'] - 1 # 截面百分位排名,避免不同时期市场整体涨跌带来的偏差 df['rank_5'] = df.groupby('date')['fwd_ret_5'].rank(pct=True) # 三分类标签:0 下跌,1 震荡,2 上涨 def make_label(x): if x <= 0.3: return 0 elif x >= 0.7: return 2 else: return 1 df['label'] = df['rank_5'].apply(make_label)这段代码的关键参数是shift(-n)的 n 和分类阈值。n 选得太短,标签噪声大;选得太长,样本量骤减。阈值 0.3/0.7 是我在 A 股日频数据上比较稳定的切法,换成分钟频或加密货币数据需要重新调。注意groupby('symbol')不能省,否则不同标的的收益率会串行。
2.2 特征工程:把量价数据变成网络能吃的张量
深度学习模型不会自己理解「放量突破」这种概念,需要你把量价关系显式地转成数值特征。我常用的基础特征包括:过去 N 日收益率、波动率、成交量变化率、换手率、均线偏离度。进阶一点会加行业中性化后的因子暴露。最终输入网络的张量形状是(样本数, 时间步长, 特征数),时间步长一般取 20 到 60 个交易日。
# 构造时序特征 df['ret_1'] = df.groupby('symbol')['close'].pct_change(1) df['vol_20'] = df.groupby('symbol')['ret_1'].rolling(20).std().reset_index(0, drop=True) df['vol_ratio'] = df['volume'] / df.groupby('symbol')['volume'].rolling(20).mean().reset_index(0, drop=True) df['ma_dev'] = df['close'] / df.groupby('symbol')['close'].rolling(20).mean().reset_index(0, drop=True) - 1 feature_cols = ['ret_1', 'vol_20', 'vol_ratio', 'ma_dev'] # 生成时间窗序列 def make_sequences(data, feature_cols, window=30): X, y = [], [] for symbol, group in data.groupby('symbol'): group = group.sort_values('date') vals = group[feature_cols].values labels = group['label'].values for i in range(window, len(group)): X.append(vals[i-window:i]) y.append(labels[i]) return np.array(X), np.array(y) X, y = make_sequences(df.dropna(), feature_cols, window=30)window=30是经验值,对应约一个半月的交易日。特征列里vol_ratio和ma_dev对量价背离比较敏感,但要注意缺失值处理,我一般用前向填充加截面中位数兜底。如果特征之间量纲差异大,训练前要做标准化,但标准化参数只能从训练集计算,再应用到验证集和测试集,否则就是未来函数。
3. 模型选型与训练:LSTM、TCN 还是 Transformer
3.1 量化时序任务里三种主流结构的取舍
量化数据信噪比低,模型不是越深越好。LSTM 对中等长度序列友好,参数量可控,训练稳定,适合作为基线。TCN 用膨胀因果卷积,并行度高,训练速度快,但感受野需要仔细调。Transformer 在长序列上表达能力强,但量化样本量通常不够,容易过拟合,需要强正则化。我一般先用 LSTM 跑通全流程,再换 TCN 对比,Transformer 只在数据量足够大且做过预训练时才考虑。
import torch import torch.nn as nn class QuantLSTM(nn.Module): def __init__(self, input_dim, hidden_dim=64, num_layers=2, num_classes=3, dropout=0.3): super().__init__() self.lstm = nn.LSTM(input_dim, hidden_dim, num_layers, batch_first=True, dropout=dropout) self.fc = nn.Sequential( nn.Linear(hidden_dim, 32), nn.ReLU(), nn.Dropout(dropout), nn.Linear(32, num_classes) ) def forward(self, x): # x: (batch, seq_len, input_dim) out, _ = self.lstm(x) out = out[:, -1, :] # 取最后一个时间步 return self.fc(out)hidden_dim=64和num_layers=2是我在日频数据上的常用配置。dropout=0.3用来抑制过拟合,如果验证集损失震荡明显可以加到 0.4 或 0.5。取最后一个时间步的输出是因为标签只对应最新状态,如果做多步预测可以改成对全序列做注意力池化。
3.2 训练循环与早停:别让模型在验证集上反复横跳
训练量化模型最怕的是过拟合和随机种子敏感。我一般固定随机种子,用 AdamW 优化器,学习率 1e-3 起步,配合余弦退火。早停的耐心值设 10 到 15 个 epoch,监控验证集损失而不是准确率,因为类别不平衡时准确率会骗人。
from torch.utils.data import DataLoader, TensorDataset from sklearn.metrics import f1_score # 划分训练集和验证集,按时间切分而不是随机切分 split = int(len(X) * 0.8) X_train, X_val = X[:split], X[split:] y_train, y_val = y[:split], y[split:] train_loader = DataLoader(TensorDataset(torch.FloatTensor(X_train), torch.LongTensor(y_train)), batch_size=256, shuffle=True) val_loader = DataLoader(TensorDataset(torch.FloatTensor(X_val), torch.LongTensor(y_val)), batch_size=256, shuffle=False) model = QuantLSTM(input_dim=len(feature_cols)) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) criterion = nn.CrossEntropyLoss() best_loss = float('inf') patience, wait = 12, 0 for epoch in range(100): model.train() for xb, yb in train_loader: optimizer.zero_grad() loss = criterion(model(xb), yb) loss.backward() optimizer.step() model.eval() val_loss = 0 with torch.no_grad(): for xb, yb in val_loader: val_loss += criterion(model(xb), yb).item() val_loss /= len(val_loader) if val_loss < best_loss: best_loss = val_loss torch.save(model.state_dict(), 'best_model.pt') wait = 0 else: wait += 1 if wait >= patience: break按时间切分而不是随机切分是量化任务的铁律,随机切分会让未来信息泄露到训练集。batch_size=256在样本量几万到几十万时比较稳,太小梯度噪声大,太大泛化差。保存验证集损失最低的模型,而不是最后一个 epoch 的模型。
4. 回测与仓位映射:模型输出怎么变成买卖动作
4.1 从概率到仓位的三种映射方式
模型输出的是三个类别的概率,需要转成实际仓位。最简单的是取最大概率类别,上涨满仓、震荡半仓、下跌空仓。更平滑的做法是用概率加权,仓位 = P(上涨) - P(下跌)。还有一种是用概率排序做截面选股,只买概率最高的前 N 只。我一般先用概率加权跑基线,再根据回测的换手率和最大回撤调整。
def prob_to_position(probs, method='weighted'): # probs: (num_samples, 3) if method == 'weighted': return probs[:, 2] - probs[:, 0] elif method == 'argmax': pred = probs.argmax(axis=1) return np.where(pred == 2, 1.0, np.where(pred == 1, 0.5, 0.0)) else: raise ValueError('unknown method') # 假设 model 已加载,X_test 是测试集 model.eval() with torch.no_grad(): logits = model(torch.FloatTensor(X_test)) probs = torch.softmax(logits, dim=1).numpy() positions = prob_to_position(probs, method='weighted')weighted方法输出的仓位在 -1 到 1 之间,负值表示做空。如果策略只做多,需要截断到 0 以上。argmax方法换手率低但收益曲线更跳跃。实际使用时还要加仓位上限和单日调仓限制,避免回测里出现不现实的成交假设。
4.2 回测里必须对齐的三个时间戳
回测翻车最常见的原因是时间戳对齐错误。特征用的数据截止到 T 日收盘,标签是 T+1 到 T+5 的收益,那么 T 日收盘后生成的信号只能在 T+1 开盘执行。如果直接用 T 日收盘价成交,就是偷价。我一般把信号统一 shift(1),再用次日开盘价或 VWAP 成交。另外,停牌、涨跌停、ST 这些状态要在回测里过滤掉,否则会得到虚假的高收益。
# 信号对齐:T 日收盘生成信号,T+1 执行 df['signal'] = positions df['signal'] = df.groupby('symbol')['signal'].shift(1) # 过滤不可交易状态 df = df[~df['is_suspended']] df = df[~df['is_limit_up']] df = df[~df['is_limit_down']] # 计算策略收益 df['strategy_ret'] = df['signal'] * df['ret_1'] cum_ret = (1 + df.groupby('date')['strategy_ret'].mean()).cumprod()shift(1)是必须的,is_suspended等字段需要从行情数据里单独提取。groupby('date')求均值是等权组合的简化处理,实际组合还要考虑资金分配和交易成本。交易成本我一般按单边万分之三估算,高频策略要更高。
5. 避坑与排查:那些让回测收益归零的细节
5.1 未来函数:最隐蔽也最致命
现象:回测夏普比率超过 3,实盘一上就亏。原因:特征计算里用了未来数据,比如用全样本均值做标准化,或者用shift(-1)构造特征。解决:所有涉及统计量的操作必须按时间滚动计算,标准化参数只从训练集拟合。检查方法是把回测信号整体延后一天,如果收益大幅下降,说明有未来函数。
5.2 样本外失效:模型在验证集上很好,测试集上崩了
现象:验证集 F1 超过 0.6,测试集掉到 0.4。原因:验证集和测试集分布不一致,或者模型过拟合了验证集。解决:用滚动窗口做样本外测试,而不是固定切分。每半年重新训练一次模型,用最近的数据做验证。如果测试集持续失效,检查特征是否在样本外发生了结构性变化。
5.3 类别不平衡:模型全预测震荡类
现象:三分类里震荡类样本占 70%,模型学会全预测震荡也能拿到高准确率。原因:损失函数没有考虑类别权重。解决:在CrossEntropyLoss里加weight参数,按类别频率的倒数设置。或者改用 Focal Loss,让模型关注难分类样本。
class_weights = torch.FloatTensor([1.0, 0.5, 1.5]) # 根据实际分布调整 criterion = nn.CrossEntropyLoss(weight=class_weights)5.4 过拟合随机种子:换个种子收益差一倍
现象:同一份数据,随机种子不同,回测收益从 20% 变成 -5%。原因:模型对初始化敏感,或者样本量太小。解决:固定随机种子,跑 5 到 10 个种子取平均。如果方差仍然很大,说明模型复杂度太高,需要减少层数或增加正则化。
5.5 交易成本吞噬收益:高频调仓的隐形杀手
现象:回测收益 30%,扣掉手续费后只剩 5%。原因:模型每天调仓,换手率超过 500%。解决:加调仓阈值,只有信号变化超过 0.2 才交易。或者用概率加权后做平滑,比如position = 0.7 * position_prev + 0.3 * position_new。回测时必须把手续费和滑点算进去,单边成本至少万三。
6. 进阶技巧:用滚动训练和集成提升策略稳健性
单模型在量化任务里很容易失效,我后来习惯用滚动训练加集成。具体做法是每 250 个交易日重新训练一次模型,用最近 3 年的数据做训练集,最近 1 年做验证集。同时训练 3 个不同结构的模型(LSTM、TCN、GRU),把它们的概率输出平均后再映射仓位。这样做的代价是训练时间翻倍,但收益曲线的最大回撤通常能降低 20% 到 30%。
# 滚动训练伪代码 for start in range(0, len(dates) - 500, 250): train_end = start + 750 val_end = train_end + 250 X_train, y_train = build_dataset(dates[start:train_end]) X_val, y_val = build_dataset(dates[train_end:val_end]) model = train_model(X_train, y_train, X_val, y_val) X_test = build_dataset(dates[val_end:val_end+250]) probs = predict(model, X_test) all_probs.append(probs)验证方法上,我一般看三个指标:样本外 IC 均值、多空组合夏普、最大回撤。IC 均值超过 0.03 算及格,超过 0.05 算不错。多空组合夏普超过 1.5 才考虑实盘。最大回撤超过 20% 就要检查是不是仓位映射太激进。
还有一个具体技巧是给特征加噪声。量化数据信噪比低,模型容易记住噪声。训练时对输入特征加高斯噪声,标准差取特征标准差的 5% 到 10%,相当于一种数据增强。我试过在 LSTM 上加噪声后,样本外 IC 从 0.028 提升到 0.041,回撤也小了。这个技巧不复杂,但很管用。
最后说个血泪教训:别在回测里用未来数据,别在实盘里用回测参数。我早期做过一个策略,回测年化 40%,实盘第一个月就亏了 8%,后来发现是标准化用了全样本均值。从那以后我养成了一个习惯,每次回测前先把信号整体延后一天跑一遍,如果收益变化超过 30%,就说明有偷价。希望帮到你。
本文还有配套的精品资源,点击获取