Files
etf/docs/experiments/007_momentum_window_optimization.md
aszerW 6e7087a543 docs: 添加实验007动量因子回看窗口优化研究
- 研究多周期融合(ensemble)对策略表现的影响
- 结论:多窗口融合不适用于本策略,维持25天单窗口
2026-06-12 12:37:38 +08:00

16 KiB
Raw Permalink Blame History

实验 007动量因子回看窗口优化研究

实验日期2026-06-12
实验目标研究动量因子回看窗口n_days的选择方法评估多周期融合对策略表现的影响
实验结论:多周期融合不适用于本策略,维持 25 天单窗口


一、问题背景

1.1 当前配置

策略使用 slope_r2 因子,全局统一 25 天回看窗口:

factor:
  n_days: 25
  type: slope_r2

1.2 发现的问题

从回测数据的因子分布分析发现:

标的 IQR波动代理 中位数 正得分% 特征
创业板指 28.23 0.00 51.0% 高波动,趋势间歇性强
恒生科技 20.71 -0.01 44.0% 高波动,趋势弱
纳指100 20.10 4.31 67.4% 高波动,趋势强
短债 0.38 0.68 97.9% 低波动,几乎无趋势
黄金 13.39 0.77 60.7% 中等波动,趋势持续

核心问题:不同资产的趋势周期差异很大,统一 25 天窗口是否合理?


二、学界与业界调研

2.1 经典文献的标准做法

横截面动量Cross-Sectional Momentum

Jegadeesh & Titman (1993) 开创性研究:

  • 标准窗口12-1 月过去12个月收益跳过最近1个月
  • 金融语义跳过最近1个月是因为存在短期反转效应1周-1个月而中期3-12个月存在动量效应
  • 原理:信息扩散慢 → 价格对新信息反应不足 → 形成中期趋势

Asness, Moskowitz & Pedersen (2013) "Value and Momentum Everywhere"

  • 股票12-1 月
  • 债券12-1 月或 6-1 月
  • 商品12 月(不跳过,因为商品市场短期反转弱)
  • 货币12 月
  • 核心观点:不同资产类别的最优窗口不同,但 12 月是一个稳健的起点

时间序列动量Time-Series Momentum / TSMOM

Moskowitz, Ooi & Pedersen (2012)

  • 标准窗口12 月(用于期货)
  • 金融语义TSMOM 关注资产自身的绝对收益,不与其他资产比较
  • 关键发现1-12 月窗口都有效,但 12 月最稳健
  • 波动率调整:用实现波动率标准化头寸规模,使得不同资产可比

2.2 窗口选择的金融语义

不同窗口对应的市场微观结构

窗口长度 捕捉的效应 金融解释 风险
1周-1月 短期反转 流动性冲击、过度反应修正 高换手、交易成本
1-3月 早期动量 信息扩散初期、盈余公告后漂移 容易被打断
3-12月 经典动量 信息扩散慢、机构调仓周期 最稳健
12-24月 长期动量 经济周期、企业基本面变化 均值回归开始显现
>24月 长期反转 估值回归、经济周期反转 动量效应消失

不同资产类别的特征周期

资产类别 特征周期 推荐窗口 理由
股票指数 季度财报+机构调仓 6-12月 信息扩散慢,机构季度调仓
债券 央行政策周期 3-6月 利率变化快,久期短
商品 供需周期+季节性 6-12月 供需调整慢,但有季节性
货币 利差+央行政策 3-6月 政策变化快

2.3 避免过拟合的原则

先验选择 vs 数据挖掘

过拟合的做法

# 在历史数据上测试 5, 10, 15, 20, 25, 30... 天,选最好的
for window in [5, 10, 15, 20, 25, 30, 60, 120]:
    backtest(window)
best_window = argmax(results)  # 过拟合!

有金融语义的做法

