数据与算法

金融时间序列的交叉验证:如何用清洗与禁运防止量化模型过拟合

量化模型最危险的时刻,不是回测曲线下跌,而是曲线漂亮得像一条没有噪声的直线。夏普比率稳,胜率高,参数轻轻一调,收益立刻抬头。你以为自己找到了市场规律,实际上很可能只是把未来信息从数据缝隙里偷了出来。…

金融时间序列的交叉验证:如何用清洗与禁运防止量化模型过拟合

金融时间序列不能直接套用传统机器学习里的随机交叉验证。价格、成交量、波动率和新闻情绪都带着时间顺序,样本之间并不独立;更麻烦的是,量化标签往往覆盖一段时间,训练样本和测试样本的评价区间可能互相重叠。模型不是在预测未来,而是在背诵未来已经发生过的路径。

这就是金融时间序列交叉验证必须引入“清洗”和“禁运”的原因。前者处理标签重叠,后者切断测试集附近的序列相关性。少一个环节,回测就可能继续漏风。

金融数据不是普通样本:传统K折验证为什么会失效

传统K折交叉验证建立在一个前提上:样本近似独立同分布。数据被随机打乱,再切成若干份,轮流拿其中一份测试,其余部分训练。这个方法在很多静态分类问题里好用,因为一条样本和另一条样本之间没有明显的时间依赖。

金融市场不吃这一套。

今天的收益率和昨天、前天的收益率存在序列相关;同一段行情里的波动率、成交量和盘口状态,也会持续影响后续样本。股票大跌、期货逼仓、宏观数据公布后的连续波动,都不是一根K线结束后立刻清零的事件。你把这些样本随机打散,模型就能在训练集里看到测试集附近的市场状态。

这已经是第一层泄漏。

第二层泄漏来自标签。假设你使用过去的特征预测未来五个交易日收益,样本标签定义为:

  • 特征观察时间:1月10日收盘;
  • 标签评价时间:1月11日至1月17日;
  • 标签结果:这五天的累计收益。

那么,1月11日和1月12日的样本,其标签评价区间可能仍然与1月10日样本发生重叠。它们看起来是不同样本,实际上都在描述同一段未来行情。

如果你把样本随机分配到训练集和测试集,测试集中的标签区间可能和训练集中的标签区间重叠。模型得到的不是干净的泛化检验,而是被允许提前窥视答案的考试。

标签重叠比特征重叠更隐蔽

很多交易者知道不能把未来价格直接放进特征,却忽略了标签时间跨度。

例如,你训练一个方向预测模型:

  • 用过去20根日线构造特征;
  • 预测未来10根日线的收益;
  • 每天生成一个训练样本。

这组样本的特征窗口可以没有未来数据,但标签窗口会连续重叠。1月10日样本的标签覆盖1月11日至1月24日,1月11日样本的标签覆盖1月12日至1月25日。两者之间存在大量共同的评价区间。

如果1月10日样本在训练集,1月11日样本在测试集,训练样本的标签就已经携带了测试样本所处行情的部分信息。

这不是代码报错能提醒你的问题。程序会正常运行,模型会正常拟合,回测曲线甚至会更加漂亮。等你把策略接上实盘,滑点开始吞噬收益,连续亏损击穿止损,才发现所谓的稳定性只是交叉验证制造的幻觉。

金融交叉验证最先要防的,不是模型太复杂,而是训练集和测试集根本没有真正分开。

直接使用随机K折,会把市场结构打碎

随机K折还有一个严重问题:它把时间顺序打乱了。

在真实交易里,你只能用过去的信息预测未来。可随机K折允许模型在训练阶段同时看到测试期之前和测试期之后的数据。即便特征本身没有显式使用未来价格,市场状态、波动聚集和相邻标签也会把未来的结构带回训练集。

尤其是以下策略,泄漏风险更高:

  • 使用滚动波动率、成交量异常、资金流向等持续性特征;
  • 标签采用未来多日收益、最大回撤或是否触发止盈止损;
  • 样本按日生成,但每个样本的评价期长于一个交易日;
  • 同一资产、同一行业或高度相关的品种同时出现在训练集和测试集;
  • 通过大量参数、特征和模型组合挑选最优结果。

