2. 案例1
在行业轮动策略中,动量因子是最常用的打分维度之一:把各行业过去一段时间的涨幅排序,买入排名靠前的行业,定期换仓。看起来逻辑简单,但真正落地时,几乎所有人都会先遇到一个参数:回看窗口长度。用20个交易日、60个交易日还是120个交易日计算动量,会直接改变行业排序,进而改变持仓、换手和最终收益。本案例要处理的,正是"参数只动了一点点,回测曲线却像换了一个策略"这种典型现象。
先明确一个参照约定:本节所有动量都定义为区间收益率,即回看窗口末收盘价除以窗口初收盘价减一,数值越大表示动量越强,排序时降序取前N个行业。窗口长度以交易日为单位,全文与图示保持一致。
现象:一条平滑曲线旁边的"锯齿"
假设我们构建了一个最简行业轮动策略:以某行业指数池(示例取28个申万一级行业,数据为教学假设)为标的,每月最后一个交易日调仓,按动量因子降序取前5个行业等权持有,交易成本单边万分之五。固定其他条件,只把回看窗口从20日逐步增加到120日,步长5日,得到21组年化收益。
如果策略稳健,这21个点应该连成一条平缓的曲线——窗口长短只带来温和的收益衰减或提升。但实际常见的情况是:窗口取55日时年化收益约14%,取60日时骤降到3%,取65日又回到12%。相邻两个参数只差5个交易日,年化收益却相差11个百分点。这种"尖刺"就是参数敏感性的直观信号,它提示我们:策略的表现可能高度依赖某个特定窗口恰好捕捉到的几段行情,而不是依赖动量逻辑本身。
上图是本节的核心诊断工具:以回看窗口为行、调仓频率为列,把年化收益填进格子里形成热力图。它比单条曲线更有信息量,因为可以同时观察两个参数维度的交互。图中用红色虚线框标出了两处典型敏感区:60日窗口在月频下收益骤降到3.2%,而相邻的40日和80日都在12%以上;120日窗口在双周频下只有4.8%,相邻的100日和140日却分别是14.4%和9.9%。这种"孤立低谷"比"整体偏低"更值得警惕,因为它说明策略在某组参数上恰好踩空了关键行情。
为什么会剧变:排序翻转与持仓断层
参数微调导致收益剧变的机制,通常不是因子计算本身出错,而是行业排序在临界点附近发生翻转。动量排序是一个离散选择过程:只要两个行业的动量值接近,窗口稍作变化就可能让它们的名次互换。如果互换发生在第5名和第6名之间,持仓就会换掉一个行业;如果这个行业恰好是后续涨幅最大的,收益就会明显偏离。
可以用一个简化的教学示例说明。假设某月末,行业A和行业B的动量值如下:20日窗口下A为8.2%、B为7.9%,A排第5、B排第6,策略买入A;把窗口改成25日后,A变为7.1%、B变为7.6%,名次互换,策略改买B。若下个月B上涨12%、A上涨2%,仅这一个持仓差异就带来约2个百分点的组合收益差(等权5只,单只权重20%,收益差10个百分点,贡献2个百分点)。一个月如此,多次累积就形成曲线上的尖刺。
更隐蔽的情况是持仓断层:某组参数下前5名行业高度集中于同一风格(例如都是周期股),而相邻参数下前5名分散到多个风格。这时组合的风险暴露发生质变,收益差异不再来自选股,而来自风格押注。热力图上表现为整行或整列的颜色突变,而不是单个格子。
参数稳定性检验:三步定位问题
识别敏感性不能只看一条曲线,建议按以下顺序做稳定性检验。
第一步,邻域扰动测试。对每个候选参数,取其上下各2个步长的邻域,计算邻域内收益的极差和标准差。以60日窗口为例,邻域为50、55、60、65、70日,若这5个值的年化收益极差超过8个百分点,就标记为敏感参数。这个阈值不是绝对的,可按策略换手和目标波动调整,但必须有明确标准,否则"感觉还行"无法复现。
第二步,排序稳定性度量。在每个月末,记录不同窗口下前5名行业的名单,计算名单重合度(交集数量除以5)。若某窗口与其邻域的平均重合度低于60%,说明排序本身不稳定,收益剧变只是表象。重合度是比收益更前置的指标,因为它不依赖后续行情,能更早暴露问题。
第三步,分段一致性检验。把回测区间切成若干段(例如按年度),分别计算每组参数在各段的收益排名。稳健参数应在多数分段中都处于中上位置,而不是靠某一年的极端行情拉高整体。若某参数的总收益很高但只在一年里领先,其余年份垫底,基本可以判定为过拟合。
热力图诊断的实操细节
热力图不是画出来好看就行,几个细节决定它能否真正用于判断。第一,颜色映射要固定:所有格子用同一套收益到颜色的映射,且最好以0为中性色、正收益偏暖、负收益偏冷,避免每组参数各自归一化导致视觉误导。第二,格子要标数值:颜色只提供直觉,判断跳变幅度必须看数字,相邻格差值超过阈值时用边框标出。第三,行列顺序要按参数单调排列,不能按收益排序,否则敏感区会被打散,看不出邻域关系。
在实现上,用Python的pandas做参数网格扫描即可:外层循环窗口长度,内层循环调仓频率,每个组合调用一次回测函数,把年化收益、最大回撤、换手率一起存入结果表,最后用透视表转成矩阵。示例伪代码如下,参数与数据均为教学假设:
windows = range(20, 145, 5) # 回看窗口,交易日
freqs = ['W', '2W', 'M', '2M', 'Q'] # 调仓频率
results = []
for w in windows:
for f in freqs:
ret = backtest(momentum_window=w, rebalance=f,
top_n=5, cost=0.0005)
results.append({'window': w, 'freq': f,
'ann_ret': ret.annual_return,
'mdd': ret.max_drawdown,
'turnover': ret.turnover})
grid = pd.DataFrame(results).pivot(
index='window', columns='freq', values='ann_ret')
得到矩阵后,除了画热力图,还应计算一个简单的敏感度指标:对每一行求相邻列的收益差绝对值之和,再对每一列求相邻行的差绝对值之和,两者都大的区域就是二维敏感区。这个指标可以直接排序,帮助快速锁定需要人工复核的参数组合。
结果判读与处置
发现敏感区后,不要急着"换一组好看的参数"。正确的处置顺序是:先确认敏感区是否由少数几个调仓日的排序翻转造成,回看那几个月的行业动量值和持仓名单;再检查是否存在数据问题,例如某行业在窗口边界附近有停牌、分红除权未复权,导致收益率计算异常;最后才考虑参数本身是否合理。若确认是排序临界翻转,可以采取两种改进:一是对动量值做平滑(例如取多个窗口动量的均值或排名平均),降低单一窗口的偶然性;二是设置缓冲区,排名前5买入、跌出前8才卖出,减少临界点附近的频繁换手。
需要强调的是,参数稳定性检验的目的不是找到一个"最优参数",而是判断策略的收益是否来自可重复的逻辑。如果一组参数在邻域内表现平稳、名单重合度高、分段胜率稳定,即使它的绝对收益不是网格里最高的,也更值得作为主参数。反之,网格里那个孤立的最高点,往往正是过拟合最典型的伪装。