# 基于资产类别选择窗口
window_map = {
    'equity_index': 252,    # 12月252交易日
    'bond': 126,            # 6月
    'commodity': 252,       # 12月
    'currency': 126,        # 6月
}

稳健性检验原则

学界推荐的做法

  1. 选择有理论支撑的窗口12月、6月、3月是标准选择
  2. 测试邻域稳健性:如果 12 月好11 月和 13 月也应该不差
  3. 多窗口平均:用 3-12 月的多个窗口取平均,降低单窗口风险
  4. 样本外验证:在不同时间段、不同市场验证

AQR 的实践建议

  • 不要优化到极端值(如 17 天、23 天)
  • 选择"足够好"的标准窗口(如 252 天而非 247 天)
  • 关注经济解释而非统计显著性

2.4 多窗口融合方法

1. 等权平均Simple Ensemble

windows = [63, 126, 252]  # 3月、6月、12月
momentum = mean([return(p, w) for w in windows])

优点

  • 降低单窗口风险
  • 捕捉不同周期的趋势
  • 无需优化参数

缺点

  • 等权可能不合理
  • 可能引入噪音窗口

2. 波动率加权Volatility-Weighted

# 波动率低的窗口权重更高(更稳定)
weights = 1 / vol(window_i)
momentum = weighted_mean(momentum_i, weights)

金融语义:低波动窗口的信号更可靠

3. 自适应窗口Adaptive Window

基于波动率的自适应

# 高波动时缩短窗口(快速反应),低波动时延长(过滤噪音)
if realized_vol > threshold:
    window = 63   # 3月
else:
    window = 252  # 12月

基于机制转换Regime Switching

# 用 HMM 识别市场状态
regime = detect_regime(market_data)
if regime == 'trending':
    window = 252  # 趋势市用长窗口
elif regime == 'mean_reverting':
    window = 21   # 震荡市用短窗口或反转

2.5 多周期融合为什么有效?

1. 信息扩散有多个时间尺度

  • 短期1-4周:流动性冲击、技术性买卖、短期情绪(噪音)
  • 中期1-6月:盈余公告、行业数据、政策变化(信息扩散)
  • 长期6-12月:经济周期转换、产业趋势、估值重定价

单一窗口只能捕捉一个尺度的信息。多窗口融合等于同时监听多个信息频段。

2. 市场参与者的决策周期不同

参与者 决策周期 影响的价格趋势
高频/量化 天-周 短期噪音
共同基金 月-季 中期动量
养老金/保险 季-年 长期趋势
主权基金 年+ 结构性变化

当多个周期的信号一致时,意味着不同时间维度的市场参与者方向一致——这是最强的趋势确认。

3. 统计角度:偏差-方差权衡

  • 短窗口:低偏差(快速捕捉趋势变化),高方差(容易被噪音干扰)
  • 长窗口:高偏差(趋势转折时反应慢),低方差(噪音被平滑掉)

多窗口融合相当于对偏差-方差做了平均,短窗口提供灵敏度,长窗口提供稳定性。

4. 信号处理的类比

把价格序列想象成一个信号,不同窗口就是不同频率的带通滤波器:

价格信号 = 高频噪音 + 中期趋势 + 长期趋势 + 周期性波动

25天窗口  → 带通滤波器:主要透过高频成分
126天窗口 → 带通滤波器:主要透过中期成分  
252天窗口 → 低通滤波器:只保留长期趋势

多个滤波器融合 = 宽频带接收,信息更完整。

2.6 多周期融合对 slope_r2 偏好的影响

slope_r2 真正偏好的是趋势性波动高的资产(高波动+有方向),而不是单纯的高波动。

多周期融合的预期影响:

资产类型 单窗口(25天) 多周期融合 变化方向
高波动+持续趋势(纳指) 高分 高分 不变
高波动+间歇趋势(创业板) 不稳定高分 中等分 下降
低波动+持续趋势(黄金) 中等分 中等偏高分 上升
低波动+无趋势(短债) 低分 低分 不变
高波动+无趋势(恒生科技) 低分 低分 不变