最后一种情况尤其危险。你可能没有直接偷看测试标签,但你反复试验不同参数,再选出回测最好的那一组,实际上已经把测试结果当成了优化信号。2014年,Bailey等人发表了关于回测过拟合概率的研究,核心警告非常直接:策略试得越多,撞上漂亮回测曲线的机会越高。

传统K折会把这种问题进一步放大。它给你多个看似独立的验证结果,却没有处理时间依赖和标签重叠。数字越整齐,陷阱越容易被忽略。

清洗机制:先把与测试标签重叠的训练样本剔掉

清洗,也就是净化操作,英文通常称为“清洗”(Purging)。它的任务单一而明确:

只要训练样本的标签评价时间区间,与测试集中任意样本的标签评价区间发生重叠,就从训练集中删除。

注意,清洗不是删除测试集,也不是简单地按照样本日期切一刀。它检查的是标签的时间跨度。

用事件时间而不是样本日期做判断

给每条样本记录两个时间:

  • t0:特征观察时间,也可以理解为样本形成时间;
  • t1:标签评价结束时间,也就是未来收益、止盈止损或事件结果完成的时间。

一条样本可以表示为区间 [t0, t1]

假设测试样本的标签区间是 [T0, T1],训练样本的标签区间是 [t0, t1]。如果两个区间满足以下条件之一,就不能把训练样本留在训练集中:

  • 训练标签在测试标签开始之前已经进入测试评价区间;
  • 训练标签在测试标签结束之后仍然与测试区间相交;
  • 两个标签区间存在任何时间重叠。

换成交易员能听懂的话:测试集在统计一段行情的结果,训练集不能拿同一段行情的结果来学习。

一个具体的标签重叠例子

假设你用期货日线构造三条样本:

样本特征时间标签评价区间标签含义
样本A1月10日收盘1月11日至1月17日未来5个交易日收益
样本B1月11日收盘1月12日至1月18日未来5个交易日收益
样本C1月18日收盘1月19日至1月25日未来5个交易日收益

如果样本B被分到测试集,那么样本A不能继续作为训练样本。它的标签区间与样本B重叠了1月12日至1月17日。样本C则没有与样本B发生标签重叠,可以进入训练集,但它还要接受禁运规则的检查。

这里最容易犯的错误,是只比较特征时间:

  • A的特征时间早于B;
  • A不是未来样本;
  • A看起来可以用于训练。

这个判断少看了一层。对于带未来窗口的标签,样本的“有效时间”不是一个点,而是一段区间。只看t0,等于把标签泄漏藏在地板下面。

清洗并不等于让样本完全独立

清洗解决的是标签评价区间重叠,不会自动消除所有序列相关性。

两个样本的标签不重叠,特征仍然可能高度相似。比如连续几天的滚动波动率、均线斜率和盘口不平衡指标,都可能在同一段趋势中保持相似状态。模型依然能够从相邻样本中识别市场环境,只是这种信息不再通过标签区间直接交叉。

所以,清洗是第一道闸门,不是整个风控系统。后面还需要禁运,把测试集附近的一段数据隔离出去。

清洗的实际流程

在工程里,清洗流程可以按下面的顺序执行:

1. 先明确标签定义。

不要只保存一个收益数值,必须保存标签开始时间和结束时间。未来持有期、止盈止损触发时间、事件确认时间,都属于标签时间的一部分。

2. 划分测试集。

测试集可以是某个连续时间段,也可以是组合净化交叉验证生成的某组分区。

3. 扫描训练样本的标签区间。

对每条训练样本,检查它是否与测试集任意标签区间相交。

4. 删除发生重叠的训练样本。

不能用填充值、降权或随机保留替代删除。既然训练样本已经接触测试期的信息,就不应继续参与拟合。

5. 记录清洗比例。

清洗后样本数量减少是正常现象。若删除数量异常庞大,说明标签持有期太长、采样过密,或者交叉验证分区设计本身不适合当前策略。

清洗比例不能简单拿一个固定数字硬套。五分钟高频策略、日线趋势策略和宏观事件策略,标签生命周期完全不同。你不能把一个适用于某类数据的经验窗口,直接扣在所有市场头上。

