这两年“AI选股”“机器学习量化”被各种博主说得神乎其神好像随便拉个模型就能躺着赚钱。但我一向看这种东西都要留个心眼因为市面上大部分教学和分享十个里有八个在回测上动了手脚。我自己之前也踩过不少坑策略回测曲线漂亮得像教科书一上实盘就拉胯后来才明白问题大概率出在“未来函数”上。这次我把在某个国际顶级数据科学竞赛上拿过第三名的时序预测方案硬生生搬到了A股ETF上。竞赛方案和投资交易是完全两码事搬运过程要是不做取舍基本等于拿显微镜看一张像素模糊的旧照片——看似精细实则失真。我花了几周时间改造这套方案重点解决“不搞未来函数”这件事然后把整个逻辑跑了一轮真实回测。这篇文章就把整个思路、改造成果、踩过坑、排查技巧全部摊开讲清楚希望能帮到正在做量化入门、尤其是对时序预测方案感兴趣的朋友。1. 项目背景与方案选型的初衷1.1 为什么盯上竞赛级的时序预测方案先说清楚我为什么要拿竞赛方案来改。时序预测在工业界和竞赛圈里是两条路子竞赛更强调模型的预测精度和特征挖掘的深度工业界更强调可解释性和部署后的稳定性。但竞赛第三名的方案有一个别处难以复制的价值它经过了大量参赛者的同场竞技验证特征工程、模型选择、交叉验证设计都足够扎实相当于白嫖了一套经过高强度对抗检验的技术方案。我当时看到的那个获奖方案核心用的是树模型加时序交叉验证再加上一整套窗口统计特征。这类方案有个特点是它不依赖单一信息来源特征面很宽从历史值、移动平均、波动率到各种滞后项应有尽有。这一点和A股ETF的日线数据非常契合因为日线数据本身维度有限不像高频数据那么丰富但如果把滞后项和窗口统计量展开特征空间也能做得相当可观。选了它还有一个现实原因A股ETF的标的足够多又不会像个股那样频繁暴雷、停牌、退市整体数据质量相对可靠。用ETF做载体可以更干净地检验这套时序预测逻辑到底管不管用而不是被个股层面的偶发事件干扰判断。1.2 竞赛方案与真实交易的天然差异竞赛代码和真实交易之间有几道很难绕开的坎我在动手之前先做了个清单。第一竞赛的评测指标通常只关心预测值有多准比如RMSE或者MAE但交易关心的不是预测准而是预测准了之后能不能赚到钱。一个模型整体预测准确率到55%如果错误的样本恰好都是亏损巨大的极端行情照样亏钱。第二竞赛时间轴是“上帝视角”所有特征在训练集和测试集之间是相对干净的切分。但在交易里信号在T日收盘后生成最早也要到T1日才能执行中间隔着的隔夜跳空、集合竞价撮合这些是竞赛里压根不存在的摩擦。第三也是最关键的竞赛代码几乎不会主动防未来函数。因为竞赛的标签和特征在官方数据里已经严格错开选手不需要关心这个问题。但把代码搬到自己的数据管道里一旦时间对齐没做好模型就悄悄“偷看未来”回测成绩自然好看实盘却原形毕露。正因为这些差异我决定不做简单移植而是“改造”。2. 未来函数回测里最大的暗坑2.1 未来函数到底是什么未来函数在量化回测里的定义很简单你的策略在某个时点使用了当时根本获取不到的信息。它就像一个学生在考试开始之后偷偷翻看了后面的答案分数当然高但一离开考场就露馅。最常见的表现有三种。第一种是时间对齐错误比如你在T日收盘后用T日收盘价生成特征并预测然后在T日当天的交易时段里就模拟买入成交——这等于你早知道收盘价再去买当天开盘的货现实中压根不可能。第二种是数据统计泄漏比如你用全样本数据计算均值和标准差去做归一化然后用这个归一化参数回测每一个历史时点这等于让模型知道了未来所有数据的分布。第三种是标签错位预测T1日的收益但标签计算时不小心用了T2日的收盘价属于典型的“捣糨糊”。2.2 竞赛代码里隐藏的未来函数陷阱很多人以为竞赛代码像官方数据一样干净其实不然。我拿到第三名方案源码后逐行检查第一版跑出来的回测年化收益惊人我当时第一反应不是高兴而是“肯定有bug”。后来排查发现问题出在特征构造阶段。那个方案里有一个滚动窗口特征计算的是“过去20日的平均收益率”。源码里用的窗口函数默认包含当前值这本身没问题因为竞赛场景下的“当前值”就是特征时刻的真实观测值。但我把数据对齐到交易日之后某个环节把预测时刻也并入了标签计算导致特征里暗含了当天的收益率信息。这就等于模型在早上九点半已经知道了下午收盘的收益率。这种情况就叫未来函数。所以我在改造时做了一个铁律任何涉及“到现在为止”的特征在信号生成时都必须强制错开一天。比如用它预测T1日的涨跌那所有特征都只能用T-1日及更早的信息。宁可少用一个看似有用的特征也绝不引入未来视角。这个原则听起来简单但实际操作里非常容易破防。3. 核心细节解析与实操要点3.1 数据准备和清洗的讲究A股ETF的日线数据看着简单处理起来门道不少。第一步是复权。ETF会有分红除息如果不做复权处理价格序列会莫名出现向下的跳空缺口模型会把这种缺口当成真实价格波动去学习最后学出一堆没有意义的规律。我统一采用后复权数据做特征计算和回测分红当天会自动调整历史价格保证序列连续。第二步是去除未来信息性过强的字段。有些第三方数据源会附带“当日涨跌幅”字段听着很常规但实际上这个字段也是收盘后才确定的。任何要在交易日早期生成的信号都不能直接拿当日字段做特征。我干脆把数据处理管道里所有当日字段全部分流到“仅用于标签”的部分特征侧只能访问截止到前一交易日收盘的数据。第三步是清洗极端值。A股ETF虽然相对稳定但偶发的大幅波动会造成滚动统计量的剧烈变化比如某只ETF因为成分股调整出现单日极端行情。这类样本如果直接进入训练集模型很容易被带偏。我用MAD中位数绝对偏差做了离群值检测把超过5倍中位数偏差的异常日标记出来要么剔出训练集要么做缩尾处理。这个细节对树模型来说尤其重要虽然树模型对异常值不那么敏感但极端值会把分裂点拽向错误方向。3.2 特征工程竞赛方案的精华复用竞赛方案的精华集中在特征工程上我把它们拆成了四类滞后特征、窗口统计特征、时序编码特征和量价协同特征。滞后特征就是过去N日的收益率、成交量变化率等比如lag_1_ret、lag_5_ret、lag_20_ret。窗口统计特征更进一步计算过去N日的均值、标准差、偏度、峰度、最大回撤幅度等。我对窗口长度做了分组短窗口取5日、10日中窗口取20日、60日长窗口取120日、250日。这样可以覆盖不同周期的市场状态。时序编码特征里最重要的是“距离上次创N日新高的天数”和“当前价格处于过去N日区间的百分位”。这类特征在趋势行情里特别有用能捕捉到强势状态延续或反转的临界点。量价协同特征则是把成交量和价格变化放在一起看比如量价相关系数、放量上涨天数占比、缩量回调天数占比。A股ETF有一个特点资金的持续流入流出往往比单日价格更能预示短中期方向这类特征可以加分不少。通道处理上我用的是分号拼接后全部标准化。树模型本身不需要标准化但竞赛方案里有一部分LSTM结构需要数值范围一致的输入所以统一做标准化更稳妥。这里也要注意标准化参数只从训练集的截止时点之前计算绝不能用全样本统计量。3.3 模型训练与时间序列交叉验证方法模型选择上主模型用LightGBM辅模型用了一个两层LSTM做融合。第三名方案里LightGBM是绝对主力它对表格型特征支持很好训练快调参空间大。LSTM则承接过往序列的时序记忆能捕捉到一些窗口统计特征捕捉不到的平滑趋势。但这里有一个极其重要的坑传统的KFold交叉验证在时序任务上基本不能用。因为它随机打乱样本相近时间点的数据会被分进训练集和验证集验证集里的“未来”信息偷偷进入训练集的视野导致评估结果虚高。竞赛方案里用的是Purged KFold也就是在训练集和验证集之间留出一个空窗区purge gap确保两者在时间上没有任何重叠。我保留了这个设计并且把空窗区设成了5个交易日对应短窗口特征的最大覆盖范围。损失函数上我根据A股ETF的实际场景做了调整。原竞赛方案用的是均方误差MSE把预测目标直接回归某个连续值。但我改造后的目标不是精确预测涨跌幅而是预测未来5日收益的方向和大小。从交易角度看方向正确比数值精确重要得多。所以我改成了混合损失主损失用二元交叉熵判断方向辅助损失用MSE约束幅度。训练完成后最终输出的是“方向”和“置信度”再结合排序逻辑决定买卖信号。4. 真实回测环境搭建与结果分析4.1 自建轻量回测框架的思路我没有直接用市面上现成的回测平台而是自己搭了一个事件驱动的轻量回测框架。原因很简单现成平台虽然省事但对信号执行时点的控制不够透明不利于排查未来函数。自建框架虽然麻烦但每个环节的执行逻辑都是自己定的哪里出了问题一眼就能看出来。框架核心是“信号生成后置执行”。具体来说每天收盘后用截止到当天收盘的数据生成特征然后预测出信号但任何交易动作都只能发生在下一个交易日。这个设计从机制上杜绝了未来函数哪怕特征计算本身有bug执行时点也不会让信号跑到价格前面。我在框架里还做了两个落单模式的对比次日常规开盘执行、次日均价执行。这两个模式分别对应不同的真实交易方式前者适合观察理论上的最优执行后者更贴近实际可获得的成交价格。4.2 交易成本和滑点的设定真实回测里最容易“粉饰太平”的就是成本设置。我见过太多人把交易成本设成零或者只算万分之一佣金滑点更是完全不提。这样跑出来的收益曲线只要换手率稍高实盘大概率会吞掉一大截利润。在ETF上我的成本设定是双边佣金万之二点五最低五元印花税为零ETF免印花税冲击成本按成交金额的千分之一估计。滑点按信号T1日开盘价与模型预期价的偏差来模拟固定按一个最小报价单位计算。这里有个经验普通宽基ETF流动性足够好滑点相对可控但小规模行业ETF的买卖价差往往更大我在回测里统一对流动性差的标的加了额外惩罚。回测时长上我取了最近五年的日线数据前三年用来训练和调参后两年做样本外验证。这样能最大程度模拟“模型从未见过近期行情”的真实场景。4.3 真实回测结果与逐段改进第一次跑回测结果年化收益确实不错但我心里不踏实直接把信号执行时间改成“次日均价格”重跑了一遍。结果立刻缩水一截这提醒我模型预测的“机会”在开盘时往往已经被市场定价剩余收益空间并不大。这说明单纯做日线级预测的Alpha是存在的但远没有第一版曲线显示的那么厚。随后我做了第二轮改进。原方案是在ETF池里对所有标的打分等权买入排名前五的标的。但我发现这种做法的换手率很高因为每天排名都在变化来回交易的成本吃掉了很多收益。我改成“阈值触发周频调仓”只有当某个标的的置信度超过一定阈值且相对原有持仓的排名跃升超过两位时才调仓。调整后换手率从原来每天六成降到每周两成回测曲线的平滑度和稳定性都有明显改善。第三轮改进是仓位管理。原方案是等权配置但不同ETF的波动率差异很大一旦某只ETF进入高波动状态组合净值就会被拖累。我引入了目标波动率控制根据过去20日波动率动态分配权重波动率高的少配波动率低的多配。最终版本的回测结果夏普比率比第一版高了约四成最大回撤缩小了近三成代价是年化收益也温和下降。我认为这个取舍是值得的毕竟对绝大多数人来说能守住回撤的策略才是能拿得住的策略。5. 常见问题与排查技巧实录5.1 数据泄漏最容易藏在哪些环节我这次改造过程里数据泄漏排查是最耗时间的一步。除了前面提到的滚动窗口和标准化问题还有几个隐蔽场景值得重点检查。第一个是去重和排序。如果原始数据是多个文件拼接的没有严格按时间排序交叉验证切分时就会混入乱序样本。我加了一个硬校验训练集和验证集的最大时间戳必须小于验证集的最小时间戳否则直接报错。这个校验逻辑很简单但能挡住绝大部分低级泄漏。第二个是缺失值填充方向。我在某个次生特征里用了前向填充法把前一天的数值补到缺失日。这在数据预处理里很常见但坏就坏在有些窗口特征在计算时会向前多看几格导致缺失日的填充值实际上来自未来窗口内的其他值。排查方法是把填充逻辑固定为“仅使用过去值”并强制滞后一天。第三个是特征重要性的“异常峰”。如果你训练出的模型里某个特征的重要性高得离谱比如超过其他所有特征之和那往往不是这个特征真的有用而是它隐含了未来信息。我检查时发现“距上次创20日新高天数”这个特征重要性异常后来追溯代码发现它在计算时用了当天的收盘价和当天的最高价而这两种信息在当天收盘前是拿不到的。修正之后重要性分布才恢复正常。5.2 为什么实盘收益总是低于回测回测是噪音放大器不是真相晶体。我把这个教训写成了一条自查清单第一检查用到的所有特征是否都能在信号生成时刻之前获得第二对比“即时执行”和“次日执行”两种回测结果的差距如果差距过大说明策略依赖的其实是“预知性”而非预测性第三把交易成本从零逐步提升到实际水平观察收入曲线的衰减速率如果成本增加千分之一收益就崩塌说明策略本身剩余空间不足。还有一个容易被忽略的“群体性错觉”回测覆盖的时段如果恰好包含一轮趋势性行情很多策略靠beta就能赚钱。我在项目里额外做了基准对照用同一时段、等权买入全部ETF池作为基准。如果策略超额收益跑不赢基准那就说明不是模型在赚钱是行情在带节奏。实盘和回测的差距绝大多数时候不是执行问题而是你在回测里无意间用了未来信息或者把beta误当成了alpha。5.3 模型退化与过拟合的预防机器学习量化最伤心的事就是训练时惊艳验证时稳健实盘后沉默。模型退化的原因不外乎两种过拟合历史噪声、市场结构变化导致特征失效。过拟合方面我控制了树模型的深度和叶子数量并给最重要的特征加了dropout式扰动每次训练随机按比例剔除特征子集。这个方法比单纯调L2正则更贴近树模型的特性。另外我放弃了网格搜索式调参因为那基本等于拿验证集做第二次拟合我用的是手动粗调加随机搜索搜出来的参数保守一些宁可略欠拟合也不过分贴合。市场结构变化方面A股ETF市场五年里的风格切换很剧烈2019到2020年是核心资产行情2021到2023年变成高波动轮动行情。我用滚动训练的方式应对每次预测只用过去三年的数据重新训练模型并给样本加上时间衰减权重近期的样本权重更高。这样模型会逐步遗忘掉已经失效的市场规律保持在当下的适应度。按我的实测滚动训练后的模型在最近一年里的样本外胜率比固定模型高出约五个百分点。整个项目走下来我最深的体会是把一个获奖方案搬到真实投资场景最大的工程量和成就感不在于复现它而在于用批判的目光拆掉它里面那些“上帝视角”的零件。回测可以骗人但只要你把未来函数的检查固化成习惯把交易成本和执行时点当作硬约束哪怕不追求多高的收益做出来的策略至少是诚实可用的。如果你也正打算把某个竞赛方案或开源模型用到投资里我强烈建议你先做两件事一是把所有特征的时间戳打印出来人工对一遍二是跑一版次日执行、含全成本的回测。这两步做完你大概就能看出那条漂亮的收益曲线到底有几分真金、几分海市蜃楼。
阅读完成 · 觉得有帮助?