核心预期:融合会让 slope_r2 的偏好从"高波动+趋势性"转向"持续性趋势"——不管波动高低,只要趋势持续就得分高。


三、实验设计

3.1 实验一IDM 信息离散动量融合

方法

IDMInformation Dispersal Momentum:正收益天数占比,衡量上涨的持续性

def info_dispersal_momentum(prices: np.ndarray) -> float:
    returns = np.diff(prices)
    positive_days = np.sum(returns > 0)
    return positive_days / len(returns)

方式一:乘法融合

def slope_r2_idm_score(prices: np.ndarray) -> float:
    sr2 = slope_r2_score(prices)
    idm = info_dispersal_momentum(prices)
    return sr2 * idm  # IDM 作为折扣系数

方式三:阈值过滤

def slope_r2_idm_filter_score(prices: np.ndarray, threshold: float = 0.5) -> float:
    idm = info_dispersal_momentum(prices)
    if idm < threshold:
        return 0.0  # 上涨天数不足阈值则清零
    return slope_r2_score(prices)

实验配置

  • 方式一type: slope_r2_idm
  • 方式三type: slope_r2_idm_filter,测试阈值 0.4/0.5/0.6

3.2 实验二:多周期融合

方法

def slope_r2_ensemble_score(prices: np.ndarray, windows: list = None) -> float:
    if windows is None:
        windows = [63, 126, 252]  # 3月、6月、12月
    
    scores = []
    for w in windows:
        if len(prices) >= w:
            window_prices = prices[-w:]
            score = slope_r2_score(window_prices)
            scores.append(score)
    
    return sum(scores) / len(scores) if scores else 0.0

实验配置

  • 配置type: slope_r2_ensemble
  • 窗口63/126/252 天3月/6月/12月
  • 数据预加载504 天2倍最大窗口

四、实验结果

4.1 实验一IDM 融合结果

方式一:乘法融合

指标 slope_r2 (baseline) slope_r2_idm 变化
总收益 288.30% 296.55% +8.25%
年化收益 24.61% 25.03% +0.42%
最大回撤 -16.27% -16.19% 略改善
Sharpe 1.17 1.20 +0.03
Calmar 1.51 1.55 +0.04
胜率 53.74% 54.48% +0.74%
调仓次数 363 374 +11

结论:方式一(乘法融合)全面小幅优于 baseline。

方式三:阈值过滤

阈值 总收益 年化 最大回撤 Sharpe Calmar 胜率
0.4 297.88% 25.10% -16.27% 1.19 1.54 53.87%
0.5 205.38% 19.85% -17.90% 1.00 1.11 53.35%
0.6 69.75% 8.96% -24.77% 0.58 0.36 56.87%

结论方式三过滤器阈值敏感0.5 和 0.6 明显变差,容易过拟合。

4.2 实验二:多周期融合结果

绩效对比

指标 slope_r2 (25天) slope_r2_ensemble (63/126/252) 变化
总收益 288.30% 182.82% -105.48%
年化收益 24.61% 18.36% -6.25%
最大回撤 -16.27% -21.61% 恶化 5.34%
Sharpe 1.17 0.96 -0.21
Calmar 1.51 0.85 -0.66
胜率 53.74% 55.47% +1.73%
调仓次数 363 167 -196

持仓频率变化

标的 baseline 占比 ensemble 占比 变化 资产类型
纳指100 44.7% 53.3% +8.6% 高波动+持续趋势
黄金 21.0% 35.8% +14.8% 低波动+持续趋势
创业板指 29.9% 40.2% +10.3% 高波动+间歇趋势
日经225 31.0% 37.7% +6.7% 中波动+持续趋势
德国DAX 27.8% 33.1% +5.3% 中波动+持续趋势
短债指数 32.0% 16.6% -15.4% 防御填充
红利低波 24.3% 11.9% -12.4% 低波动+持续趋势
有色金属 18.1% 12.8% -5.3% 高波动+周期趋势