禁运机制:在测试集之后留出隔离带

清洗处理标签重叠,禁运处理测试集附近的序列相关性。

“禁运”(Embargoing)的做法是在测试集结束之后,立即剔除一段缓冲期内的训练样本。常见的经验设置是禁运比例为样本量的1%至2%,但这只是起点,不是万能答案。真正的窗口长度,应当结合数据的自相关结构、标签持有期和交易频率来决定。

为什么测试集结束后还要删数据

很多人会问:测试集已经结束,后面的样本不是未来数据吗?为什么还不能拿来训练?

因为交叉验证不是简单的时间切分。它往往把多个时间区间轮换成训练集和测试集。当前测试集结束后紧邻的样本,可能和测试集处于同一段市场状态,特征与标签都带有强烈的连续性。

举个更贴近实盘的例子。

你测试的是一段剧烈波动行情,测试区间从1月10日持续到1月20日。1月21日紧接着出现的样本,其波动率、成交量和价差仍可能延续测试期的状态。如果你把1月21日样本直接放进训练集,模型就会通过紧邻的序列结构间接接触测试期信息。

它未必看到了测试标签,但它看到了测试期留下的市场指纹。

对于新闻驱动、事件驱动和趋势延续策略,这种影响尤其明显。一次央行会议、财报公布、合约换月或逼仓行情,不会在一个时间戳上突然结束。数据表里虽然换了日期,市场状态却还在延续。

禁运窗口不能靠拍脑袋

事实资料中常见的禁运比例是1%至2%的样本量,例如embargo=0.01embargo=0.02。这个范围可以作为工程初始值,但不能直接当成行业标准。

禁运窗口至少要考虑四件事:

  • 标签持有期。

未来预测五个交易日的策略,不能用单日预测的隔离窗口处理。

  • 样本采样频率。

五分钟一个样本和一天一个样本,即使禁运比例相同,实际隔离的时间长度也完全不同。

  • 市场状态的持续性。

波动率聚集明显的品种,需要面对更长的状态延续;流动性突然收缩时,价差和成交冲击也不会在下一根K线立刻恢复。

  • 特征的回看长度。

如果特征依赖过去60个交易日的滚动统计,边界附近的样本共享大量历史信息,分割设计就必须对此保持警惕。

可以把禁运窗口理解成交易所里的缓冲区。不是因为缓冲区里必然存在泄漏,而是因为你不能把正在冒烟的战场边缘直接当作干净样本。

清洗和禁运的区别

两者经常被混为一谈,但它们针对的是不同问题:

机制主要处理的问题判断依据典型动作
清洗训练标签与测试标签发生重叠标签评价区间是否相交删除重叠训练样本
禁运测试集附近存在序列相关性测试集结束后的时间距离删除测试集后的缓冲样本
普通时间切分保持基础时间顺序样本形成时间训练用过去,测试用未来
组合净化交叉验证评估多条回测路径分区组合与时间约束先清洗,再禁运,生成多条路径

如果只做清洗,测试集结束后的相邻训练样本仍然可能带入市场状态信息。如果只做禁运,训练集与测试集的标签区间仍可能重叠。两个环节缺一不可。

清洗是在边界上绞杀重叠标签,禁运是在边界外再架一道火力网。只做一个,防线仍然会漏。

从普通时间切分到组合净化交叉验证

普通的向前滚动切分比随机K折更符合交易逻辑,但它不是万能药。很多人拿TimeSeriesSplit跑完,看到训练集永远早于测试集,就以为泄漏问题已经结束。

没有这么简单。

如果标签跨越多个时间点,向前滚动切分仍然可能让训练标签和测试标签重叠;如果测试集附近存在强序列相关,紧邻样本仍会传递市场状态;如果你反复调整参数并只保留最好的那条回测路径,模型依旧会被选择过程过拟合。

CPCV解决什么问题

组合净化交叉验证,英文称为“组合净化交叉验证”(Combinatorial Purged Cross-Validation,简称CPCV),是在净化K折基础上的扩展。

它把时间序列划分为多个连续分区,再从中组合训练集与测试集。在每一次组合中:

1. 选定一组分区作为测试集;

2. 对训练集执行清洗,删除标签区间重叠的样本;

