简介:面向量化交易与深度学习交叉领域的完整实践项目包,适合高校学生完成期末大作业、毕业设计,也可作为机器学习初学者在金融场景中的上手范例。资源共46个文件,压缩包仅216KB,以23个Python脚本为主体,覆盖数据读取与清洗、SQLite存储、深度学习建模、策略模拟回测等关键模块;同时配有股票历史数据、单元测试用例、依赖清单与项目说明文档,代码结构清晰,模块划分明确,便于快速复现和二次开发。目前已有45人学习下载。通过该项目能够走通从数据预处理、特征构造、模型训练到回测评估的完整流程,了解CNN、RNN、LSTM在时间序列预测中的具体用法,并延伸至过拟合控制、超参数优化与风险管理思路;项目还预留了GPU训练入口和扩展接口,适合在此基础上继续开展毕业设计或策略研究。
1. 深度学习做量化,先想清楚“为什么是你”
如果只用一句话回答标题:基于深度学习的量化投资策略,是用神经网络从行情、资金流、宏观数据里自动提炼特征,再把这些特征映射到收益预测、仓位调整等交易动作上的一套工程实践。但在我接触过的几十个量化项目里,最常看到的失败不是模型不收敛,而是把深度学习当成“锦上添花”的工具,却忽略了数据对齐、标签构造和回测偏差这三个更底层的问题。
下面这套流程按我自己做策略的推进顺序来讲:先解决数据与特征,再谈 LSTM 等模型如何训练,然后讲回测里真正要盯的指标,最后是上线前的漂移检查和打包验证。适合你已经掌握 Python,写过简单的均线或因子策略,现在想用深度学习把信号合成这一环做得更扎实的人。这里先给一个反直觉结论:深度学习在量化里最大的价值不是预测准,而是帮你在高噪声样本里稳定提取弱信号,但前提是标签必须足够干净。
2. 特征与数据工程:决定策略上限的 80% 工作
2.1 量化场景里“干净数据”的真实含义
行情数据从 akshare、tushare、baostock 这类接口拿下来后,第一件事不是算特征,而是做三件对齐:时间戳对齐、复权对齐、标签对齐。时间戳对齐指的是不同市场(比如 A 股、港股)的交易日和交易时段不同,直接把 DataFrame 拼接会引入未来数据。复权对齐更隐蔽——前复权适合看历史走势,后复权适合计算真实收益率,如果混合使用,收益率序列会突变。标签对齐则是指你计算“未来 5 日收益”时,必须保证标签对应的窗口起始点在当前特征之后。
一个典型的反例是:有人在计算滚动动量时用了shift(-1)却不小心把当天收盘价也算进去,导致模型无意中“看到”了未来。这类未来函数在深度学习里比在传统线性模型里更危险,因为神经网络擅长记住这种欺骗性映射,验证集上表现极好,实盘却崩溃。我一般会在特征工程后做一次数据泄漏检查:随机打乱标签后重新训练,观察 AUC 或相关系数是否接近 0,如果显著不为 0,说明特征里掺了未来信息。
2.1.1 数据泄漏的快速自检方法
df['label'] = df['close'].shift(-horizon) / df['close'] - 1这类代码写完后,把close列整体随机打乱,再重新训练同一个模型。正常打乱后模型应该失去预测力;如果指标还很高,就去检查特征里是否有被意外 shift 的列。
2.2 用 pandas 生成 5 类可复现特征的代码与参数
下面这段代码覆盖了动量、波动率、RSI、滚动相关性、量价背离五类特征,是我在初版策略里最常用的组合。特征不在多,在于每一类都对应一种市场行为假设。
import pandas as pd import numpy as np def build_features(df: pd.DataFrame) -> pd.DataFrame: """从标准 OHLCV 数据生成特征列""" df = df.sort_index().copy() # 1. 多窗口动量:捕捉不同周期的趋势延续 for window in [3, 5, 10, 20]: df[f'mom_{window}'] = df['close'].pct_change(window) # 2. 已实现波动率:近 10 日收益率的滚动标准差,衡量风险状态 df['ret_1'] = df['close'].pct_change(1) df['vol_10'] = df['ret_1'].rolling(10).std() # 3. RSI:以 14 日为基准的超买超卖指标 delta = df['close'].diff() up = delta.clip(lower=0).rolling(14).mean() down = (-delta.clip(upper=0)).rolling(14).mean() df['rsi_14'] = 100 - 100 / (1 + up / down) # 4. 成交额动量相关性:验证量价是否配合 df['amount_chg'] = df['amount'].pct_change(10) df['corr_ret_amount'] = df['ret_1'].rolling(20).corr(df['amount'].pct_change(1)) # 5. 日内位置:收盘价在当日振幅中的相对高度 high_low_range = df['high'] - df['low'] df['close_position'] = (df['close'] - df['low']) / high_low_range.replace(0, np.nan) return df.replace([np.inf, -np.inf], np.nan).dropna()参数说明:mom_3到mom_20分别代表 3 日到 20 日的累计涨幅,短窗口捕捉近期动量,长窗口识别中期趋势;vol_10用的是 10 日而非 20 日,因为 label 周期通常选 5 日,波动率窗口保持 label 的两倍左右会更稳定;rsi_14是通用参数,如果换成更短的 7,会提高对短期反转的敏感度,但噪声也会同步放大。
这里的小坑是pct_change(window)与pct_change(1).rolling(window).mean()并不等价,前者是“隔 window 日涨跌幅”,后者是“window 日内日收益率的均值”,前者更常用在动量因子上,别在写特征时混用。
除了 OHLCV,另一条常见的数据扩展路径是另类数据——新闻热度、北向资金、期权隐含波动率。这些数据在 Python 里拿到的难度参差不齐,但如果你想提升策略区分度,建议先看交易型另类数据(资金流、大单)而不是舆情文本,因为文本数据清洗成本高,而且从词向量到收益率的映射链路很难稳定。
2.3 一个被低估的预处理步骤:滑动窗口的标签构造
特征决定模型能看到什么,标签决定模型要学什么。常见做法是把目标定义为未来 N 日收益的二分类或三分类(涨 / 跌 / 平),或者直接用回归预测未来收益率。回归的好处是信息损失少,但容易被极端值带偏;分类更稳健,却需要手动设定阈值。
| 标签类型 | 构造方法 | 优点 | 适用场景 |
|---|---|---|---|
| 回归 | (close.shift(-N)/close - 1) | 信息完整,便于排序 | 有严格风控的仓位优化 |
| 二分类 | 上述收益 > 阈值则 1 否则 0 | 稳定、不易受极端值影响 | 递归的买卖信号 |
| 三分类 | 上涨 / 震荡 / 下跌 | 可单独建模震荡区间 | 波段策略 |
构造标签时还要注意:N 的大小决定训练样本重叠程度,N=5 的日线策略里相邻样本高度重叠,会让训练集出现隐性序列相关。解决方法是按时间间隔采样,或者保证验证集与训练集在时间上严格不重叠。我通常在 5 日标签上用 2000 个样本以上的数据,并保留最后 20% 作为真验证集。
提示:验证集切在时间序列末尾,而不是随机切分。随机切分会把未来信息混入训练过程。
3. 从 LSTM 到 Transformer:模型选型与训练闭环
3.1 为什么序列模型天然适配行情数据,但别照搬 NLP 整套
行情数据本质是多变量时间序列,LSTM、GRU 这类循环网络的设计初衷就是处理时间依赖,所以很自然地被用到量化里。而 Transformer 通过自注意力机制能捕捉更长距离的依赖关系,近期也被验证在部分高频场景有效,但它的训练需要更多数据,也更容易在小样本上过拟合。我的建议是:先用单层 LSTM 跑通全流程,再用 GRU 做一次对比实验,不要一上来就上大模型。
这里有一个决策边界:如果你的样本量小于 3000 条日线,LSTM 很可能打不过带 10 个特征的 XGBoost。深度学习的收益在数据量足够大(比如小时级、分钟级数据)时才明显。用一小时级数据训练时,输入序列长度我一般取 48(两天的交易时段)到 120(约两周),因为金融序列的相关性在短期内衰减很快,取太长反而会把陈旧噪声带进来。
3.2 一个最小可用的 PyTorch 训练代码
下面这段代码定义了带 Dropout 的单层 LSTM 预测器,并用 HuberLoss 作为损失函数。HuberLoss 比 MSE 对异常价格波动更稳健,我在收益预测里默认用它。
import torch import torch.nn as nn class LSTMPredictor(nn.Module): """输入 (batch, seq_len, input_size),输出 (batch, 1) 的收益预测""" def __init__(self, input_size, hidden_size=32, num_layers=1, dropout=0.2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=dropout) self.regressor = nn.Sequential( nn.Linear(hidden_size, 16), nn.ReLU(), nn.Linear(16, 1) ) def forward(self, x): out, _ = self.lstm(x) # out: (batch, seq_len, hidden) return self.regressor(out[:, -1, :]) # 取最后一个时间步 criterion = nn.HuberLoss(delta=1.0) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5)训练循环里有个容易被忽略的细节:金融序列的收益分布有厚尾,所以 batch 里会经常出现某个样本的收益是普通样本的 10 倍以上,HuberLoss 的delta=1.0正是为了在这些极端值出现时不让梯度爆炸。weight_decay=1e-5起的正则化作用在样本量小时更要保留。
3.2.1 把 DataFrame 转成 PyTorch 的 DataLoader
用 pandas 构造好X和y之后,转成 PyTorch 标准数据集时要注意数据类型,LSTM 要求输入是float32,标签如果是分类还要转成long。
from torch.utils.data import TensorDataset, DataLoader features = torch.tensor(X, dtype=torch.float32) labels = torch.tensor(y, dtype=torch.float32) dataset = TensorDataset(features, labels) loader = DataLoader(dataset, batch_size=64, shuffle=True, drop_last=True)这里的shuffle=True在大部分深度学习场景都是默认项,但在金融时间序列上需要解释:LSTM 的时间依赖已经被限制在每个样本内部的时间步上,所以样本之间 shuffle 不会破坏序列结构;但你绝不能把每个样本内部的时间步随机打乱。另外,drop_last=True可以避免最后一个 batch 样本数过少导致 BN 层统计量抖动。
3.2.2 提前停止和最佳模型保存
我在每个 epoch 结束后计算验证集损失,连续 10 个 epoch 验证损失不下降就停止,并在保存模型时只保存验证损失最小的权重。PyTorch 里用torch.save(model.state_dict(), 'best_model.pt')即可,后面上线打包时这个文件会和其他配置一起放进 zip。
3.3 三个必须盯的过拟合信号
训练损失降到接近 0,但验证损失一路上升,这是教科书级的过拟合,但在量化里还有两个更隐蔽的信号。
第一个是特征层面的过拟合:模型在训练集上高度依赖某单一特征,例如vol_10出现极端值时预测疯狂波动,你可以在训练后把每个特征的均值梯度打印出来,看是否过于集中。第二个是时间稳定性:把验证集再切成两半,前半段和后半段的指标差异超过 30%,说明策略依赖的是特定市场状态(比如单边上涨),换一个环境就会失效。
| 超参数 | 常见区间 | 过小的影响 | 过大的影响 |
|---|---|---|---|
| learning rate | 1e-4 ~ 3e-3 | 训练慢、容易困在局部 | 损失震荡、不收敛 |
| hidden_size | 16 ~ 64 | 表达力不足 | 过拟合、训练慢 |
| num_layers | 1 ~ 2 | 依赖长度不足 | 梯度消失、更难训练 |
| dropout | 0.1 ~ 0.4 | 正则不足 | 欠拟合、预测平庸 |
dropout 的数值在金融序列上建议比 CV 任务调高一点,因为信噪比低,随机失活能鼓励模型学到冗余特征,减少对某几个特征组合的依赖。
4. 回测不是答题,是挑错:从夏普比率到滑点压力测试
4.1 一套自写的纯 Python 回测最小实现
当你有了预测信号,回测无非是“根据信号调仓,按调仓成本扣费,记录净值曲线”。很多量化平台提供了现成回测框架,但我更建议新手先自己写一个 50 行以内的版本,因为框架包装太多,反而看不清成交假设。
import numpy as np import pandas as pd def backtest(pred_df, cost=0.001, lot=100): """pred_df 含 'signal' 列,取值为 -1, 0, 1""" df = pred_df.copy() df['pos'] = df['signal'].shift(1).clip(-1, 1).fillna(0) # 次日生效 df['ret'] = df['close'].pct_change() df['strategy_ret'] = df['pos'] * df['ret'] - cost * df['pos'].diff().abs() df['equity'] = (1 + df['strategy_ret']).cumprod() return df说明两点:signal.shift(1)是在模拟“用今天收盘后的信号,明天开盘或收盘执行”的真实约束,防止前视偏差;cost * df['pos'].diff().abs()计算的是调仓产生的双边手续费加冲击成本,默认 0.1% 适合按日频交易的主流市场。如果你的策略一小时调一次仓,cost 至少要设到 0.0005 到 0.001 之间,并分买卖方向分别计算。
4.2 用这几个指标读数判断策略是真的还是运气
单看收益曲线没有意义,我一般输出四个指标:年化夏普比率、最大回撤、胜率、盈亏比。夏普比率低于 1 的策略不值得实盘,回撤超过 20% 的模型在情绪上很难坚持,胜率配合盈亏比才能判断策略是偏趋势还是偏震荡。
def sharpe_ratio(returns, periods=252): return np.sqrt(periods) * returns.mean() / returns.std() def max_drawdown(equity): rolling_max = equity.cummax() return ((equity - rolling_max) / rolling_max).min()关于最大回撤,补充一个细节:用日频数据算出的回撤往往比实盘乐观,因为盘中的瞬时回撤不会被日线收盘价记录。上线前我会用 30 分钟 K 线跑一次同样的回测,如果最大回撤比日频结果扩大 30% 以上,说明策略对执行时点敏感,需要进一步压低仓位或者放宽止损。
胜率的统计方式也值得注意。一种是把每个信号独立统计,另一种是按持仓周期统计。前者容易被高频小手数信号干扰,我通常会把信号按signal列分组,先算每组平均收益,再计算组间胜率,这样更接近真实资金曲线。
4.3 参数稳定性检查:滚动切分与参数抖动测试
这是从“模型能用”到“策略可信”的分水岭。把 3 年数据按 1 年窗口切成三段,分别训练、分别回测,如果三段的最大回撤差异很大,优先怀疑是模型过拟合了某一段特殊行情。
其次是参数抖动测试:把hidden_size从 32 换成 16 或 64,把dropout从 0.2 调成 0.3,指标如果大幅变差,说明模型处于参数敏感区,实盘中任何一个环境变化都可能把它推向悬崖。我的习惯是把每次抖动后的夏普比率记成一张表格,观察 80% 的参数组合夏普落在 0.8 到 1.5 之间,才算稳定。
5. 部署前最后的 5 个检查:模型打包、漂移监控与小步验证
5.1 模型状态和特征配置一起打包成 zip
训练完成后,把 PyTorch 权重、特征名称列表、归一化参数、回测报告一并归档。常见的目录结构如下:
strategy_package/ ├── model_best.pt ├── feature_config.json ├── scaler.pkl ├── backtest_report.csv └── README.md将这个目录压缩为 zip 发到生产环境,好处是模型、配置和环境是绑定的,不会出现“特征顺序变了但模型还是旧权重”这种低级错误。我一般还会在feature_config.json里记下特征生成代码的哈希值,一旦特征代码变动,部署脚本就拒绝加载旧模型。
5.2 模型漂移监控:用 PSI 检查输入分布变化
金融市场会在某个时间点切换风格,模型在旧分布上训练,到了新分布上预测偏差会越来越大。常见做法是每周统计线上特征分布与训练时分布的差异,用 PSI(Population Stability Index)打分,分数超过 0.25 就告警并触发重新训练。
def calculate_psi(expected, actual, bins=10): expected_hist, _ = np.histogram(expected, bins=bins, density=True) actual_hist, _ = np.histogram(actual, bins=bins, density=True) psi = np.sum((actual_hist - expected_hist) * np.log(actual_hist / expected_hist)) return psi这个指标原本用于风控领域,但用在特征漂移检测上非常顺手。你可以把expected设为训练集的收盘动量分布,把actual设为最近一周的实时分布,每周计算一次。要注意actual_hist中出现 0 的情况,可以加上一个 1e-6 的小常数再取对数。
5.3 小步验证:模拟盘跑 4 周再决定是否实盘
模型上线第一周,我不会直接接真实资金,而是先跑模拟盘,按相同信号生成订单,但价格用 next bar 的开盘价成交,同时把预测值与实际值的偏差记录下来。如果 4 周后预测方向胜率保持在回测的 80% 以上,再以最小手数进场。最终把所有检查脚本做成每晚定时任务,每天早上 9:15 前收到信号和监控报表,整个闭环才算跑通。
本文还有配套的精品资源,点击获取