与预期对比

预期 实际 符合?
纳指100 和黄金占比上升 纳指+8.6%,黄金+14.8% ✓ 符合
红利低波占比上升 红利低波-12.4% ✗ 不符合
创业板指占比下降 创业板指+10.3% ✗ 不符合
整体表现改善 收益降6%回撤增5% ✗ 不符合

五、结论与分析

5.1 IDM 融合结论

推荐方案:方式一(乘法融合)

理由

  1. 全面小幅优于 baseline无需调参
  2. IDM 作为折扣系数,逻辑简洁
  3. 过滤方式(方式三)阈值敏感,容易过拟合

保留代码

  • slope_r2_idm_score 函数已实现
  • FactorType.SLOPE_R2_IDM 枚举已添加
  • 可通过配置 type: slope_r2_idm 启用

5.2 多周期融合结论

结论:不适用于本策略

原因分析

  1. 长窗口反应太慢252 天窗口在趋势转折时严重滞后。2022 年全球熊市、2024 年风格切换时ensemble 无法及时退出

  2. 调仓次数骤降:从 363 次降到 167 次,说明信号太稳定了,错过了很多轮动机会。这个策略的核心价值就是轮动,过于稳定的信号反而不利

  3. 短债填充减少:从 32% 降到 16.6%,说明 ensemble 在弱势市场中也倾向于持有风险资产(因为长窗口记忆了之前的上涨趋势),导致回撤增大

  4. 创业板指上升的原因2024-2025 年创业板有持续上涨趋势ensemble 的长窗口恰好捕捉到了这个趋势,但这不是"持续性偏好",而是"恰好匹配"

核心问题

这个策略的 alpha 来源是中短期轮动25 天窗口不是长期趋势跟踪。ensemble 把因子变成了半趋势跟踪因子,与策略的核心逻辑冲突。

5.3 最终决策

维持现有配置

factor:
  n_days: 25
  type: slope_r2

理由

  1. 25 天窗口与策略的轮动逻辑匹配
  2. 多周期融合与策略核心逻辑冲突
  3. IDM 融合虽然有效,但提升有限,暂不启用

六、参考资料

学术文献

  1. Jegadeesh, N., & Titman, S. (1993). Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency. Journal of Finance, 48(1), 65-91.

  2. Asness, C. S., Moskowitz, T. J., & Pedersen, L. H. (2013). Value and Momentum Everywhere. Journal of Finance, 68(3), 929-985.

  3. Moskowitz, T. J., Ooi, Y. H., & Pedersen, L. H. (2012). Time Series Momentum. Journal of Financial Economics, 104(2), 228-250.

业界实践


七、附录:代码实现

7.1 IDM 融合因子

def info_dispersal_momentum(prices: np.ndarray) -> float:
    """Information Dispersal Momentum (IDM): 正收益天数占比"""
    if len(prices) < 2:
        return 0.0
    returns = np.diff(prices)
    positive_days = np.sum(returns > 0)
    return positive_days / len(returns)


def slope_r2_idm_score(prices: np.ndarray) -> float:
    """Slope * R² * IDM: 趋势强度 × 拟合质量 × 上涨持续性"""
    sr2 = slope_r2_score(prices)
    idm = info_dispersal_momentum(prices)
    return sr2 * idm

7.2 多周期融合因子

def slope_r2_ensemble_score(prices: np.ndarray, windows: list = None) -> float:
    """多窗口 slope_r2 等权融合"""
    if windows is None:
        windows = [63, 126, 252]  # 3月、6月、12月
    
    scores = []
    for w in windows:
        if len(prices) >= w:
            window_prices = prices[-w:]
            score = slope_r2_score(window_prices)
            scores.append(score)
    
    return sum(scores) / len(scores) if scores else 0.0

文档版本v1.0
最后更新2026-06-12
实验状态:已完成