3. 对测试集之后的相邻训练区间执行禁运;

4. 得到一条符合时间约束的回测路径;

5. 通过不同分区组合,生成多条回测路径。

关键变化在于:你不再只看一条从左到右的回测曲线,而是观察多条路径上的性能分布。

如果一套策略只有一条切分结果好,其他路径全部失血,那不是稳健性,而是路径依赖。它可能恰好吃到了某一段趋势、某一次波动扩张,或者某个品种的特殊行情。CPCV把时间分区重新组合,逼着模型面对更多市场顺序和状态变化。

组合数量不是越多越好看

当数据被划分为N个分区,并从中选择k个分区作为测试集,理论组合数量可以表示为C(N, k)。组合数增加,回测路径也会增加,但计算量、模型训练次数和结果解释难度会同步上升。

不要为了制造更多曲线而盲目增加分区。分区太细,单个测试区间可能缺乏足够交易事件;分区太粗,路径数量又不足以反映不同市场环境。分区设计必须服从策略的交易周期和标签生命周期。

例如:

  • 日线中短线策略,需要确保每个测试区间包含足够的交易样本;
  • 低频宏观策略,单个事件之间间隔长,不能把相邻事件强行当作独立样本;
  • 高频策略,样本数量虽然庞大,但微观结构自相关强,禁运不应只看样本条数;
  • 期货跨合约策略,还要处理换月、主力切换和不同合约流动性迁移。

CPCV能提供性能分布,但不会自动替你判断哪条路径最接近未来。它只是把单点结论拆开,让你看见策略在不同路径上的脆弱位置。

你真正该看的不是最高收益

面对CPCV结果,第一反应不要去找收益最高的路径。那是交易者最熟悉的陷阱:看到曲线抬头就追,看到回撤就解释。

更值得观察的是:

  • 不同路径的收益方向是否一致;
  • 回撤是否集中发生在某类市场状态;
  • 交易次数变化后,收益是否完全依赖少数几笔交易;
  • 换手、滑点和手续费加入后,性能分布是否整体塌陷;
  • 参数轻微变化时,结果是否立刻反转;
  • 单一品种的收益是否掩盖了其他品种的亏损;
  • 训练窗口变化后,模型是否仍然能保持基本的风险结构。

不要把“所有路径都盈利”当作唯一标准,也不要把某一条路径的高夏普当作模型能力证明。金融数据本身存在非平稳性,合理的模型也会经历失效期。真正要防的是:回测只在一种切法、一个参数、一个市场阶段里成立。

标签定义是整套验证的地基

很多交叉验证失败,并不是验证器写错,而是标签定义从一开始就不完整。

如果你只在数据表中保存:

  • 特征时间;
  • 标签数值;
  • 是否上涨。

却没有记录标签评价结束时间,那么清洗就没有执行基础。程序不知道标签覆盖到哪一天,只能按照样本日期机械切分。看似运行顺利,实际上已经把最关键的信息丢掉了。

常见标签类型与时间边界

不同标签必须使用不同的结束时间:

固定持有期收益

例如预测未来五个交易日收益。标签结束时间就是第五个交易日的收盘时间。如果中间遇到节假日或停牌,不能用简单的自然日加法替代交易日历。

止盈止损标签

例如使用三重障碍法,样本在止盈、止损或最长持有期中的任一条件触发时结束。标签结束时间应该是实际触发时间,而不是固定写成未来第N根K线。

波动率或风险标签

如果标签是未来一段时间的实现波动率、最大回撤或尾部损失,结束时间就是统计窗口的终点。标签虽然不是方向收益,照样存在区间重叠。

事件标签

如果模型预测财报、宏观数据或链上事件后的价格反应,标签时间要覆盖事件发生后的完整评估窗口。事件本身的发布时间、数据修订时间和市场可获得时间,也不能混在一起。

特征时间也必须遵守可得性

清洗和禁运解决不了特征本身的未来函数。

例如,你在收盘后生成交易信号,却使用了当天收盘价构造特征,再假设自己能以当天收盘价成交。这个问题与交叉验证无关,属于执行时间错误。再比如,使用财务数据时没有考虑公告发布时间,直接把季度报告的最终值回填到整个季度,也会把未来信息灌进历史。

