量化回测避坑指南:数据底座、因子与稳健性全解析
刚把第一本 Python 数据分析和量化分析教材刷完的读者大概率会经历一个既兴奋又困惑的阶段照着例子写出来的策略曲线很漂亮可换个时间段或者换一批标的曲线就面目全非。我当年也踩过这个坑最早以为是自己找的因子不够高端后来才明白真正的问题常常不在策略逻辑本身而在围绕策略的数据、回测和评估流程。这篇算是这套量化分析笔记的卷2专门整理入门之后真正影响结果的部分数据底座怎么搭才不在回测里欺骗自己因子怎么从原始行情里长出来回测系统里哪几个假设最要命以及从研究到实盘执行之间到底隔了多远。适合已经会用 pandas 和 numpy 做表格处理、写过简单信号、但还没有完整研究流程的朋友参考也适合任何准备把自己的回测水平往上推一个台阶的人。1. 卷1到卷2的断层为什么很多策略在纸面上赚钱实盘却亏损1.1 从“能跑通”到“值得信赖”之间的距离学完第一轮书之后大部分人能用 pandas 完成这样的事读行情、算均线、按固定规则生成买卖信号、把持仓收益累加成净值曲线。这套流程跑通之后人会本能地觉得“我已经有自己的策略了”。但真正开始用它做判断时你会发现一个残酷的事实回测做得好和策略真实有效中间隔着一整套容易被忽略的假设。举个例子。很多初版策略回测的时候信号是在当天收盘后计算的持仓也从当天收盘价开始记录。这看起来没毛病但实际成交需要在第二天开盘甚至开盘后的几分钟内才能完成。你在回测里顺手用了收盘价成交就把一个几乎不可能拿到的价格当成了基准。类似这样的假设累积起来会让回测系统在纸面上显得异常顺滑。卷2要解决的就是把这些假设一个个掰开来看。另一个常见断层是“策略曲线很漂亮但你不知道它为什么赚钱”。如果让你解释某个策略在特定年份之间为什么有效很多人只能含糊地说“市场趋势好”。这种解释水平拿去做判断几乎等于闭着眼睛开车。量化的价值在于把这种含糊的直觉变成可检验、可复制的规则并且时时刻刻清楚自己的策略暴露在哪些风险之下。1.2 回测里的三类信息泄露我在代码评审里最常看到的问题是信息泄露也就是策略不小心用到了“未来数据”。第一批典型计算某日信号时把当天收盘后的统计量写成当天开盘前的可用信息。比如“当天收益率”要等收盘才知道可如果你在开盘时点的信号里引用了它就产生了前视偏差回测自然好看实盘必然失灵。第二批典型出现在指标计算的窗口里。滚动均值、滚动波动率这类指标看似人畜无害但很多人会把整个序列先做标准化再切训练集和测试集导致测试环境里混入了全样本统计量。正确做法是只用过去数据做滚动计算测试集永不参与计算中心的任何环节。第三批是拼接错误。两个数据表按日期合并时索引没对齐可能让同一行的价格和收益率错位。pandas 的 merge 和 reindex 很方便却也容易在不知不觉间把未来的行搬到过去。我的经验是在关键数据处理之后先打印几行人工核对一下时间顺序比写十行注释都管用。每一次合并都要想一想“这一行的时间到底有没有错位”。1.3 幸存者偏差你回测的股票池可能根本不真实如果在今天上市公司的名单里选股票做历史回测天然会漏掉那些已经退市、被并购或者价值崩塌的标的。这类偏差叫做幸存者偏差会让回测结果系统性偏高。尤其在A股市场壳资源、退市规则、ST制度对股价的影响都不小忽略退市样本等于只记住了赢家却忘了输家多惨。解决方向有两个第一尽量使用某个历史时点的股票池通常由数据商提供“点时间成分股”第二至少在回测结果里把退市风险作为敏感性进行检验。如果策略高度依赖一批到现在还活得不错的公司那么它的稳健性存疑。把股票池的构建方式明确写进研究记录是量化分析里专业与否的分水岭这一步偷懒后面全是空中楼阁。2. 数据底座复权、快照和清洗比策略逻辑更早决定胜负2.1 复权要选对口径价格连续了收益才可算量化研究里很少直接使用原始价格原因很简单分红、送股、配股会改变价格但不会让持有人的真实财富凭空变动。比如一只100元的股票10送10后价格变成50元如果直接用50元算收益率会得到-50%的虚假跌幅。所以必须在数据处理早期就完成复权而不是放到策略代码里再做。常见的做法有前复权、后复权和不复权。前复权把历史价格按最新股价调整优点是过去的价格很平滑缺点是新数据出现后历史价格会整体变化后复权把上市以来的价格按除权因子放大历史价格固定适合做长期收益统计但数字看起来非常不直观。我一般会同时存一组“不复权收盘价”和一组“复权因子”按需生成对应口径的价格。这里还要注意计算单日收益率建议用复权后的价格但信号里的价格形态识别比如突破某个历史价位则要确认自己用的是哪一类价格。把复权做错后面所有计算都白费这是卷2的第一课。2.2 点时间数据避免“用今天的信息做昨天的决策”点时间数据听起来抽象简单说就是决策当天你真的能拿到什么数据。拿财务指标举例公司年报要在次年规定时间前披露你在年初就用“该年度年报”的数据做因子其实是在用未来信息。技术上数据表里必须保留公告日期或生效日期而不是只存报告期。实操上可以把基本面数据整理成三列股票代码、公告日期、字段值。回测时按日期做非精确匹配只取公告日期早于当天的最新值。这个过程可以在 pandas 里用 merge_asof 完成效率也很高。很多人嫌麻烦索性用了“报告期”数据做回测结果就是因子在报表真正发布前就有反应回测自然好看但实盘一定对不上。行情数据也有同样的问题。比如日内分钟线很多数据源会做一些事后修正如果直接用最新版本拉历史某些时刻的价格可能包含后来重新确认过的信息。所以做分析时对数据的来源、更新时间、版本保留记录不是洁癖而是研究可靠性的基础。2.3 一个可以直接复用的数据清洗流程我给你一个亲测好用的固定流程它不复杂但能把大多数数据坑拦住统一时间格式。全部转成带时区的 datetime按交易日历对齐别用字符串直接排序。重命名和去重。把不同来源的股票代码、日期字段统一命名然后对“代码日期”做主键去重保留最后一次更新。处理缺失值。停牌、新股上市都会造成空值。用 ffill 只适合少数情况更稳的是把空值占比高的样本剔除或者标注为缺失而不是填成零。检查价格是否异常。负价肯定错误成交量零但价格变动明显一般暗示数据断点价格跳跃超过合理范围时要去核对除权除息日期。把清洗结果落盘保存。用 parquet 列式存储保留版本避免每次回测都重新读原始数据。我在写回测框架之前都会先把数据处理成“从某一天起每个标的每天都能找到价格、成交量、涨跌停状态和可用因子”的长表。这一步看起来枯燥却是后面所有分析能立住的根。数据不对后面的一切都是在给错误结论化妆。3. 因子特征搭建把市场逻辑变成可回测的数值3.1 先有行为假设再写因子代码很多新人的做法是先找一堆数据把能想到的公式都算一遍再放到回测里筛相关性。这看起来高效实际上容易挖出统计噪音。我的建议是每个因子先问一句它在捕捉什么市场行为比如“动量因子”捕捉的是趋势延续“均值回归因子”捕捉的是价格过度波动后回到中枢“流动性因子”捕捉的是交易活跃度对定价的影响。想清楚逻辑之后再从原始数据派生特征。典型的一族因子包括过去N日收益率、过去N日波动率、日内振幅、换手率、成交量变化率、相对强弱指标等。它们都可以用 pandas 的 rolling、ewm、shift 几步算出来难点不在代码而在组合和验证。还有一个容易犯的误区把因子名字起得很响亮但代码里只是一段没有逻辑支撑的公式。因子研究不是起名大赛市场逻辑需要解释得通统计结果才有讨论价值。一个因子如果背后的故事讲不通就算回测成绩再好我也不太敢拿它上实盘。3.2 横截面处理去极值、标准化、中性化单看一只股票的时间序列特征很难判断它在全市场里处于什么位置。所以做选股型策略时通常要把因子做横截面处理在每个交易日的全市场样本里对因子值排序、去极值、标准化。去极值的常用方法是 MAD 法比如把超过中位数±3倍绝对偏差的值截断到该边界。标准化可以用 z-score也就是因子值-当日均值/ 当日标准差再配合分位数转换更稳。中性化更复杂常见做法是在横截面回归里把行业虚拟变量、市值对数等作为控制变量取残差作为纯净因子。这一步能让因子不因为集中在少数行业或大盘股上而产生虚假表现。举个例子用市值中性化的均值回归因子往往比未中性化的版本在样本内表现弱但样本外更稳定。很多人接受不了“回测成绩变差”其实这正是去伪存真的一部分。一个因子如果剥离了市值暴露之后几乎不赚钱说明它原本只是借了市场风格的势。3.3 用IC、分组回测评估因子不要只看相关性排名因子评估最怕只列一张相关系数表。行业里常见的做法是算信息系数 IC每个交易日计算因子值与未来N日收益率的 Spearman 秩相关。然后看 IC 的均值、ICIRIC 均值除以标准差、IC 为正的占比以及 IC 随时间是否衰减。分组回测也很直观每日按因子值分成5组或10组算每组未来N日的平均收益。好的因子应该有清晰的单调性第一组和最后一组收益差异明显中间组稳定过渡。如果只是某一组特别高、其余组乱跳那这个因子很可能只是在个别标的上碰运气。我在实际工作里会同时保留“单因子IC”和“加入组合后的边际IC”因为单因子强不代表在组合里能带来增量。这个判断标准在卷2阶段比任何花哨的指标都重要。因子研究本质上是寻找稳定收益来源的过程而不是在几十个变量里硬找一段漂亮的相关性。4. 回测引擎的取舍别迷信复杂度先抓住四个核心假设4.1 向量化回测还是事件驱动回测很多教程会推荐“用向量化方式写回测”对初期研究完全够用。向量化回测本质是用数组运算模拟在每天收盘后按固定规则调仓速度快、代码短适合验证因子逻辑。事件驱动回测则更像流水线按事件顺序处理每个交易日支持条件止损、逐笔成交、订单排队等复杂逻辑但代码量和执行时间都上了一个台阶。我的建议是分阶段因子筛选用向量化回测策略落地前再用事件驱动回测做压力和细节检验。不要一开始就上全内存撮合引擎那样调试成本极高反而容易掩盖策略本身的问题。多数朋友圈里流传的“复杂回测系统”其实用一个几千行的向量化框架就能覆盖需求。4.2 信号与成交时点对齐用代码看清假设下面用一段极简代码展示信号与成交时点的对齐方式。假设我们用 20 日均线产生持仓信号t 日收盘后生成信号t1 日再按开盘或收盘成交。import pandas as pd # 假设 df 已经包含 date、close 两列 df df.sort_values(date).reset_index(dropTrue) df[ma20] df[close].rolling(20).mean() df[signal] (df[close] df[ma20]).astype(int) # t日收盘确认 df[position] df[signal].shift(1) # t1日才可持仓 df[ret] df[close].pct_change() df[strategy_ret] df[position] * df[ret] # 持有期收益 cum (1 df[strategy_ret]).cumprod() print(cum.tail(10))这段代码虽然简单但已经把“信号不能当天成交”的约束体现出来了。如果你把shift去掉回测收益通常立刻变好因为策略变相获取了当天的涨跌信息。踩过这个坑之后我每次看到新的回测结果第一件事就是检查持仓序列是不是比信号慢了一拍。4.3 交易摩擦手续费、滑点、涨跌停和成交量约束回测里交易摩擦设得太低是策略造假的重灾区。手续费至少包含佣金和印花税印花税通常卖出单边收取佣金按双边万二到万三设置比较常见。滑点则取决于标的流动性市值小的标的冲击成本很高简单按固定万分之一设基本意味着研究结果被过度美化。我通常会在回测里加一档“乐观”和一档“保守”参数。乐观档是双边万五的综合成本保守档是双边千三再加固定滑点。策略如果在保守档下收益明显衰减说明它对交易成本极度敏感这类策略实盘会非常难过。还有一个容易忽视的约束是涨跌停涨停时买不进跌停时卖不出回测里如果不处理等于允许自己做一笔无法完成的交易。可以在价格表里标记涨跌停状态在信号生成后剔除不能成交的交易日。每次回测报告的末尾我都会附上一张“成本敏感度”小表格看策略在不同成本假设下的收益变化。这比回测结果本身更能说明策略的真实价值。5. 稳健性分析把过拟合从策略里挤出去5.1 别只看年化和夏普换手率、最大回撤和容量也要看看一份策略报告如果只有“年化 50%、夏普 2.5”我基本会打问号。低换手率、高容量和高稳定性往往比高收益率更重要。年化收益可以由一两段行情贡献夏普在样本少时也极不稳定把它作为唯一指标等于自欺欺人。换手率决定了交易成本也会暴露策略的敏感性。如果一个因子策略每个月狂换仓最终收益会对成本假设非常敏感。容量则指在不显著影响市场价格的情况下能承载多少资金量通常和标的的日均成交额、策略参与比例有关。一个容量 500 万的策略和一个容量 50 亿的策略研究价值完全不同。所以我更偏向用“扣费后的年化收益、最大回撤、夏普、换手率、容量”五维表格来比较策略。某个指标特别优秀而其他指标平庸不是值得高兴的事反倒要怀疑是不是过拟合。好的策略不一定每个指标都第一但各项指标之间往往有合理的关系。5.2 滚动前向验证让策略不断面对“没见过的时间段”把全部数据调一遍参数再拿同一段数据看结果不算聪明。更可靠的做法是滚动前向验证将时间轴分成若干段每次只在最新的一段训练集上选参数随后在紧接着下一段验证集上测试然后滚动推进。这样每一个验证结果都来自“没见过”的数据能比较真实地反映样本外表现。实现并不复杂核心逻辑如下results [] for start in range(0, total_len - train_len - test_len, step): train dates[start:start train_len] test dates[start train_len:start train_len test_len] best_param select_best_param(train) # 仅使用训练段 perf evaluate_strategy(test, best_param) # 用验证段评估 results.append(perf)如果滚动验证的结果普遍比“全样本调参”差一个档次不用沮丧这就是真实水平。把参数选择改成“在训练集上选在验证集上验证”的纪律是卷2阶段必须养成的习惯。坚持一段时间之后你会发现自己对“样本内收益率”的兴奋度大幅下降这其实是很好的信号。5.3 参数敏感性分析与组合权重的干扰现在机器学习流行很多人会在参数上做一个网格搜索找到最优组合。问题在于网格搜索天然容易选中过拟合区域。我的做法是在最优参数附近画一个敏感性热力图如果一个策略在某个参数上有一个尖锐的峰换个相邻参数就崩掉那它不可靠如果在很大一片平缓区域都比较稳定那才值得保留。组合层面也要注意。很多回测里“选股组合”采用等权等权对市值小的标的天然权重更高可能放大风格风险。如果想观察因子本身的边际效果可以先做等权和市值加权两组对比如果结果差异巨大多半说明因子里还混着风格暴露。等你开始调整权重组合收益曲线会明显变化别因为曲线好看就忘记问它来自哪里。我见过不少策略换个权重方案就从“优秀”变成“平庸”这种策略的核心并不在于选股能力而在于风格押注。稳健性的本质是让结论不依赖于某一个很窄的设定。6. 从研究到执行的最后一公里信号、仓位和风控6.1 信号输出与实际成交之间隔着一个执行层研究阶段的回测输出是一张带有目标持仓的表格可实盘执行不是“到了时间自动按计划下单”就完事。现实里的下单要考虑集合竞价还是连续竞价、订单是否能全部成交、大额订单对盘口的影响、规则对申报价格范围的限制等。回测里写“按开盘价成交”到了实盘等于默认自己总能抢到开盘瞬间的好价格这基本不成立。我见过不少团队在研究和交易之间搭了一层简单而有效的桥把每日信号表导出成目标持仓表再交给交易端执行交易端负责记录实际成交均价。研究端和交易端通过标准文件解耦既方便复盘也避免策略代码里混入太多下单细节。执行层的成交价格和速度应该成为回测假设的重要校准依据。6.2 仓位管理和风险预算的简单实现策略回测通常隐藏了仓位决策的复杂性。比如同样一个“买入10只股票”的信号每只买多少、总仓位多少、止损怎么设都会影响最终曲线。这里给一个很简单的固定风险预算思路每只股票预计波动率越高分配资金越少单只股票最大仓位设一个上限。计算目标权重的代码可以这样写# 假设 daily_vol 是每只标的的滚动波动率 target_weight (1 / daily_vol).clip(upper0.1) target_weight target_weight / target_weight.sum()这只是简化示例但它说明了一个原则仓位不应该是信号之外的随机结果而应该纳入回测和实盘的一整套风控框架。回测时加一个“每日最多只能调仓 X%”的约束也能提前暴露流动性风险。很多人只研究信号却忽略仓位结果信号没问题资金曲线却因为仓位集中而剧烈波动。6.3 让研究工作可复核日志、版本和结果归档最后一点听起来不酷但非常重要所有因子计算、参数、回测版本都要能追溯。我自己的做法是每次跑实验都会生成一个结果目录包含时间戳、用到的数据版本、参数文件、收益报告和净值序列。代码本身放在版本管理里数据文件做好日期版本标记。这么做的回报在半年后特别明显。那时候你会重看当初某个策略能立刻知道它当时在什么假设下做出了“好成绩”而不是看着当初的记录一头雾水。复盘不一定能让新策略变好但至少能防止旧坑重蹈。做量化的时间越长越会发现多数失误不是模型不够复杂而是研究流程的某个环节忘了记录。最后分享一个我自己的小习惯每跑一组重要实验之前我会先写一段不超过 20 行的“假设清单”把成交时点、成本、涨跌停约束、数据版本列清楚等实验跑完再回头看哪些假设最影响结果。坚持半年之后你对策略的判断力会比看一百篇文章提升得更快。卷2最大的收获不在某一招技巧里而在于你终于开始怀疑自己的回测并且知道该在哪个环节下手验证。愿意怀疑才是这一步真正迈过去的样子。