ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深度学习股票量化全流程:数据处理、模型训练与回测避坑指南

深度学习股票量化全流程:数据处理、模型训练与回测避坑指南 简介一套面向深度学习与量化交易交叉领域的完整项目源码适用于毕业设计、课程设计或期末大作业等实践场景帮助学习者从零搭建基于神经网络的股票交易系统将金融数据获取、序列建模、策略生成与回测评估串联为完整链路。压缩包共49个文件以42个Python脚本为主体辅以2个Jupyter Notebook交互式示例、1个Markdown说明文档以及若干环境与配置项整体仅2.93MB目录按数据加载、模型构建、策略制定、运行监控等模块分层组织便于定位与二次开发。项目内实现了多种深度学习时序模型结构并配套数据预处理、止损逻辑、回测验证、日志监控等工具可直接在真实行情数据上运行也支持对同一数据集进行不同模型的横向对比。目前已有87人学习下载适合具备一定Python与机器学习基础希望将课本知识落到金融量化场景中的中高级学习者。1. 先别急着解压这份深度学习股票量化交易.zip 到底能解决什么收到一份打包成 zip 的深度学习股票量化交易项目第一反应往往是解压、训练、看净值曲线然后期待它变成自动提款机。实际上这类方案解决的是「把行情数据变成可训练样本、用深度模型提取弱信号、再用回测验证策略是否可交易」的完整闭环而不是一个保证盈利的黑匣子。适合有 Python 基础、想从传统因子模型迁移到深度模型的量化从业者也适合做相关毕设或工程验证的研究者。一个反直觉的结论是日频股价预测本质上是弱信号提取单模型的预测准确率通常在 0.52~0.55 附近真正拉开差距的不是模型有多深而是标签怎么构造、数据怎么切分、交易成本有没有算对。所以解压之前先想清楚三件事数据是否干净、标签是否对齐、回测里有没有藏着未来函数。这三件事决定后面所有工作值不值得做。2. 解压到跑通深度学习量化项目目录结构、环境复现与数据检查拿到任何股票量化相关的 zip 包我都会先建一个独立目录再解压而不是直接丢进当前工作区。常见做法是先静默解压再快速扫一遍文件索引确认里面是数据、代码、文档还是混合体。用下面两条命令完成解剖unzip -q stock_deep_learning_quant.zip -d stock_dl find stock_dl -maxdepth 2 -type f | sort-q参数静默解压-d指定目标目录find输出两层以内的文件清单。重点看是否有data、features、models、notebooks这类目录以及requirements.txt或environment.yml这类环境声明文件。如果 zip 里只有 .ipynb 而没有训练脚本后面复现时得自己补大量胶水代码如果data目录下是 parquet 或 csv还需要确认时间范围和数据源字段。2.1 深度学习量化 zip 里最常见的目录组织与数据约定这类项目最常见的组织结构是data放原始行情features放生成好的特征src放特征工程与训练脚本models放权重文件backtest放回测脚本。命名不一定完全一致但数据流是确定的原始数据 → 特征 → 训练样本 → 模型权重 → 回测报告。拿到手第一步就是按数据流找齐对应脚本缺哪一段就补哪一段。stock_dl/ ├── data/raw/ # 原始日线行情 ├── data/features/ # 预处理后的特征 ├── src/ │ ├── features.py # 特征工程 │ ├── train.py # 模型训练 │ └── backtest.py # 回测 ├── models/ # 权重文件 └── requirements.txt注意有的 zip 为了减小体积会把数据目录留空只留代码。这时要先找数据下载说明或生成脚本别自己脑补字段格式。数据文件的字段通常包含date、code、open、high、low、close、volume、amount可能还带复权因子列或前收盘价。如果 zip 里带features目录先检查特征文件末尾时间戳是否早于标签时间戳这是后期排查未来函数的重要线索。常见的数据时间跨度是三到五年日线太短的数据训练出来很容易过拟合。2.2 环境复现让深度学习训练环境不再依赖“玄学”股票量化训练依赖的包不算多但版本敏感。torch、numpy、pandas、scikit-learn只要有一个版本不对训练结果就会有差异尤其是 CUDA 版 torch 和 CPU 版 torch 的行为并不完全一致。尽量用 conda 建独立环境再按依赖文件安装conda create -n stock_dl python3.9 -y conda activate stock_dl pip install -r requirements.txt如果 requirements.txt 里写的torch1.8实际安装会拿到最新版这通常不是好事。深度学习量化项目复现性本来就差权重随机初始化、数据顺序、dropout 都会带来波动环境版本再不固定结果根本无法横向对比。建议安装后把真实版本锁下来pip freeze requirements_lock.txt之后每次复现都用 lock 文件安装而不是原始 requirements.txt。GPU 环境下还要确认 torch 的 CUDA 版本和驱动匹配常见报错是 cuDNN 不匹配或 CUDA 运行时找不到这类问题属于环境问题不是代码问题。在 lock 文件里固定好 CUDA 对应的 wheel 版本能少踩一半的坑。2.3 数据检查发现数据泄漏的第一道防线打开 zip 后的第一个数据检查不是画 K 线而是确认索引、缺失值和数据口径。很多项目的行情数据来自不同渠道字段名和复权方式都不一样。写一个小脚本做最基础的体检import pandas as pd df pd.read_csv(stock_dl/data/raw/600000.csv, parse_dates[date]) print(df.columns.tolist()) print(df[date].min(), df[date].max()) print(df.isnull().sum()) print(df.duplicated(subset[date]).sum())这段代码分别输出字段名、时间范围、缺失值数和重复日期数。字段名要对齐后再谈其他时间范围决定训练集和测试集怎么切缺失值处理在股票数据里不能简单dropna因为停牌导致的缺失行一旦删除会破坏时间连续性后面的 rolling 窗口计算就会错位。重复日期出现时优先怀疑是复权数据重复或除权除息造成的前收盘差异需要回溯数据源确认。如果数据是前复权直接用收盘价算收益率基本没问题如果是原始价除权日会跳出一个大坑前一日 close 和当日 open 之间出现巨大的假跳空。深度学习模型会把这种由除权造成的跳空当成真实的突变行情来学。所以数据检查时必须确认复权方式并保证特征计算和标签计算用同一套复权口径。这一步没做对后面的模型再高级也只是在拟合错误的数据。3. 标签与特征深度学习量化交易里决定成败的 80% 环节很多人的第一反应是一上来就调 LSTM 层数实际上在股票量化项目里模型结构对结果的影响远小于标签和特征。同一套模型标签从「T1 收益率」换成「截面排名收益」回测结果可能从亏损变成盈利这不是模型玄学而是学习目标的信噪比发生了变化。这一章把标签构造、特征工程和时间切分讲透照着做就能避开大部分训练阶段低层次翻车点。3.1 标签怎么造T1 收益率与截面排名标签最简单的标签是 T1 收益率指用 T 日收盘后可得的信息去预测 T1 日的涨跌。工程实现上要注意方向用close.pct_change()算出来的是 T 日相对 T-1 日的收益率必须shift(-1)才能对齐到 T 日因子对应的 T1 标签。df[ret_1d] df[close].pct_change() df[label_ret] df[ret_1d].shift(-1)shift(-1)把未来收益整行上移让 T 行的特征和 T1 收益配对。尾部会出现 NaN训练前需要dropna。另一个更直观的做法是直接用收盘价错位相除df[label_ret] df[close].shift(-1) / df[close] - 1用shift后的 close 计算逻辑更清晰不容易把 pct_change 的窗口方向搞反。直接拿原始收益做回归存在两个问题一是极端行情样本主导 loss二是不同股票价格区间不同收益分布不齐。更贴近实盘的做法是把原始收益转成截面相对强弱标签即每天对全部股票按收益排序再做归一化df[rank] df.groupby(date)[label_ret].rank(pctTrue) df[label] (df[rank] - 0.5) * 2rank(pctTrue)输出每个股票当天的百分位排名减去 0.5 后得到 [-0.5, 0.5] 的截面相对强弱标签。这消除了大盘普涨普跌的影响模型学到的是「相对谁更强」更贴合多空组合的交易逻辑。注意分组必须是同一交易日日期分组写错排名就失去意义。3.2 特征怎么选量价因子、滚动窗口与截面标准化特征上不需要一上来就堆一百个因子。深度学习模型对冗余特征敏感特征太多训练慢、过拟合早还容易把不稳定噪声学进去。我一般先构造最基础的几类动量、波动、量能。动量用不同窗口的历史收益率波动用收益率滚动标准差量能用量比和成交额均值。全部只用历史窗口计算不允许引入未来数据。for w in [5, 10, 20, 60]: df[fmom_{w}] df[close].pct_change(w) df[fvol_{w}] df[ret_1d].rolling(w).std() df[famt_{w}] df[amount].rolling(w).mean() df[vol_ratio] df[volume] / df[amount].rolling(5).mean().div(df[close])pct_change(w)是 w 日收益率rolling(w).std()是 w 日波动率都按时间窗口滑动不包含未来数据。vol_ratio用五日均量作为基准衡量当日量能是否异常放大。长期停牌后复牌的股票rolling 窗口里会包含大量 NaN需要先决定填充还是剔除不能任其污染特征。特征构造完还要做截面标准化。股票数据不是独立同分布的不同股票的价格范围差异很大直接把原始价格喂给模型会学偏。标准做法是每个交易日对每个特征做 z-scoredef zscore_by_date(feat, df): grouped df.groupby(date)[feat] return (df[feat] - grouped.transform(mean)) / grouped.transform(std)grouped.transform按日期分组计算均值和标准差并广播回原 DataFrame保证特征在每只股票上的截面分布被拉齐。标准化参数只能来自训练集内部不能用全样本计算否则测试集的分布信息会通过均值潜进训练过程。按日截面逐日独立计算本身就是相对安全的标准化方式。3.3 按时间切分随机切分等于给模型开未来之眼深度学习教程里常见的 random split 在股票项目里是致命的。股价序列有强自相关同一个月的数据被拆到训练集和测试集模型等于提前见过同一种市场状态回测指标虚高。正确切分必须严格按时间顺序train_end 2021-01-01 val_end 2022-01-01 train df[df[date] train_end] val df[(df[date] train_end) (df[date] val_end)] test df[df[date] val_end]切分后要验证三段数据没有日期重叠最简单的方法是打印三份数据各自的最小最大日期。除了排序问题还要注意嵌入期训练集最后两周样本和验证集开头两周样本在时间上紧挨着滚动窗口生成的特征会重叠让验证集评估虚高。常见做法是在训练集末尾留出 10 到 20 个交易日的空窗期这段数据不参与训练也不参与验证只用于阻断时间泄漏。4. 模型训练与回测评估从 LSTM 参数到绩效指标的一条线标签、特征和切分都就绪后模型部分反而简单。日频股票数据的样本量通常只有几万到几十万条模型结构的选择范围有限真正影响上线效果的是序列怎么组织、损失函数怎么设、过拟合怎么控制以及最终回测有没有被未来函数污染。这一章从模型选型一路写到绩效指标形成一条可复现的完整链路。4.1 模型选型LSTM、GRU、Transformer 在日频数据上的真实差距股票日频数据每个样本只有几十到几百个时间步样本量有限Transformer 的优势体现不出来反而容易在训练集上快速过拟合。LSTM 和 GRU 是更稳妥的起点。两者对比GRU 参数少、训练快在日频数据上效果基本和 LSTM 持平LSTM 在两到三层堆叠时能稍微捕捉更长的记忆但前提是序列长度足够否则只是增加训练成本。CNN 的一维卷积也能提取局部 K 线形态但做不了长程依赖。选型不需要过度纠结。我一般把 LSTM 当作基线模型序列长度设为 20 到 60 个交易日中间层大小 32 到 128dropout 设在 0.2 到 0.5。股票数据噪声比图像高得多dropout 宁可偏大也不要让模型把短期的随机波动当成规律记住。如果 zip 项目里同时有 LSTM 和 Transformer 两套代码先跑 LSTM把结果当基线再尝试提升。模型输出必须是标量预测最后一层收敛到单输出不要直接输出多分类概率再硬切阈值。4.2 最小可行训练脚本序列构造、网络定义与训练循环先构造序列样本。假设特征和标签已经被整理成按日期排序的二维数组feat和label用滑动窗口生成(N, seq_len, n_features)张量import numpy as np import torch from torch.utils.data import DataLoader, TensorDataset SEQ_LEN 20 def make_sequences(feat, label, seq_lenSEQ_LEN): xs, ys [], [] for i in range(len(feat) - seq_len): xs.append(feat[i : i seq_len]) ys.append(label[i seq_len]) return ( torch.tensor(np.array(xs), dtypetorch.float32), torch.tensor(np.array(ys), dtypetorch.float32), ) X, y make_sequences(feat, label) dataset TensorDataset(X, y) loader DataLoader(dataset, batch_size256, shuffleFalse)shuffleFalse是因为时间序列样本之间有重叠窗口打乱整个数据集会破坏时间连续性。如果想增加随机性可以在每个 epoch 内对 batch 顺序重新洗牌但样本内部的先后顺序不能变。seq_len20对应一个月交易日是日频数据的常见起点取 60 会覆盖季度级动量但训练时间和显存占用都会上升。再定义模型。一个两层 LSTM 加一层全连接输出足够处理大多数日频预测任务import torch.nn as nn class LSTMModel(nn.Module): def __init__(self, n_features, hidden_size64, num_layers2, dropout0.3): super().__init__() self.lstm nn.LSTM( input_sizen_features, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropoutdropout, ) self.fc nn.Linear(hidden_size, 1) def forward(self, x): out, _ self.lstm(x) return self.fc(out[:, -1, :])batch_firstTrue让输入形状变成(batch, seq_len, features)out[:, -1, :]取序列最后一个时间步的隐藏状态作为全连接输入。取最后一个时间步而不是整个序列展开是因为要预测 T1 收益只有最后一个时间步完整编码到 T 日为止的信息。训练循环里损失函数的选择会影响对极端收益的敏感度。日频收益有厚尾特征个别极端行情会把 MSE 拉偏我一般优先用平滑 L1Huber损失坚持用 MSE 的话先对标签做 winsorize 截尾。loss_fn nn.SmoothL1Loss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) for epoch in range(30): model.train() total_loss 0.0 for xb, yb in loader: xb, yb xb.to(device), yb.to(device) pred model(xb).squeeze(-1) loss loss_fn(pred, yb) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() * len(xb) print(fepoch {epoch:02d} loss {total_loss / len(dataset):.6f})device在脚本开头根据torch.cuda.is_available()决定。股票数据序列短batch size 256 足够学习率从 1e-3 起步验证集 loss 连续两三个 epoch 不降就减半。训练时还可以加梯度裁剪防止某次极端样本导致梯度爆炸。训练监控的关键是每周每个 epoch看验证集 loss。训练集 loss 一路下降、验证集 loss 先降后升就是过拟合信号。用早停保存最优权重best_loss float(inf) patience 5 no_improve 0 for epoch in range(30): # ...训练循环... val_loss evaluate(model, val_loader) if val_loss best_loss: best_loss val_loss no_improve 0 torch.save(model.state_dict(), stock_dl/models/best.pth) else: no_improve 1 if no_improve patience: print(fearly stop at epoch {epoch}) break保存权重只存state_dict不要序列化整个模型对象避免 PyTorch 版本升级后类结构变化导致加载失败。验证评估时要开model.eval()关闭 dropoutbatch norm 在股票小批量数据上不稳定优先用 layer norm。4.3 回测三大纪律与绩效指标年化、夏普和最大回撤模型预测要变成净值曲线才算完成量化闭环。回测里最常见的三大纪律是未来函数、前视偏差和幸存者偏差。未来函数指使用了当时不可得的数据典型例子是用全样本归一化参数处理历史数据。前视偏差指用未来信息做当前决策比如按当日收盘价生成信号却在同一根 K 线内用当日最高价验证止损。幸存者偏差在股票池回测里最隐蔽如果股票池按今天的成分股选股历史上被退市的股票从未进入样本结果会系统性高估收益。绩效指标不能只看总收益率。用日频收益序列计算equity (1 strategy_returns).cumprod() n_days len(strategy_returns) annual_return equity.iloc[-1] ** (252 / n_days) - 1 annual_vol strategy_returns.std() * np.sqrt(252) sharpe (strategy_returns.mean() * 252 - 0.02) / annual_vol max_drawdown (equity / equity.cummax() - 1).min()annual_return用复利年化不能用总收益除以年数夏普比率用年化收益减去无风险利率再除以年化波动无风险利率通常取 2% 到 3%。最大回撤用净值除以历史峰值再减一取最小值。回测时间至少覆盖三年以上一年的结果会被极端样本主导。还要关注换手率。策略日均换手超过 100% 时即使单次手续费很低每年摩擦成本也会吞掉大部分超额收益。换手率计算turnover (position - position.shift(1)).abs().sum(axis1) / 2position是持仓权重矩阵每个交易日的调仓幅度求和后除以 2因为一次买入加一次卖出才算一次换手。4.4 交易成本建模手续费、滑点与涨跌停可成交性实盘交易的摩擦成本对日频策略影响巨大。A 股双边合计手续费加印花税约 0.1%~0.15%中小市值股票的冲击成本可能更高。深度学习策略容易偏爱小市值高波动股票滑点会明显大于大盘蓝筹。回测里至少把双边 0.15% 以上的成本计入净值cost_rate 0.0015 net_ret gross_ret - cost_rate * turnover_t检验成本敏感度很简单把成本从 0 调到 0.002看年化从多少掉到多少。如果收益主要来自高频换手成本敏感度会极高这类策略实盘基本不可用。成交假设上第一天收盘生成信号、第二天开盘价成交是比收盘价成交更保守的做法因为收盘价有动量信号和价格存在正相关。涨跌停的一字板也要处理涨停封死时买不进跌停封死时卖不出回测里却默认全部成交这种幻觉最坑人。可以在回测撮合时加入涨停跌停过滤或者至少把这两天的信号强制跳过。5. 深度学习量化项目最常见的翻车点排查5 个让盈利变亏损的隐藏坑训练集表现好、回测后实盘亏损多数不是模型问题而是数据与回测脚本里的隐藏 bug。下面五条来自真实项目的血泪经验按现象、原因、解决顺序写。打开 zip 项目后按这份清单排查能省下大量调参时间。5.1 归一化参数用了全样本测试集在偷看未来现象训练集 loss 和测试集 loss 差距不大但测试集预测值整体接近 0IC 却高得离谱。 原因特征标准化时用全数据的均值和标准差测试集的分布信息已经被提前注入模型在评测时“见过”了未来的统计量。 解决只从训练集计算均值和标准差存成 json 或 npy 文件验证集和测试集加载后直接用这套参数。检查方法是打印 test 特征标准化前后的均值如果标准化后均值明显偏离 0就是标准化参数用错了样本。5.2 标签偏移用 T 日因子预测 T 日收益现象训练曲线很漂亮一到样本外立刻衰减。 原因某些项目直接用pct_change()当标签忘了shift(-1)特征和标签同一天模型学的是同步映射而不是预测。当日涨跌和当日动量因子天然相关拟合看似有效实则是把“答案”和“题目”放在同一行。 解决构造标签后打印一行确认2020-01-05 这行的 label 必须等于 2020-01-06 的收益。更稳的方式是用close.shift(-1) / close - 1并在训练前检查特征和标签当日的相关性如果相关系数接近 1基本可以断定标签偏移了。5.3 一字涨停买不进、一字跌停卖不出现象回测净值在牛市里完美复利实盘委托却大量不成交。 原因回测假设任何价位都能成交忽略了一字板。涨停板封单巨大时挂单买不进跌停板封死时挂单卖不出但回测把这些单子按成交计入收益。 解决在回测撮合逻辑里加涨跌停过滤。当日涨停且收盘封住时禁止次日开盘买入当日跌停封死时禁止次日开盘卖出。最简单的过滤df[limit_up] df[close] df[close].shift(1) * 1.095 - 1e-4 df[limit_down] df[close] df[close].shift(1) * 0.905 1e-4生成信号时检查标志位命中涨停且是买入就跳过命中跌停且是卖出就跳过。5.4 zip 解压乱码与中文路径环境问题先于模型问题现象代码能跑读数据时提示 FileNotFoundError或者 pandas 读进来列名全是乱码。 原因zip 内文件编码通常是 GBKLinux 下用默认 UTF-8 解压中文目录名和文件名全部乱码Windows 下路径包含中文时部分深度学习库和编译器也会踩坑。 解决Linux 下解压时指定 GBK 编码Windows 下用带有编码选项的压缩工具解压或者解压后把中文目录统一改成英文路径。代码里所有路径尽量用pathlib或相对路径避免硬编码含中文的绝对路径。5.5 复权因子不一致除权日制造假跳空现象特征波动率在除权日附近突然异常放大模型对这些日期产生极端预测。 原因数据源用前复权但特征里混入了未复权的最高最低价或不同文件一个用前复权、一个用后复权拼接后时间序列断裂。 解决全项目统一复权口径。检查方法很简单把close.pct_change()绝对值排序看最大的几个是否集中在除权日附近。如果无法统一至少给除权日加一个事件标记让模型知道这一天是除权造成的跳空而不是真实涨跌。6. 从预测到可交易策略用 IC 和滚动再训练验证信号预测模型在测试集上指标好只是第一步。量化交易要的是信号在时间截面上的稳定性以及面对市场风格变化时的抗衰减能力。投入实盘之前IC 验证和滚动再训练是最值得做的两件事。6.1 IC 与分层回测判断信号是否可交易IC 是每个交易日预测值和实际收益的截面相关系数直接反映信号每天的有效性。常用 Spearman 相关消除量纲和异常值影响ic test.groupby(date).apply( lambda x: x[pred].corr(x[label], methodspearman) ) print(fIC mean: {ic.mean():.4f}, IC std: {ic.std():.4f}, IR: {ic.mean() / ic.std():.4f})IC 均值超过 0.03 就说明信号具备实际使用价值IR 大于 0.5 说明稳定性尚可。IC 接近 0 甚至为负翻回去查特征和标签比继续调参更划算。分层回测是把预测值按十分位分组看每组收益是否单调递增如果预测最强的组反而收益最差说明模型学到了反向信号可以考虑翻转信号而不是直接弃用模型。6.2 滚动再训练与模型衰减股票市场非线性且非平稳模型学到的规律会随时间衰减。常见做法是每 20 到 40 个交易日用最新数据重新训练一次或采用滑动窗口增量训练。每次重训前固定随机种子维护一份验证集比较新旧模型在最近 60 个交易日的 IC。新模型没有显著提升时宁可用旧模型避免频繁更换模型带来的隐性交易成本。最后一件事回测再漂亮也要先放到模拟盘或最小资金量下跑一段时间重点观察成交滑点和信号衰减是否符合回测预期。我自己踩过最大的坑是被一份回测夏普很高的 zip 项目带着跑最后发现它在涨停板上一律假设成交把策略收益凭空抬了一倍。把回测里的隐藏假设一个个挖出来比换更复杂的模型重要得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表