必须把以下时间分开:

  • 数据实际发生时间;
  • 数据对市场公开的时间;
  • 数据进入数据库的时间;
  • 策略允许使用的时间;
  • 下单与成交时间。

金融数据不是一张静态表,而是一条带时间戳的事件流。只要把这些时间揉成一个字段,泄漏就已经开始。

Python工程实现:验证器不是一个简单的切片器

在Python生态中,可以使用skfolio提供的CombinatorialPurgedCV,也可以参考Kaggle社区常见的PurgedGroupTimeSeriesSplit思路,自己实现带清洗和禁运的交叉验证器。

工具只是外壳,时间边界才是核心。不要因为类名里出现了“Purged”或“Embargo”,就认为数据已经被净化。你必须确认它实际使用的是哪一列时间、如何处理标签结束时间、禁运按照样本数还是时间长度计算,以及是否支持不规则交易日。

一个可执行的工程设计

建议把数据处理拆成四层,而不是把所有逻辑塞进一个训练函数。

第一层:原始事件层

保存价格、成交量、盘口、新闻、资金流和链上数据的原始时间戳。不要在这一层直接填充未来值,也不要为了方便把不同来源数据强行对齐到同一时刻。

第二层:可用数据层

按照真实发布时间和策略可见时间,生成当时可以使用的字段。任何后到数据都不能回填到过去。对于新闻和财务数据,发布日期比报告所属期间更关键。

第三层:样本与标签层

为每条样本明确记录:

  • 特征形成时间;
  • 标签起始时间;
  • 标签结束时间;
  • 资产或合约标识;
  • 分组标识;
  • 是否存在停牌、换月或异常成交。

这一步是清洗的入口。没有标签结束时间,后面所有净化动作都只能靠猜。

第四层:交叉验证层

交叉验证器接收样本的时间区间,生成训练索引与测试索引。每次生成训练集后,执行两步:

1. 删除与测试标签区间重叠的训练样本;

2. 删除测试集结束后的禁运窗口样本。

如果策略涉及多个品种,还要决定清洗是按单品种执行,还是按整个市场事件执行。比如股指期货和相关ETF在同一重大事件中同步波动,单独按资产切割可能无法阻断跨资产信息传播。

PurgedGroupTimeSeriesSplit的使用边界

PurgedGroupTimeSeriesSplit这类自定义验证器,常见做法是先把相邻样本划分成时间组,再在组之间进行训练测试切分。它比随机K折更接近时间序列,但“分组”不能替代标签区间检查。

如果一个组内的样本标签跨越多个时间段,组边界仍然可能不干净。你需要确认:

  • 分组依据是样本形成时间,还是标签结束时间;
  • 测试组前后的训练组是否被正确清洗;
  • 禁运窗口是否覆盖了测试期之后的样本;
  • 多个测试组之间的标签区间是否发生交叉;
  • 样本权重和标准化参数是否只在训练集上计算。

尤其要注意标准化、缺失值填补、特征选择和降维。若你先用全量数据计算均值、标准差、相关系数或主成分,再进入交叉验证,数据泄漏已经发生。清洗标签不能替你挽救全量预处理。

正确的顺序应当是:每个训练折单独拟合预处理过程,再将同一组参数应用到测试折。测试集只能被变换,不能参与参数估计。

用日志把泄漏留下证据

交叉验证不是黑箱。每一折至少记录:

  • 训练样本原始数量;
  • 清洗删除数量;
  • 禁运删除数量;
  • 最终训练数量;
  • 测试样本数量;
  • 训练集最晚标签结束时间;
  • 测试集最早与最晚标签时间;
  • 禁运窗口的起止时间;
  • 每个资产和每个分组的样本分布。

如果你只保存最终夏普、收益和回撤,出了问题只能凭曲线猜。把清洗与禁运日志保存下来,才能回答一个硬问题:这一折的训练集,究竟有没有碰到测试集的标签区间?

为什么普通TimeSeriesSplit仍然不够

向前滚动验证有一个明显优点:训练集在时间上早于测试集,符合交易顺序。但它只解决了“整体时间方向”问题,没有自动解决标签重叠、状态延续和复杂预处理泄漏。

