ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

深度学习交易模型实盘翻车?从数据管道到成本建模的工程化避坑指南

深度学习交易模型实盘翻车?从数据管道到成本建模的工程化避坑指南

1. 从"能跑通"到"能赚钱":为什么你的深度学习交易模型总在实盘翻车

很多人第一次用 Python 搭出 LSTM 预测股价的模型时,都会经历一个极其相似的兴奋期:训练集上 loss 一路下降,验证集准确率看着也不错,回测曲线平滑向上,年化收益漂亮得不像话。然后兴冲冲把模型接到模拟盘或者小资金实盘,结果没几天就开始怀疑人生——回撤比回测大得多,胜率断崖式下跌,手续费和滑点像蚂蚁搬家一样把利润啃光。

这个现象太普遍了,普遍到几乎成了算法交易圈的"新手墙"。问题不在于你的 LSTM 写得不对,也不在于 PyTorch 版本装错了,而在于从"预测准确"到"交易盈利"之间,隔着一条被绝大多数教程刻意忽略的鸿沟。这条鸿沟里埋着三类东西:数据层面的幸存者偏差与前视偏差、模型层面的过拟合与分布漂移、执行层面的成本建模与仓位管理。任何一环没处理好,回测再漂亮都是自欺欺人。

这篇内容承接前两篇关于神经网络与算法交易的基础铺垫,重点聊第三块——当你已经能写出一个能跑的深度学习模型之后,怎么把它打磨成一个在真实市场里不至于立刻崩掉的交易策略。适合已经掌握 Python、了解基本神经网络结构、但实盘经验还不多的朋友。我会把踩过的坑、验证过的做法、以及那些"文档里不会写但实盘会教你做人"的细节,尽量讲透。

需要先说明一点:下面所有涉及具体标的、参数、收益的数字,都是基于常见实践构造的示例,用于说明方法论,不构成任何投资建议。市场里没有圣杯,只有不断迭代的工程流程。

2. 数据管道:深度学习交易策略里最容易被做手脚的地方

2.1 前视偏差:你以为的"历史数据"其实偷看了未来

前视偏差是量化交易里最隐蔽也最致命的错误。举个最常见的例子:你用日线数据训练模型,特征里包含了当天的收盘价、最高价、最低价,标签是"明天涨还是跌"。看起来没问题对吧?但如果你在构造特征时,不小心把当天收盘后才知道的信息用在了当天开盘时的决策上,模型在回测里就会表现得异常聪明,因为它"偷看"了未来。

更隐蔽的一种情况是标准化。很多人习惯在划分训练集和测试集之前,对全量数据做 z-score 标准化或者归一化。这一步看似无害,实际上测试集的均值和方差信息已经泄露到了训练过程里。正确的做法是:只用训练集计算均值和标准差,然后把这个统计量应用到验证集和测试集上。如果做滚动回测,那每个滚动窗口内的标准化参数都必须重新计算,绝不能跨窗口复用。

我在早期项目里就栽过这个跟头。当时用某指数过去五年的数据训练一个多层感知机,回测夏普比率高得离谱,后来逐行检查代码才发现,特征工程里有一个"过去20日波动率"的计算,用的窗口是rolling(20).std(),但我在做 train/test split 之前就把整个 DataFrame 算好了。这意味着测试集第一个样本的波动率,实际上用到了它之前20天的数据——而这些数据在真实交易时是拿不到的。修正之后,夏普直接腰斩。

提示:判断是否存在前视偏差的一个笨办法是,把数据管道里每一个特征的计算逻辑写下来,然后问自己:"在真实交易的那个时间点,这个数字我能不能算出来?"如果答案是"不能"或者"不确定",那就需要重新设计。

2.2 幸存者偏差与样本选择:你回测的标的可能早就退市了

另一个常被忽略的问题是标的池的构造。如果你只用当前还在交易的股票、期货合约或者币种来构建历史数据集,那么那些已经退市、下架、归零的标的就被系统性地排除了。这会导致回测结果偏乐观,因为你的模型从来没有机会在那些"死亡标的"上亏钱。

处理这个问题没有银弹,但有几个务实的方向。第一,尽量使用包含退市标的的历史数据库,很多专业数据源会提供 point-in-time 的成分股列表。第二,如果拿不到这种数据,那就在回测时对标的池做动态维护:每个调仓周期,只使用当时确实可交易的标的,而不是用今天的列表去回溯历史。第三,对于加密货币这类退市频繁的领域,手动维护一个"已下架币种"列表,把它们也纳入回测,哪怕数据质量差一点,也比完全忽略要好。

2.3 数据频率与标签设计:日线、分钟线还是 tick,标签怎么定

