这时最容易犯的错误,是把一切归结为市场风格切换。风格变化当然会让策略衰减,但它通常不是第一嫌疑人。更常见的情况是,数据在回测中提前泄露,特征计算混入了未来信息,研究人员在大量试验中挑中了最幸运的参数组合,或者回测的成交假设根本不符合实盘。
策略失效不能只看一条收益曲线。要把收益差异拆成数据偏差、信号偏差、模型偏差和执行偏差,再沿着时间顺序回到代码、数据表和订单记录里逐层核对。明日开盘前,可以先从下面这条路径开始:检查点时数据架构,隔离特征生成时点,重新评估多重检验后的夏普,再用影子组合核对实盘成交,最后通过同算法双跑和灰度上线确认工程实现没有走样。
数据架构的隐形陷阱:从前视偏差到点时一致性
回测里看到的漂亮股票,可能是市场替你筛过的幸存者,而不是当年真正可以买到的全部股票。
第一层要排查的是幸存者偏差。今天仍在交易的股票池,并不等于历史上每个时点都存在的股票池。退市股票、长期停牌股票、曾经被特别处理的股票,以及后来被并购或吸收合并的标的,如果从历史数据中消失,回测系统就会在不知不觉中获得一份事后筛选过的样本。
例如,用当前股票池回测多年前的选股策略,系统天然避开了后来表现极差、无法继续交易或已经退出市场的标的。即使策略没有任何选股能力,也可能因为样本被清理得过于干净而得到更好的结果。这个问题不能通过简单地把股票数量补回去解决,因为关键不只是股票是否存在,还包括它在当时是否属于可交易状态、当时的证券代码是什么、行业分类如何记录,以及策略当时是否有资格把它纳入候选池。
第二层是前视偏差。它的表现通常更隐蔽:代码可以正常运行,回测也没有报错,但信号在生成时使用了当时尚未公开或尚未形成的数据。
最典型的错误,是用当日收盘价计算特征,却把信号安排在当日开盘成交;或者用当天最终确认的财务指标,回填到财报正式披露之前的历史区间。还有一种常见情况是,研究数据经过后复权或统一清洗,价格序列看起来连续漂亮,但回测引擎没有同步处理复权因子生效的时间,导致历史上的价格和可交易价格不再对应。
点时数据库不是一个字段,而是一套权限边界
解决这类问题,不能只在回测脚本里加一句日期过滤。真正可靠的做法,是建立点时数据库,也就是让每条数据同时记录“数据描述的时间”和“这条信息何时可以被市场知道”。
这两个时间经常不同:
- 行情数据描述的是某个交易时段的价格、成交量和盘口状态,但它在收盘前和收盘后可用的内容不同。
- 财报数据描述的是某个报告期的经营结果,但市场能够使用它的时间通常是公告发布时间,而不是报告期结束日。
- 行业分类、成分股名单和评级数据都有自己的生效时间,不能只保留当前版本。
- 复权因子可能在除权除息后发生变化,但回测需要知道历史上当时可见的价格口径。
因此,一张只包含“交易日期”和“数值”的表,通常不足以支撑严谨回测。至少需要明确以下信息:
- 观测时间:这条数据描述的是哪个交易日、报告期或事件发生时点。
- 发布时间:数据何时被公开,或者何时进入策略可访问的数据源。
- 生效时间:对于行业分类、指数成分和规则变更,需要记录新值从何时开始生效。
- 修订版本:财务数据、宏观数据和清洗后的基础数据可能被修订,历史回测不能默认使用最终修订版本。
- 数据来源与版本:不同供应商的字段定义、交易日历和复权规则可能并不相同。
在查询层面,特征函数必须接收一个明确的时点参数。它不应当直接读取数据库中的最新快照,而应当返回截至该时点已经公开的数据。类似下面这种不带日期约束的全表读取,在研究阶段很方便,在生产级回测中却属于高风险接口:
SELECT * FROM financials
更稳妥的查询逻辑,是让数据访问层强制要求调用者提供时点,并由底层接口统一处理可见性。这样做的价值在于,研究人员即便忘记手动加过滤条件,也不容易越过数据权限边界。
回测时间、信号时间和成交时间要分开
许多前视偏差并不是来自数据表,而是来自时间定义模糊。一个交易日至少可以拆成三个概念:
1. 信息可用时间:策略真正能够读取这条信息的时间。
2. 信号生成时间:模型完成计算并决定交易方向的时间。
3. 订单成交时间:订单进入市场后实际成交的时间。
如果策略使用收盘价生成信号,那么信号通常不能被安排在同一根日线的开盘成交。若策略使用盘中数据,数据延迟、聚合周期和订单提交时间也必须写进回测规则。否则,系统会把一条在现实中尚未产生的信息,提前放到交易决策之前。
在排查时,不要只问“这列数据属于哪一天”,还要问“策略在什么时刻能看到它”。这两个问题的答案不一致,往往就是回测和实盘出现断层的起点。
时间穿越测试:故意污染未来,观察信号是否异常
排查前视偏差,一个很实用的方法是做反向污染测试:有意让特征访问未来数据,观察策略是否出现不合比例的改善。
以“过去若干日均价”为例,可以分别构造正常版本和人为加入未来一天收盘价的污染版本,然后比较信号变化、换手率变化和收益曲线变化。污染版本当然不应被用于评价策略,但它可以帮助确认测试框架是否真的能够捕捉到未来信息。
更系统的方式,是为每个特征设置不同的未来偏移量,记录以下结果:
- 受到影响的信号数量;
- 买卖方向发生变化的日期;
- 持仓权重变化;
- 换手率和交易成本变化;
- 收益、回撤及风险指标的变化。
需要注意的是,不能把某个固定的收益变化比例当成唯一判定标准。不同策略的敏感性不同,低频策略和高频策略、横截面因子和时序信号的反应也不一样。更可靠的做法,是结合信号级别的差异来判断:只要一个特征在加入未来信息后大面积改变历史信号,就应当回到数据来源和窗口边界重新审计。
特征工程的“未来窥视”:如何通过视图校验剔除无效信号
特征函数如果提前看到了底牌,回测曲线就不是预测结果,而是事后重放。
特征工程的核心不是把更多字段堆进模型,而是保证每个字段在信号生成时确实可见。要做到这一点,最好把特征视为带有“可见时间”的数据对象,而不是普通的数据框列。
例如,财报中的净利润同比,不能只用报告期字段标记;它还需要绑定公告时间。一个在年末形成的经营结果,如果在次年某个日期才披露,那么策略在披露之前只能使用上一版公开信息。对于修订报告,也要保留版本变更,否则最终数据库中的数值会倒灌到较早的回测区间。
第一层:单变量穿越检验
先从核心特征开始做单变量检验。可以选择过去一段时间的成交量均值、动量指标、波动率、财务指标或行业相对强弱,再为每个特征单独构造未来偏移版本。
检验重点不是看污染版本能赚多少,而是看它改变了什么:
- 信号是否集中出现在收益最好的日期;
- 原本没有交易的日期是否突然出现大量开仓;
- 特征排名是否发生整体重排;
- 交易方向是否出现系统性变化;
- 收益改善是否只来自少数极端交易。
如果加入很少的未来信息就能让大量信号重新排序,说明特征对时点边界极其敏感。此时不能把结果解释为“信号很强”,更应该先确认它是否包含了不该出现的信息。
单变量检验还可以帮助识别组合层面的掩盖效应。多个特征一起进入模型时,一个有问题的字段可能被其他字段的噪声遮住;单独拆出来后,问题通常更容易定位。
第二层:滚动窗口和标签窗口严格分离
滚动计算需要明确窗口的左边界和右边界。默认参数并不等于安全参数,因为默认值可能在不同库版本、封装函数或团队公共组件中被改写。
在日频研究中,常见的安全原则包括:
- 滚动统计只使用当前时点及其之前的数据;
- 需要预测下一交易日时,标签和特征必须明确错开;
- 未来收益只能存在于标签表,不应出现在特征表;
- 特征生成、标签生成和样本切分最好使用不同的数据对象;
- 训练集、验证集和测试集之间不能因为填充、标准化或排序操作发生信息交换。
例如,shift(-5)经常被用于构造未来五日收益标签,这本身并不一定错误,关键在于它是否被误放进特征工程流程。如果这列数据参与了特征选择、标准化、缺失值处理或模型训练,就会把未来收益带回输入端。
滚动窗口的对照可以写成下面这样:
| 检查对象 | 高风险做法 | 更稳妥的处理 |
|---|---|---|
| 滚动统计 | 依赖默认窗口方向,未显式说明边界 | 明确窗口只向历史展开,并在测试中验证边界 |
| 未来收益 | 与特征放在同一张宽表中统一处理 | 标签单独生成,训练前再按时点连接 |
| 标准化 | 用全样本均值和标准差处理历史数据 | 每个训练窗口只使用当时可见样本拟合参数 |
| 缺失值填充 | 对所有字段统一向前填充 | 按字段语义决定是否允许填充,并限制填充范围 |
| 排名与分组 | 先使用全市场最终数据再回填历史 | 在每个交易时点内独立完成截面计算 |
fillna也是经常被忽略的风险点。价格字段、财务字段、停牌状态和行业分类的缺失含义不同,不能因为代码简洁就全表采用同一种填充方法。尤其是跨证券、跨报告期或跨数据源的填充,很可能把另一时点的信息带进当前样本。
第三层:特征视图校验
与其让每个研究员自行记住所有时间规则,不如在数据层建立特征视图。每个特征视图都应当带有元数据,至少说明:
- 特征的观测窗口;
- 特征的最早可用时间;
- 是否依赖公告数据;
- 是否允许向前填充;
- 是否需要交易日历对齐;
- 是否会因为停牌、缺失或修订而改变。
回测引擎读取特征时,先检查特征的可用时间是否早于信号时间。如果不满足,就拒绝计算,而不是默默返回一个数值。对于研究效率而言,这种“宁可报错,不要猜测”的设计比自动补齐更值得信任。
分布检验也有价值,但不能把它当成前视偏差的单独证据。特征分布发生变化,可能来自数据源更换、市场结构变化、股票池变化或真实的制度环境改变。可以按时间窗口检查均值、波动、分位数和缺失率,再结合数据版本和信号日志定位原因。统计检验只能告诉你“这里有差异”,不能自动告诉你“差异一定来自未来信息”。
因此,发现异常后要回看三类记录:
1. 数据供应商和字段版本是否发生切换;
2. 股票池、行业分类和交易规则是否发生变化;
3. 特征计算的窗口和发布时间是否在某个日期后改变。
多重检验与过拟合:利用概率夏普与折损夏普修正真实性
一个好看的夏普,不足以证明策略有效;你还要说明它经过了多少次尝试。
数据窥视不仅发生在读取未来数据时,也发生在研究流程中。研究人员反复调整参数、指标、持仓周期、止盈止损和股票池,最后从众多结果中挑出表现最好的一组。即便每次试验都严格使用历史数据,最终被选中的策略也可能只是噪声中的幸运者。
这类问题的关键,不是禁止试验,而是完整记录试验。参数组合、特征版本、样本区间、交易成本假设、异常处理规则和筛选理由,都应该进入研究日志。没有试验记录,就无法估计最终结果受选择过程影响的程度。
多重检验:显著不等于可信
当候选策略越来越多,单独看某个策略的显著性水平会产生误导。常见的处理方法包括:
- 邦弗罗尼校正:将整体显著性要求分摊到多次检验中,控制整体出现假阳性的风险。它比较保守,候选数量较少或错误代价较高时更容易解释。
- 错误发现率控制:允许候选集合中存在一定比例的假阳性,以换取更高的信号发现能力。它更适合研究阶段的大量候选筛选,但不能被理解为每个入选策略都可靠。
- 家族错误率控制:关注整个检验集合中至少出现一次假阳性的概率,通常比错误发现率控制更严格。
这些方法的共同点,是把“我试了多少次”纳入结论。只报告最佳参数的夏普和回撤,而不报告候选数量、淘汰规则及数据切分方式,相当于只展示选拔结果,不展示选拔过程。
概率夏普:先承认估计误差
样本夏普是一个估计值,不是策略真实风险收益比的直接观测。样本期较短、收益分布偏斜、尾部事件较多或收益存在自相关时,样本夏普尤其容易不稳定。
概率夏普的思路,是估计真实夏普超过某个基准的概率。它会考虑收益分布的偏度和峰度,也会受到样本量影响。样本量不足时,即便观察到较高的夏普,结论仍然可能不稳;样本量增加后,估计才逐步收敛。
使用概率夏普时,不能只看一个百分比就下结论。还要同时检查:
- 收益序列是否按正确的持仓频率构造;
- 是否把未成交订单误算成已成交;
- 是否将资金曲线收益与单笔交易收益混在一起;
- 是否考虑了交易成本、融资成本和停牌期间的资金占用;
- 收益分布是否存在明显的非正态特征。
折损夏普:把试验次数和运气放回模型
折损夏普在概率夏普的基础上,进一步考虑候选策略数量以及收益分布特征。它关注的问题不是“这条曲线是否看起来不错”,而是“在尝试了这么多次之后,这样的结果是否仍然超出偶然获得的范围”。
这对参数优化尤其重要。假设研究人员反复试验不同的回看窗口、调仓频率和阈值,最后只保留表现最好的结果,那么最佳夏普已经包含了选择偏差。试验越多,最优结果越可能夹带运气成分,验证标准就应该越严格。
折损夏普不能被简化为一个固定门槛。不同策略的试验数量、独立程度、样本长度和收益分布不同,不能机械地规定“超过某个数就有效,低于某个数就失效”。更好的报告方式是同时展示:
| 项目 | 应说明的内容 |
|---|---|
| 观察夏普 | 使用哪种收益序列、是否扣除交易成本 |
| 试验规模 | 尝试过多少组参数、特征和样本区间 |
| 样本结构 | 训练、验证和测试的时间范围及是否重叠 |
| 收益分布 | 偏度、峰度、自相关和极端收益情况 |
| 修正结果 | 概率夏普、折损夏普及其假设 |
| 稳健性 | 参数扰动、时间切片和不同成本假设下是否仍成立 |
如果一个策略只有在某个精确参数、某个特定起止日期和某种理想成交假设下表现良好,它更像是一个研究结果,而不是已经完成验证的交易系统。真正值得继续推进的信号,应该在相邻参数、不同时间切片和更保守成本条件下保持大致一致的逻辑。
回测与实盘的鸿沟:影子组合与成交假设的拆解归因
回测假设你总能按理想价格成交,实盘则会提醒你,流动性不是免费服务。
策略从回测到实盘,收益差异不能笼统归因于滑点。滑点只是结果,不是原因。要知道差异从哪里来,需要建立影子组合:影子组合使用真实产生的交易信号,但按照实盘订单、成交回报和市场状态记录组合变化。
影子组合至少要保存四套时间:
- 信号生成时间;
- 订单提交时间;
- 委托被接受或撤改单的时间;
- 实际成交时间。
如果只有最终成交价,没有这些过程时间,就很难判断损耗来自信号延迟、通道拥堵、拆单效率还是市场价格本身的变化。
把执行损耗拆成可解释的项目
常见的执行差异可以拆成以下几类:
| 成本类型 | 归因思路 | 需要保留的记录 |
|---|---|---|
| 买卖价差 | 比较信号参考价与可成交盘口之间的差异 | 信号时盘口、成交方向、成交价 |
| 市场冲击 | 订单规模改变盘口后带来的额外价格损耗 | 委托数量、盘口深度、分笔成交 |
| 时间延迟 | 从信号生成到订单成交期间的价格变化 | 信号、提交、接受和成交时间 |
| 未成交损耗 | 订单未完成导致的机会成本或仓位偏离 | 撤单、拒单、部分成交和剩余数量 |
| 资金与借券成本 | 持仓、融资融券或期货保证金带来的实际成本 | 资金流水、利息、保证金和费用明细 |
| 交易规则影响 | 涨跌停、停牌、最小单位等造成的无法成交 | 交易状态、盘口状态和订单拒绝原因 |
影子组合和实盘组合的关系,也可以帮助区分策略层与执行层问题。
如果影子组合按照真实成交回报重放后,曲线与实盘高度接近,而理想回测明显更好,那么主要问题在成交假设。可能是回测使用了信号价成交,忽略了盘口深度,也可能是没有模拟部分成交、撤单、排队和成交延迟。
如果影子组合仍然明显优于实盘,问题可能在组合记账、资金同步、订单状态回传或实盘风控层。此时不能继续修改因子,而应先核对持仓、现金、手续费和订单生命周期。
如果从信号开始,两套系统给出的买卖方向就不一致,才需要回到数据和算法层检查。常见原因包括:
- 回测使用收盘后数据,实盘使用盘中不完整数据;
- 复权价格和实盘成交价格口径不同;
- 回测允许买卖停牌或触及涨跌停的标的;
- 期货合约换月规则没有与实盘一致;
- 股票、期货和现金头寸的单位换算不一致;
- 实盘风控在信号生成后改变了订单,但回测没有记录这次拒绝。
股票和期货的成交假设不能混为一谈
股票回测需要处理涨跌停、停牌、最小交易单位、集合竞价和成交量约束。对于无法成交的订单,应当明确是保留、撤销还是顺延,而不是默认在下一根K线自动完成。
期货还要额外处理合约到期、换月、主力连续合约与真实合约之间的差异。连续合约适合研究价格序列,但它不一定对应一个可以直接下单的真实合约。如果回测使用连续合约,实盘却持有具体到期月份的合约,那么价差、换月和流动性差异都可能被误算成策略收益。
回测撮合器至少应该让以下参数显式可见:
- 成交价采用买一、卖一、中间价还是区间内其他价格;
- 成交是否受当根成交量限制;
- 订单能否部分成交;
- 信号与订单之间存在多长延迟;
- 交易费用、滑点和冲击成本如何计算;
- 无法成交时如何处理;
- 调仓时是否考虑已有仓位和现金约束。
这些设置不是越复杂越好,但必须与实际执行方式一致。一个复杂却无法解释的撮合器,未必比一个简单而保守的模型更可靠。
工程化排查路径:同算法双跑与灰度上线
策略上线不是让程序开始运行,而是让回测、模拟和实盘在同一套规则下持续对比。
当数据和模型完成初步审计后,下一步是确认工程实现没有把研究逻辑悄悄改写。最有效的办法不是等待净值出现异常,而是提前建立可对比的中间结果。
第一步:同算法双跑
让回测引擎和实盘引擎使用同一批输入数据、同一版本的特征计算和同一套组合构建规则,输出每日应下单清单。对比不应只看最终标的,还要逐项核对:
- 买卖方向;
- 目标数量和目标权重;
- 参考价格;
- 信号生成时间;
- 风控过滤结果;
- 被拒绝的交易及拒绝原因;
- 组合现金和已有持仓;
- 订单四舍五入后的实际数量。
若两边结果不一致,先不要讨论策略是否有效。应该把差异定位到最小可复现样本:某个证券、某个日期、某个特征或某次订单。常见的工程原因包括:
- 数据接口的单位不统一;
- 复权处理方式不同;
- 交易日历不一致;
- 时区或时间戳精度不同;
- 缺失值处理规则不同;
- 最小交易单位和数量取整方式不同;
- 交易状态过滤在两套系统中执行顺序不同;
- 随机数种子、模型版本或依赖库版本不同。
双跑结果应当写入日志,并保留输入快照。否则下一次重新运行时,数据已经变化,团队只能看到“现在不一致”,却无法知道最初是哪一步产生了偏差。
第二步:影子组合预演
在正式扩大仓位之前,让策略以模拟方式接受真实行情、真实信号和真实订单回报。影子组合不需要承担全部资金风险,但必须尽可能遵循实际的交易流程:包括延迟、部分成交、撤单、交易限制和资金占用。
这一步的重点不是观察某一天赚没赚钱,而是观察偏离是否稳定:
- 信号组合与实际持仓之间的差异;
- 目标权重与成交权重之间的差异;
- 计划成交价与实际成交价之间的差异;
- 预期成本与实际成本之间的差异;
- 回测、影子组合和实盘三条曲线的相对关系。
如果影子组合的损耗稳定且可以解释,说明执行假设至少有机会被量化。如果偏离随着交易量增加而快速扩大,就要重新评估策略容量、订单拆分和流动性约束,而不是简单地提高资金规模。
第三步:分批灰度上线
灰度上线的目的不是用小资金证明策略一定赚钱,而是验证系统在真实环境中的行为。仓位增加应当有明确的暂停条件,例如:
- 信号与双跑结果持续不一致;
- 订单拒绝或部分成交异常增加;
- 影子组合与实盘组合的偏离扩大;
- 实际交易成本明显超过研究假设;
- 数据延迟、字段缺失或时间戳异常;
- 风控模块频繁介入,却没有被回测记录。
任何一个条件触发,都应该先停在当前规模,保存现场数据,再回到代码和订单日志复盘。灰度阶段最忌讳的是把系统异常解释为“样本还不够”,然后继续加仓。没有完成归因之前,扩大交易规模只是在放大未知风险。
第四步:定期进行点时审计
数据供应商切换、字段更新、数据库迁移和清洗脚本改动,都可能重新引入前视偏差。点时审计不能只在策略上线前进行,生产环境中的数据链路也需要持续检查。
审计可以围绕以下问题展开:
- 新增字段是否记录了发布时间和版本;
- 财务数据是否区分报告期、公告日和修订日;
- 行情数据是否保留停牌、涨跌停和成交状态;
- 复权因子是否在正确的生效时点切换;
- 行业分类和指数成分是否使用了当时有效的版本;
- 数据源切换前后,历史字段定义是否一致;
- 清洗脚本是否会把未来修订值回填到旧日期;
- 特征视图是否仍然拒绝访问信号时点之后的数据。
对于每一次数据或代码发布,都应当保存一组回归测试结果。只要输入版本没有变化,核心中间结果就不应无故改变;如果确实发生变化,必须能够解释变化来自哪里。
收尾:策略失效不是命运,而是尚未还清的工程债
量化策略进入实盘后失效,并不神秘。市场结构变化确实存在,但在下结论之前,应该先确认数据、算法和执行链路没有把结果污染。
一条收益曲线只能告诉你发生了什么,不能告诉你为什么发生。真正有用的归因,需要把问题拆开:
- 点时数据架构负责回答:信号生成时,这条信息是否已经可见;
- 特征视图校验负责回答:模型输入是否只使用了历史信息;
- 多重检验、概率夏普和折损夏普负责回答:这个结果是否经得起试验次数和估计误差的修正;
- 影子组合负责回答:理论成交与真实成交之间的损耗来自哪里;
- 同算法双跑和灰度上线负责回答:研究逻辑是否被工程实现改变。
不要把“回测很好、实盘很差”当成一个需要重新调参的问题。先把差异归因到数据、信号、组合和执行中的具体环节,再决定是否修改模型。否则每一次失败都会催生一轮新的参数搜索,而新的参数搜索又会制造更多过拟合。
明日开盘前,先检查裸查询和时间戳,再核对特征窗口与标签边界;随后重新记录策略经历过的试验数量,评估修正后的夏普;最后让影子组合和实盘订单逐笔对照。三道关都能解释清楚,策略才有资格继续运行。
回测曲线可以漂亮,但漂亮本身不是证据。只有当数据在时间上诚实、信号在验证中稳定、成交在现实中可复现,策略才不再只是回测机里的西施。
Related reading: 量化策略的终极博弈:为什么高质量数据比复杂算法更决定成败 and 量化模型中的前瞻偏差:数据泄露的隐蔽成因与检测机制.
