最早接触神经网络做交易时我以为把价格序列丢进 LSTM 就能自动找到规律。结果模型在训练集上精准得吓人一到样本外就崩成随机猜测。后来我才意识到深度学习在算法交易里的最大门槛不是模型本身而是数据切分、标签设计和特征构造这套看不见的工程细节。这篇文章是我从零开始用 Python 深度学习构建算法交易策略的完整复盘适合已经会用 pandas 处理行情数据、熟悉 Python 基础但对“把神经网络接到交易场景里”还没有系统思路的读者。我会把目标设计、数据边界、特征工程、模型训练和回测验证从头到尾捋一遍也会把那些让我亏过时间的坑单独拎出来说清楚。1. 传统因子模型为什么不够用从线性视角到非线性视角1.1 当线性回归遇上价格数据做量化交易的人对这套流程应该不陌生拿收益率当因变量拿估值、动量、波动率这类因子当自变量塞进线性回归或者简单决策树里得到一个方向打分然后排序选标的。这个思路本身没问题但它有一个隐含假设因子和未来收益之间的关系是线性的或者至少是可加性的。可是行情数据的真实结构往往不是这样。举个例子一个动量因子在趋势行情里是正贡献在震荡行情里可能变成反向指标波动率因子在低波环境下的预测力很强高波环境下又会失效。同一个因子在不同市场状态下对收益的影响方向甚至可能翻转。这种“条件式”的关系线性模型表达不出来因为线性模型只能给每个因子配一个固定权重。我在做传统因子模型时最常遇到的情况是单因子测试 IC 均值不高但换一种市场状态分组后信号效果差异巨大。这说明信息其实就在数据里只是没有被模型结构捕捉到。这也是我开始转向神经网络的原因——我需要一个能自动挖掘交互效应和非线性结构的工具而不是把所有关系强行压成线性权重。1.2 神经网络的“万能近似”在交易里意味着什么神经网络本质上是一个带大量可学习参数的复合函数。只要层数和神经元数量足够它可以逼近任意复杂的连续函数。这个性质叫万能近似定理。放在交易场景里理解就是它有机会自动发现“低波动强动量量能配合”这类组合条件对收益的影响而不需要我把这些交互项手工构造出来。但这里有一个关键认知万能近似是数学上的能力不是实践中的承诺。样本量有限、噪声极大、信噪比极低的行情数据会让模型很容易学到无关的随机模式也就是过拟合。我在跑第一批实验时训练集 AUROC 能到 0.65 以上看起来很不错但验证集只有 0.50 上下标准的随机水平。后来我才明白神经网络在交易里的价值不在于“无中生有创造 Alpha”而在于“用够复杂的结构去逼近那些用传统规则无法准确描述的关系”。1.3 深度学习适合解决的交易问题边界不是所有交易问题都该用神经网络。如果要做的是类似“均线金叉后上涨概率”这种强规则场景简单统计就能解决引入深度学习反而容易因为噪声过拟合。根据我的经验神经网络更适合下面这几类问题特征之间关系复杂、存在明显交互效应但无法手工指定规则样本量足够大至少几十万条以上的训练样本预测目标本身有弱可预测性而不是完全随机你愿意投入工程时间处理数据切分和验证设计而不只是调模型。反过来如果数据量很小、行情结构非常单一、或者你只是想快速验证某个规则有效性直接用经典因子模型或者滚动统计方法会更稳。神经网络不是银弹它是一个需要完整工程配套的复杂工具。2. 模型还没写先定这三件事目标、数据边界和时间切分2.1 标签设计预测收益方向还是预测收益大小写第一版模型时很多人第一反应是“预测明天的收盘价”。这个目标我强烈不建议直接用回归去做。价格序列本身是非平稳的今天的绝对价格和十年前的绝对价格没有可比性模型要同时学会“市场涨跌”和“绝对价格尺度”学习负担极大而且很容易被价格水平带偏。我更推荐的做法是构造相对标签。最常见的一种是把未来 N 期收益率作为连续标签做回归另一种是把未来 N 期收益率的正负变成二分类标签。选哪种取决于你的策略逻辑如果你后面要做仓位大小差异化用回归输出更自然如果你只打算生成“开多/开空/空仓”的方向信号二分类更直接。我给实际项目做标签时会考虑两个细节第一预测期限 N 要和你的调仓频率匹配做日内频繁调仓就别用未来 5 天收益做标签信号早衰减没了第二样本类别不均衡要提前看清楚市场大部分时候涨幅区间很小如果强行切成“涨/跌”二分类两类样本可能比例接近但收益幅度差异很大模型会忽略那些幅度大但方向相同的情况。2.2 时间序列不能随机乱切分组与滚动窗口这是我最想强调的一点。很多刚接触深度学习的人默认把数据打成随机样本训练集、验证集、测试集三切分这在 CV 任务里没问题但在金融时间序列里是灾难。原因很简单相邻交易日的样本高度相关。比如用 20 天窗口做特征第 100 天和第 101 天的样本里有 19 天特征重叠。如果随机切分训练集里可能混进“未来”数据验证集里也可能出现和训练集几乎重复的样本。模型在验证集上的成绩会虚高实盘时立刻现形。我现在的做法是严格按时间顺序切分并且给样本分组。具体来说训练集用 2018 年至 2021 年数据验证集用 2022 年数据测试集用 2023 年上半年数据训练集内部再做一次滚动前推验证而不是单次切分。这样每一个样本的时间位置都是严格确定的验证集里的任意一个样本都不可能在训练集里找到“未来的自己”。这个环节看起来简单但数据泄漏问题有七成以上出在切分逻辑上。2.3 数据泄漏的高发场景归一化、未来函数、重采样除了切分数据泄漏还会潜伏在三个操作里我逐个踩过。第一个是归一化参数。常见的 z-score 归一化需要计算均值和标准差如果你用全样本的均值和标准差去缩放训练集和测试集测试集的分布信息就已经被模型看到了。正确做法是只用训练集统计量去归一化验证集和测试集。第二个是未来函数。构造特征时如果某个指标用了未来一段时间的价格计算比如“当前价格相对未来 5 日均值的变化率”这个特征在回测里会非常“准”但实盘永远拿不到当天的未来数据。我查代码时发现很多第三方指标库默认实现里就可能引入未来窗口尤其是在计算“当日回看 N 期”和“N 期后”这类需求时容易混淆。每写一个特征都要问自己一句“当前这个时刻我真的能拿到这个数吗”第三个是重采样。把分钟数据重采样成日线时会有个隐性问题当天最后一根分钟线和收盘价之间的信息差。如果重采样逻辑把当天完整价格路径都用尽了某些特征其实隐含了收盘后的信息实盘策略很难在收盘前完成交易。这个问题在做高频或日内策略时尤其严重做日线级别虽然影响小一些但也要心里有数。3. 技术栈选择我用什么库、为什么这样选3.1 Python 环境与核心依赖我的开发环境是 Python 3.10配合以下核心库模块用途pandas / numpy数据处理、滚动特征计算PyTorch神经网络建模与训练scikit-learn分类/回归评估指标、简单预处理joblib模型持久化某开源行情接口获取日线、分钟线行情数据很多人会纠结要不要安装 TA-Lib我的建议是如果只是做最基础的 MA、RSI、布林带这类指标用 pandas 的 rolling 方法自己实现就够还能保证不引入未来函数。等指标复杂到需要大量 C 扩展计算时再考虑引入专门的指标库。3.2 选 PyTorch 而不是其他框架的理由这个选择更多是基于日常维护的考虑。PyTorch 的动态图机制让我在调试序列模型时能随时打印中间变量对理解 LSTM 每一时刻的状态很有帮助。同时它的 Dataset 和 DataLoader 机制对时间序列样本的采样控制比较方便我可以自定义按时间顺序取样本不用担心框架默认打乱。训练预测模型用的是分类交叉熵输出层是 2 个神经元对应未来 N 期收益正负两个方向。这种设计在代码上最直观也方便和其他模型做对比实验。layer 1 的 dropout0.4 是后来调参调出来的。一开始我设 0.2训练集和验证集差距还是大提高到 0.4 之后明显缓解。同时我给 LSTM 输出接了一个 BatchNorm 层它能让梯度在深层网络中更容易传播收敛速度比不接快不少。hidden_size32 在四维特征下足够用不需要一味加大网络复杂度。4.4 训练过程中的过拟合信号与对策我第一个 LSTM 模型训练到 15 个 epoch 时训练集损失下降到 0.28验证集损失却停在 0.69 附近不再下降。这个 0.41 的 gap 就是典型的过拟合信号。当时我做了三件事加大 dropout从 0.2 提到 0.4把 LSTM 层数从 2 层减回 1 层减少序列层级堆叠加入早停验证集损失连续 5 个 epoch 不改善就停止训练。改完之后验证集损失下降到 0.58 左右虽然预测准确率依然不高但至少训练集和验证集之间的差距缩小了。这个阶段我的判断标准不是“涨了多少”而是“验证集评估是否稳定复现”。如果验证集结果一会儿好一会儿差说明样本量不够或者特征噪声太大需要回到特征工程环节重新思考。5. 从零写一个方向预测模型MLP 版本到 LSTM 版本的工程实现5.1 设计思路为什么最终选了 LSTM在序列建模之前我试过把 20 天的特征摊平成 80 维向量直接喂给 MLP。结果训练起来很慢而且效果一般。原因很好理解MLP 把每一天的特征当作完全独立的输入没有建模时间顺序关系的能力。而行情数据里真正有价值的是“趋势的延续”“动能的衰减”这类顺序性信息。LSTM长短期记忆网络通过门控结构维护一个细胞状态可以记住几十步之前的有效信息同时丢掉无关噪声。在预测未来 N 期收益方向这个任务里它天然适合捕捉“过去一段时间价格如何演变”这类问题。这也是我选择 LSTM 作为系列第一代模型的原因。5.2 模型定义代码下面这个是我实际在用的模型结构输入是 shape 为batch_size, 20, 40的张量20 是时间步长40 是特征维度。输出是 2 分类 logits。import torch import torch.nn as nn class DirectionLSTM(nn.Module): def __init__(self, input_size40, hidden_size32, num_layers1, num_classes2): super(DirectionLSTM, self).__init__() self.lstm nn.LSTM( input_sizeinput_size, hidden_sizehidden_size, num_layersnum_layers, batch_firstTrue, dropout0, ) self.bn nn.BatchNorm1d(hidden_size) self.dropout nn.Dropout(0.4) self.fc nn.Linear(hidden_size, num_classes) def forward(self, x): out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的输出 last_out out[:, -1, :] last_out self.bn(last_out) last_out self.dropout(last_out) logits self.fc(last_out) return logits这里有一个我反复确认过的细节batch_firstTrue让输入维度更直观取out[:, -1, :]表示把最后一个时间步的隐藏状态取出代表模型对整段序列的最终解读。如果任务换成预测未来多期收益我会把这里改成h_n的组合输出但方向预测用最后一个隐状态已经足够。5.3 训练循环与早停训练循环本身不复杂但有几个点需要处理好学习率调度、类别权重、早停。import torch.optim as optim from torch.utils.data import DataLoader, TensorDataset def train_model(model, train_dataset, val_dataset, epochs30, lr3e-4): train_loader DataLoader(train_dataset, batch_size256, shuffleTrue) val_loader DataLoader(val_dataset, batch_size1024, shuffleFalse) optimizer optim.Adam(model.parameters(), lrlr) scheduler optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience3 ) class_weight torch.tensor([1.0, 1.0]) # 如果样本不平衡再调整 criterion nn.CrossEntropyLoss(weightclass_weight) best_val_loss float(inf) patience_counter 0 for epoch in range(epochs): model.train() train_loss 0.0 for x_batch, y_batch in train_loader: optimizer.zero_grad() logits model(x_batch) loss criterion(logits, y_batch) loss.backward()n optimizer.step() train_loss loss.item() * x_batch.size(0) model.eval() val_loss 0.0 correct 0 total 0 with torch.no_grad(): for x_batch, y_batch in val_loader: logits model(x_batch) loss criterion(logits, y_batch) val_loss loss.item() * x_batch.size(0) preds logits.argmax(dim1) correct (preds y_batch).sum().item() total y_batch.size(0) val_loss / total train_loss / len(train_dataset) val_acc correct / total print(fEpoch {epoch1}: train_loss{train_loss:.4f}, val_loss{val_loss:.4f}, val_acc{val_acc:.4f}) scheduler.step(val_loss) if val_loss best_val_loss: best_val_loss val_loss patience_counter 0 torch.save(model.state_dict(), best_model.pth) else: patience_counter 1 if patience_counter 5: print(early stopping) break这里shuffleTrue只用在训练集验证集必须保持原始时间顺序。训练时打乱样本能让每个 batch 分布更接近整体分布加速收敛验证时如果打乱了评估结果也不受影响但从时间一致性角度我习惯保持不变。5.4 我踩过的训练坑类别不平衡与学习率过高第一次训练时我没注意类别权重结果模型几乎把所有样本都预测成“上涨”。检查后发现虽然总体涨跌样本比例接近 50:50但收益幅度大的上涨样本占比更高模型学到的决策边界偏向更容易降低整体损失的类别。后来我在训练时增加了一个类别权重校准步骤先按训练集统计两个类别的比例再把class_weight设为反比让模型更关注少数类。学习率方面Adam 用 3e-4 是我测试下来比较稳的起点。有段时间我图快用了 1e-3结果 LSTM 的梯度在序列反传时不太稳定训练损失反复横跳。后来加了 ReduceLROnPlateau让它每 3 个 epoch 没改善就降低一半学习率收敛稳定很多。6. 回测结果好看不等于能赚钱验证环节最容易踩的坑6.1 准确率不是交易指标模型在验证集上准确率 53%看起来比随机高一点但能赚钱吗不一定。交易里最怕的是“预测对的时候赚小钱、预测错的时候亏大钱”。准确率只统计方向预测的对错次数完全不考虑盈亏幅度。我见过不少模型方向准确率有 55%但错的样本往往是大幅下跌或者大幅上涨的反向错的幅度大过对的幅度最终收益曲线还是往下走。所以我在评估模型时还会同时看三组指标预测正确的平均收益、预测错误的平均收益、以及按预测概率分层的收益单调性。如果高概率预测组的未来收益明显高于低概率组模型才算真的有信息量。6.2 前视偏差与幸存者偏差回测阶段最容易让人产生“我已经稳了”错觉的就是前视偏差。举个例子如果你用某一天的完整日线数据去生成特征而模型信号是在收盘后计算的那你的回测下单时间必须设在收盘价之后或者第二天开盘。很多人直接把当日信号和当日收盘价对齐实际上是在用当天的结果“预测”当天回测收益自然高得离谱。幸存者偏差则是标的池选择的问题。如果回测时只选了现在还存在的标的那些已经退市或者长期停牌的标的完全没有进入样本策略的普适性就被高估了。做全市场选股类策略时处理退市数据是个非常麻烦但不得不做的事否则业绩曲线的真实性要打折扣。6.3 滚动前推回测我常用的验证方法我现在的标准流程是滚动前推验证。拿日线数据举例第一次用 2018-2021 年训练2022 年验证第二次把训练集扩到 2022 年验证 2023 年上半年之后再扩到 2023 年上半年验证 2023 年下半年。每滚动一次就相当于经历一次“未来”。这样做的好处很明显模型每换一段新的验证期都会面对不同波动环境能看出它对市场状态变化的适应能力。如果模型只在某一段牛市行情里有效滚动验证会立刻暴露问题。我曾经的模型在 2022 年验证集上还行但滚到 2023 年却大幅衰减说明它学习到的规律不稳定。遇到这种情况我会回到标签设计和特征构造去重新思考而不是硬调超参。6.4 成本模型手续费和滑点会吃掉利润很多第一次写回测的人都忽略交易成本。觉得买卖一次只有千分之几但高频换仓下成本是复利级别的。日线策略一次完整的开平仓手续费加滑点我一般按双边 0.1% 到 0.2% 估算。如果模型信号每天变化一次一年 200 多个交易日成本会非常惊人。我第一次跑完整回测时把成本设成 0收益曲线 45 度向上人差点飘起来。后来加上 0.15% 的成本后实际净收益直接腰斩再腰斩。从那以后我养成了习惯任何回测结果都必须标注“含成本”和“不含成本”两条收益曲线并且用含成本曲线作为决策依据。7. 从“能跑”到“能持续跑”的几点个人心得7.1 模型更新频率与信号衰减我训练完模型后的第一感受是“它怎么这么快就失效了”。一个模型在验证期结束后实际部署阶段的效果会随着时间推移逐渐衰退。原因不复杂市场参与者在变交易规则在变宏观环境在变所有历史规律都会老化。我的应对方案是固定更新节奏每周五收盘后用最新数据重新训练一次同时保留过去几个版本的模型做集成投票避免单一版本在某一周突然崩溃。序列数据本来就不是平稳的模型更新频率本身就是策略的一部分不能当作事后补救。7.2 组合与仓位管理才是真正的护城河走到这一步我最深刻的体会是单标的的预测模型再准也不如组合层面的设计重要。神经网络的预测信号只是生成一个“倾向”真正决定收益曲线形态的是怎么分配仓位、怎么控制回撤、怎么在不同策略之间分散风险。我现在会把单个模型当作一个信号源然后同时跑多个不相关信号源再用风险平价或者波动率目标去分配权重。这样单个模型的失效对整体组合的冲击是有限的。深度学习模型给出的概率本质上只是一个权重输入不是决策的全部。7.3 给同样走这条路的你几个建议回望整个第一代策略的开发过程我最想总结的是下面几条把 70% 的时间花在数据理解、标签设计和验证流程上模型只占 20%剩下的 10% 才是调参任何验证集上的“惊喜”都要先怀疑数据泄漏而不是庆祝发现了 Alpha回测结果必须完整覆盖成本、滑点、停牌、退市这些现实因素否则没有参考价值模型越复杂样本量要求越高先跑通简单模型建立基线再逐步增加复杂度靠单一模型很难持续赚钱构建多信号组合才是更稳健的方向。这个系列的第一篇到这里核心是把“用深度学习做交易”这条路的完整工程框架盘清楚。后面我会继续展开特征工程的更多坑、模型集成的方法以及实盘部署中遇到的那些教科书里不会写的细节。每一步我都踩过泥写出来希望你能走得快一点。