数据频率的选择直接决定了策略的容量和成本结构。日线数据噪声相对小,适合中低频策略,交易成本占比低,但信号衰减慢,竞争激烈。分钟线和 tick 数据信息量大,但噪声也大,对模型的抗噪能力要求高,而且交易频率上去之后,手续费和滑点会迅速吃掉利润。

标签设计同样关键。最常见的二分类标签是"下一期收益率是否大于0",但这个定义太粗糙。更合理的做法是设定一个阈值,比如下一期收益率大于某个值才标记为正样本,小于负阈值标记为负样本,中间区域丢弃或者单独处理。这样做的好处是让模型专注于那些"值得交易"的机会,而不是在微小波动上浪费预测能力。

还有一个细节是标签的时间对齐。如果你用 t 时刻的特征预测 t+1 时刻的收益,那么标签应该是(close[t+1] - close[t]) / close[t],而不是(close[t+1] - open[t+1]) / open[t+1]。前者对应"收盘买入、次日收盘卖出"的逻辑,后者对应"次日开盘买入、次日收盘卖出",两者的交易假设完全不同。很多教程在这里含糊其辞,导致读者回测时用了一套逻辑,实盘时又用了另一套。

3. 模型结构选型:LSTM、Transformer 还是简单的全连接,别被时髦带偏

3.1 序列建模的三种主流选择及其适用边界

在金融时序预测里,常见的深度学习结构大致分三类:循环神经网络家族(LSTM、GRU)、注意力机制家族(Transformer、Informer 等)、以及把时序问题转化为表格问题的全连接网络或梯度提升树。

LSTM 和 GRU 的优势在于对局部时序依赖的建模能力,参数量相对可控,训练速度快,适合中小规模数据集。缺点是难以捕捉超长距离依赖,而且对输入序列的长度比较敏感。Transformer 类模型在长序列建模上更强,但参数量大,对数据量要求高,金融数据往往不够它"吃饱",容易过拟合。全连接网络和梯度提升树则完全放弃了对时序结构的显式建模,转而依赖手工构造的滞后特征和滚动统计量,在很多中低频策略里反而表现稳健。

我的经验是:如果数据量在几万到几十万条样本之间,特征维度几十到几百,LSTM 或 GRU 通常是性价比最高的选择。Transformer 更适合有海量高频数据、且计算资源充足的场景。而如果特征工程做得足够扎实,梯度提升树在很多任务上并不输深度学习模型,而且可解释性更好,调试起来也更快。

3.2 输入窗口长度的确定:不是越长越好

输入窗口长度(lookback window)是 LSTM 类模型的关键超参数。很多人直觉上认为窗口越长,模型能看到的历史信息越多,效果应该越好。但实际恰恰相反:窗口太长会引入大量噪声,稀释有效信号,还会增加计算负担和过拟合风险。

确定窗口长度的一个实用方法是自相关分析。计算目标变量(比如收益率)的自相关系数,找到自相关显著不为零的最大滞后阶数,以此作为窗口长度的参考上限。另一个方法是网格搜索,但要注意在验证集上做,而不是测试集。我通常会在 10 到 60 个时间步之间做粗筛,然后对表现最好的几个值做细调。

还有一个容易被忽略的点是窗口内的数据对齐。如果你用的是日线数据,但某些交易日停牌或者没有交易,那么简单的rolling(window)会把停牌日也计入窗口,导致实际时间跨度不一致。正确的做法是先对数据进行重采样和填充,确保每个窗口对应相同的时间长度,或者在窗口内做掩码处理。

3.3 输出层设计:回归、分类还是多任务

输出层的设计取决于你的交易逻辑。如果策略是"预测收益率大小然后按比例建仓",那输出层应该是回归,损失函数用 MSE 或 Huber。如果策略是"预测涨跌方向然后全仓或空仓",那输出层是二分类,损失函数用交叉熵。如果策略同时关心方向和幅度,可以考虑多任务学习,共享底层特征,分别输出方向概率和预期收益。

这里有个实操上的坑:回归任务的输出尺度问题。金融收益率通常很小,比如日收益率在 0.01 量级,如果直接回归原始收益率,MSE 损失会非常小,梯度也小,训练容易停滞。常见的做法是对标签做标准化,或者把损失函数换成对尺度不敏感的形式。另一个做法是预测排序而不是绝对值,比如用 pairwise ranking loss,让模型学会"哪些标的相对更好",而不是精确预测每个标的的收益。

4. 训练与验证:时间序列交叉验证和那些让回测失真的细节

4.1 为什么不能随机划分训练集和测试集