假设训练集截止到3月31日,测试集从4月1日开始。若3月31日样本的标签覆盖未来五个交易日,那么它的标签直接进入了测试区间。训练集虽然在样本日期上早于测试集,标签却没有结束。

这就是为什么“训练集日期早于测试集日期”不是充分条件。

还要看特征窗口。若测试集从4月1日开始,而训练集最后一个样本使用了3月31日收盘后的信息,策略实际能否在4月1日开盘前取得这些数据?如果不能,时间切分再漂亮也没有意义。

普通时间切分更适合做基础 sanity check,而不是当作金融模型唯一的验证框架。它可以保留,但必须在标签净化、禁运和预处理隔离之后使用。

最容易把回测做坏的几种操作

把未来收益先算好,再随机切分

这是最直接的泄漏。标签先在全量数据上滚动计算本身并不一定错误,但切分时必须依据标签区间执行清洗。否则,重叠标签会被同时分配到训练集和测试集。

只删除测试区间内的训练样本

有些实现看到训练样本日期落在测试期内,就把它删除;测试期外的样本则全部保留。这种做法没有检查标签结束时间,也没有处理测试集之后的禁运窗口。

样本日期不是标签边界。两者混用,等着被回测曲线扫损。

禁运固定成一个拍脑袋的天数

不同频率的数据不能共享同一个禁运天数。五分钟数据里一天可能包含数百个样本,日线数据里一天只有一个样本。将“禁运三天”硬编码,既可能过短,也可能把训练集砍得所剩无几。

应当明确禁运按什么计算:

  • 按时间长度;
  • 按样本数量;
  • 按交易日比例;
  • 按标签持有期;
  • 按自相关衰减窗口。

如果采用1%至2%的样本比例作为起点,也要把实际转换后的时间跨度记录下来。

先用全量数据做特征筛选

你用全量样本计算特征与收益的相关性,再挑选最相关的指标进入交叉验证。这等于提前让测试集参与了特征选择。即使最终模型没有直接读到测试标签,筛选过程已经根据测试期结果改变了输入变量。

特征筛选必须在每个训练折内完成。模型选型、参数搜索、阈值调整也一样。

反复试验后只展示最好的一次

这不是验证,是挑曲线。

当你尝试了大量标签窗口、止盈止损参数、入场阈值、品种组合和模型结构,最终只保留最好结果时,模型已经经历了选择偏差。CPCV能帮助你看到多条路径,但不能替你消除“试很多次再选最好”的问题。

解决方式不是把坏结果藏起来,而是记录实验空间、固定评价协议,并保留未参与调参的最终测试区间。

如何判断净化后的结果是否更可信

净化后,样本数量下降、收益回落、夏普降低,都不代表模型失败。恰恰相反,很多原本依赖泄漏的策略,会在清洗和禁运后迅速失血。

这一步像把杠杆从二十倍砍回三倍。账户曲线不再那么刺激,但你终于看见了真实风险。

可以从几个角度观察结果。

性能是否从“单点神迹”变成“分布”

净化交叉验证不应只输出一个均值。你需要查看不同路径的收益、回撤、胜率、换手和交易次数分布。性能分散很大,说明策略对时间路径敏感;收益方向相对稳定,说明模型并非只吃到某一段行情。

回撤是否符合策略逻辑

趋势策略在横盘期回撤,均值回归策略在单边行情中受伤,事件策略在事件稀疏时交易机会减少,这些都属于可解释的风险。

无法解释的回撤更危险。比如所有亏损集中在换月、夜盘、低流动性时段,说明模型可能没有处理交易执行;收益全部来自少数极端行情,说明策略对尾部样本过度依赖。

加入交易成本后是否仍有生存空间

滑点不是回测里的装饰品。期货盘口变薄时,成交价会迅速偏离信号价;高换手模型即使预测方向正确,也会被手续费、冲击成本和排队成交吞掉。

成本模型至少要覆盖:

  • 手续费;
  • 买卖价差;
  • 滑点;
  • 市场冲击;
  • 换月成本;
  • 夜盘与流动性差异;
  • 涨跌停、熔断或无法成交的情况。

净化后本来就脆弱的策略,加入现实成本可能直接爆仓。这不是成本太保守,而是模型原先把不可交易的收益当成了利润。

