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

471 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 实验 007动量因子回看窗口优化研究
**实验日期**2026-06-12
**实验目标**研究动量因子回看窗口n_days的选择方法评估多周期融合对策略表现的影响
**实验结论**:多周期融合不适用于本策略,维持 25 天单窗口
---
## 一、问题背景
### 1.1 当前配置
策略使用 `slope_r2` 因子,全局统一 25 天回看窗口:
```yaml
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 数据挖掘
**过拟合的做法**
```python
# 在历史数据上测试 5, 10, 15, 20, 25, 30... 天,选最好的
for window in [5, 10, 15, 20, 25, 30, 60, 120]:
backtest(window)
best_window = argmax(results) # 过拟合!
```
**有金融语义的做法**
```python
# 基于资产类别选择窗口
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
```python
windows = [63, 126, 252] # 3月、6月、12月
momentum = mean([return(p, w) for w in windows])
```
**优点**
- 降低单窗口风险
- 捕捉不同周期的趋势
- 无需优化参数
**缺点**
- 等权可能不合理
- 可能引入噪音窗口
#### 2. 波动率加权Volatility-Weighted
```python
# 波动率低的窗口权重更高(更稳定)
weights = 1 / vol(window_i)
momentum = weighted_mean(momentum_i, weights)
```
**金融语义**:低波动窗口的信号更可靠
#### 3. 自适应窗口Adaptive Window
**基于波动率的自适应**
```python
# 高波动时缩短窗口(快速反应),低波动时延长(过滤噪音)
if realized_vol > threshold:
window = 63 # 3月
else:
window = 252 # 12月
```
**基于机制转换Regime Switching**
```python
# 用 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**:正收益天数占比,衡量上涨的持续性
```python
def info_dispersal_momentum(prices: np.ndarray) -> float:
returns = np.diff(prices)
positive_days = np.sum(returns > 0)
return positive_days / len(returns)
```
**方式一:乘法融合**
```python
def slope_r2_idm_score(prices: np.ndarray) -> float:
sr2 = slope_r2_score(prices)
idm = info_dispersal_momentum(prices)
return sr2 * idm # IDM 作为折扣系数
```
**方式三:阈值过滤**
```python
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 实验二:多周期融合
#### 方法
```python
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 最终决策
**维持现有配置**
```yaml
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.
### 业界实践
- [Momentum Factor Investing: 30 years of Out of Sample Data](https://alphaarchitect.com/momentum-factor-investing-30-years-of-out-of-sample-data/)
- [Systematic Trend-Following with Adaptive Portfolio Construction](https://arxiv.org/html/2602.11708v1)
- [Value and Momentum Everywhere - AQR Capital Management](https://www.aqr.com/Insights/Research/Journal-Article/Value-and-Momentum-Everywhere)
---
## 七、附录:代码实现
### 7.1 IDM 融合因子
```python
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 多周期融合因子
```python
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
**实验状态**:已完成