这是时间序列任务和普通机器学习任务最大的区别之一。随机划分(比如train_test_split默认的 shuffle)会破坏时间顺序,导致训练集里包含了测试集之后的信息。在金融场景下,这相当于让模型在训练时"看到未来",回测结果会严重虚高。

正确的做法是按时间顺序划分:用较早的数据训练,较晚的数据验证和测试。如果要做交叉验证,必须用滚动窗口或扩展窗口的方式,确保每次验证时,训练数据都在验证数据之前。scikit-learn 提供了TimeSeriesSplit,但它的默认行为是扩展窗口,如果你的策略对训练集长度敏感,可能需要自己实现滚动窗口版本。

4.2 验证集上的早停与模型选择

早停(early stopping)是防止过拟合的常用手段,但在时间序列任务里,早停的验证集选择需要格外小心。如果验证集和测试集在时间上太接近,模型可能会在验证集上过拟合,导致测试集表现不佳。一个务实的做法是:在训练集内部再切出一段"验证集",用于早停和超参数调优,测试集只在最后评估时用一次。如果数据量足够,还可以做多折滚动验证,取平均表现作为模型选择的依据。

另一个细节是模型权重的保存策略。很多人习惯保存验证集 loss 最低的那个 epoch 的权重,但这在金融数据上未必最优。因为金融数据的噪声大,验证集 loss 最低的点可能只是恰好拟合了某段噪声。更稳健的做法是保存多个 epoch 的权重,然后在验证集上评估它们的交易表现(比如夏普比率、最大回撤),选择交易表现最好的那个,而不是 loss 最低的那个。

4.3 学习率、批大小与正则化的实战配置

学习率方面,Adam 或 AdamW 通常是默认选择,初始学习率设在 1e-3 到 1e-4 之间比较常见。如果训练不稳定,可以配合学习率预热(warmup)和余弦退火(cosine annealing)。批大小在金融数据上不宜太大,32 到 128 之间比较合适,太大的批会降低梯度噪声,反而容易陷入尖锐的局部最优。

正则化方面,Dropout 在 LSTM 里通常加在输入和输出之间,比例 0.1 到 0.3。权重衰减(weight decay)用 1e-5 到 1e-4。另外,梯度裁剪(gradient clipping)在 RNN 类模型里几乎是必须的,阈值一般设在 1.0 到 5.0 之间,防止梯度爆炸。

还有一个容易被忽略的正则化手段是数据增强。在图像领域,旋转、裁剪、加噪声都是常见操作。在金融时序里,可以尝试对输入序列做轻微的时间扭曲、幅度缩放,或者对多个标的的序列做混合。但要注意,增强后的数据必须仍然符合金融数据的统计特性,不能引入不切实际的模式。

5. 回测引擎与成本建模:滑点、手续费和那些让收益归零的隐形杀手

5.1 回测框架的选择:自己写还是用现成的

常见的开源回测框架有 Backtrader、VectorBT、Zipline 等,各有优劣。Backtrader 功能全面,社区活跃,但事件驱动架构在大量标的和高频数据上性能一般。VectorBT 基于向量化运算,速度快,适合做参数扫描和快速原型,但对复杂订单逻辑的支持不如 Backtrader。Zipline 曾经很流行,但维护节奏放缓,新项目里用得少了。

我的建议是:如果策略逻辑不复杂,先用 VectorBT 做快速验证,确认思路可行后再用 Backtrader 或自研引擎做精细回测。自研引擎的好处是完全可控,能精确实现你想要的成本模型和订单逻辑,坏处是开发和调试成本高,容易在细节上出错。对于大多数个人开发者,从 VectorBT 起步是更务实的选择。

5.2 滑点建模:固定值、比例还是基于订单簿

滑点是回测和实盘差距的主要来源之一。最简单的建模方式是固定滑点,比如每次交易扣除 0.01% 或 0.05%。这种方式实现简单,但过于粗糙,无法反映不同标的、不同时段的流动性差异。

更精细的做法是按比例建模,滑点与交易金额或成交量挂钩。比如设定滑点为成交额的 0.02%,或者根据标的的历史平均买卖价差来估算。如果数据允许,还可以基于订单簿深度做更精确的模拟,但这需要 Level 2 数据,成本较高。

一个实用的折中方案是:对每个标的分别设定滑点参数,参数值参考该标的的历史买卖价差和成交活跃度。流动性好的大盘股滑点可以设低一些,小盘股或冷门合约设高一些。这样虽然不如订单簿模拟精确,但比一刀切的固定值靠谱得多。

5.3 手续费与资金费率:别忽略那些"小钱"