轻微改动会不会击穿模型

把标签窗口从五个交易日改成四个或六个,把训练窗口前后移动,把禁运比例从1%调整到2%,结果不应当立刻从盈利变成彻底失效。

如果一丁点改动就击穿收益,模型可能在拟合某个特定边界。真正稳健的信号不会只藏在一个神奇参数里。

当然,参数稳定不代表实盘必赚。它只说明模型没有那么容易被一根边界线绞死。市场一旦换状态,仍然可能让模型失效。

一个适合实战团队的验证流程

如果你正在搭建新的量化策略,我建议把验证流程固定成下面几步,不要每次研究都临时改协议。

1. 先写清楚交易时钟。

明确特征何时生成、信号何时确认、订单何时提交、成交价如何获得。没有执行时钟,收益标签就是一笔糊涂账。

2. 为每条样本保存标签起止时间。

固定持有期、止盈止损、事件标签都必须拥有明确结束时间。

3. 冻结数据版本。

不要在模型训练期间不断替换历史数据、修正异常值或回填字段。数据版本变化会让不同实验无法比较。

4. 先做时间顺序检查。

用向前滚动切分确认最基本的训练测试方向没有反转。

5. 加入清洗。

删除训练集中所有与测试标签评价区间重叠的样本。

6. 加入禁运。

在测试集结束后留出隔离窗口,初始可以从1%至2%的样本比例开始,再结合自相关和标签持有期调整。

7. 将预处理放进训练折。

标准化、缺失值处理、特征选择、降维和模型调参,全部只使用训练集拟合。

8. 使用CPCV观察多条路径。

不要只看平均收益,保留每条路径的完整交易记录和风险指标。

9. 加入现实交易成本。

把手续费、价差、滑点和成交限制放到回测引擎,而不是在最后用一个固定比例随手扣掉。

10. 保留未触碰的最终样本。

研究阶段结束后,使用独立的时间区间做最终检验。这个区间不能拿回来反复调参,否则它也会被你吃掉。

最后的账户警告

金融时间序列交叉验证不是为了把回测曲线修饰得更好看,而是为了把虚假的好看击穿。

清洗删除标签重叠,禁运隔离测试集附近的序列相关,组合净化交叉验证把单一路径拆成性能分布。它们不能消除模型误差,不能保证实盘盈利,也不能替你处理数据质量、执行成本和市场制度变化。

但如果连这些基本防线都没有,模型的夏普、胜率和收益曲线就没有多少讨论价值。你看到的不是策略能力,而是泄漏、相关性和反复试验共同喂出来的幻觉。

回测里少赚一点,最多难受;实盘里带着泄漏上杠杆,才是真正的爆仓。先把验证做干净,再谈模型能不能赚钱。

Related reading: 量化模型中的前瞻偏差:数据泄露的隐蔽成因与检测机制 and 量化策略的终极博弈:为什么高质量数据比复杂算法更决定成败.

常见问题

为什么金融数据不能直接用传统的随机K折交叉验证?
金融数据具有时间顺序和序列相关性,随机打乱样本会使模型在训练集中看到测试集附近的市场状态,导致模型通过背诵未来路径而非学习规律来拟合数据。
清洗(Purging)和禁运(Embargoing)有什么区别?
清洗旨在删除标签评价区间与测试集重叠的训练样本,解决标签重叠问题;禁运则是在测试集结束后剔除一段缓冲期内的样本,解决测试集附近的序列相关性问题。
如何确定禁运窗口的长度?
禁运窗口没有统一标准,应结合标签持有期、样本采样频率、市场状态的持续性以及特征的回看长度来决定,初始可参考样本量的1%至2%进行设置。
组合净化交叉验证(CPCV)比普通时间切分好在哪里?
CPCV通过将时间序列划分为多个分区并进行组合,生成多条回测路径,能揭示策略在不同市场状态下的性能分布,从而识别策略是否过度依赖单一路径或特定行情。
在交叉验证中,预处理步骤应该如何操作?
标准化、缺失值填补、特征选择和降维等预处理过程,必须在每个训练折内单独拟合,并将参数应用到对应的测试折,严禁使用全量数据进行预处理。