手续费看似是小钱,但在高频策略里会迅速累积。股票交易有佣金和印花税,期货有开平仓手续费,加密货币有 maker/taker 费率差异。回测时必须把这些都建模进去,否则收益会被高估。

还有一个容易被忽略的是资金费率,主要出现在永续合约里。如果你做多永续合约,需要支付或收取资金费率,这个费率每8小时结算一次,长期持仓下来影响不小。回测时如果忽略资金费率,多空策略的收益会被系统性高估或低估。

注意:回测里最危险的不是参数设得不准,而是完全忘了设。我见过不少回测代码里手续费和滑点都是零,跑出来的曲线漂亮得像艺术品,实盘一上就原形毕露。养成习惯:回测引擎初始化时,第一件事就是把成本参数填上。

6. 从回测到实盘:仓位管理、风控和模型衰减的应对

6.1 仓位管理:凯利公式、固定比例还是波动率目标

仓位管理决定了你的策略能走多远。凯利公式在理论上能最大化长期增长率,但对参数估计误差极其敏感,实际中很少直接使用全凯利,通常用半凯利或四分之一凯利。固定比例法简单直接,比如每次投入总资金的 1% 或 2%,适合新手。波动率目标法根据标的的近期波动率动态调整仓位,波动大时减仓,波动小时加仓,能在一定程度上平滑收益曲线。

我的经验是:对深度学习策略,波动率目标法通常比固定比例更稳健。因为深度学习模型的预测置信度会随市场状态变化,波动率目标法能间接反映这种变化。具体实现上,可以设定一个目标年化波动率(比如 15%),然后根据标的的近期波动率计算仓位比例,再叠加一个最大仓位限制。

6.2 风控规则:止损、止盈和最大回撤控制

止损和止盈是最基础的风控手段,但参数设置很讲究。止损太紧会被噪声打掉,太松则单笔亏损过大。一个常见的做法是基于波动率设置止损,比如止损线设在入场价下方 2 到 3 倍 ATR(平均真实波幅)的位置。止盈可以类似处理,或者用移动止盈(trailing stop)让利润奔跑。

最大回撤控制是策略级别的风控。当策略回撤超过某个阈值时,强制减仓或暂停交易,直到模型重新通过验证。这个阈值通常设在 10% 到 20% 之间,具体取决于策略的风险偏好和资金属性。

6.3 模型衰减与再训练:什么时候该重新训练

深度学习模型在金融数据上会衰减,因为市场结构在变,参与者在变,规律也在变。模型衰减的表现是:实盘表现逐渐偏离回测预期,预测准确率下降,交易信号变得迟钝。

应对模型衰减的常见做法是定期再训练,比如每月或每季度用最新数据重新训练模型。再训练的频率取决于策略的换手率和市场变化速度。高频策略可能需要每周甚至每天更新,中低频策略每月更新通常够用。

另一个做法是在线学习,让模型随着新数据不断微调。但在线学习在金融场景下风险较高,因为市场噪声大,模型可能被短期波动带偏。更稳妥的方式是滚动窗口训练:每次用过去 N 个月的数据训练,预测接下来一个月,然后窗口向前滚动。这样既能适应市场变化,又不会过度反应。

7. 一些让我少亏钱的实操习惯

最后分享几个我在实际项目里养成的习惯,都是踩坑之后总结出来的,不一定高大上,但确实管用。

第一,任何回测结果,先看最大回撤和夏普比率,再看年化收益。年化收益高但回撤大的策略,实盘时很难拿得住。夏普比率低于 1 的策略,除非有特殊理由,否则不值得投入实盘。

第二,回测时把交易成本设得比预期高一点。比如你估计滑点是 0.02%,回测时就设 0.03%。这样跑出来的结果如果还能接受,实盘时心里更有底。

第三,保留一份"最差情况"的回测报告。除了正常参数,再跑一组悲观参数(高滑点、高手续费、延迟成交),看看策略在恶劣条件下的表现。如果悲观情况下亏得底朝天,那实盘时就要格外小心。

第四,模型上线前,先跑至少一个月的模拟盘。模拟盘能暴露很多回测发现不了的问题,比如数据延迟、接口稳定性、订单被拒等。模拟盘表现和回测差距在合理范围内,再考虑小资金实盘。

第五,记录每一笔交易的决策依据。深度学习模型是黑盒,但交易日志可以帮你事后复盘。当策略表现异常时,翻看日志往往能发现是数据问题、模型问题还是市场结构变化。

这些习惯看起来琐碎,但正是这些琐碎的地方,决定了你的策略是"回测英雄"还是"实盘幸存者"。算法交易这条路,技术只是入场券,工程细节和风险意识才是长期存活的关键。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进