16 KiB
实验 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月
}
稳健性检验原则
学界推荐的做法:
- 选择有理论支撑的窗口:12月、6月、3月是标准选择
- 测试邻域稳健性:如果 12 月好,11 月和 13 月也应该不差
- 多窗口平均:用 3-12 月的多个窗口取平均,降低单窗口风险
- 样本外验证:在不同时间段、不同市场验证
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 信息离散动量融合
方法
IDM(Information 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 融合结论
推荐方案:方式一(乘法融合)
理由:
- 全面小幅优于 baseline,无需调参
- IDM 作为折扣系数,逻辑简洁
- 过滤方式(方式三)阈值敏感,容易过拟合
保留代码:
slope_r2_idm_score函数已实现FactorType.SLOPE_R2_IDM枚举已添加- 可通过配置
type: slope_r2_idm启用
5.2 多周期融合结论
结论:不适用于本策略
原因分析:
-
长窗口反应太慢:252 天窗口在趋势转折时严重滞后。2022 年全球熊市、2024 年风格切换时,ensemble 无法及时退出
-
调仓次数骤降:从 363 次降到 167 次,说明信号太稳定了,错过了很多轮动机会。这个策略的核心价值就是轮动,过于稳定的信号反而不利
-
短债填充减少:从 32% 降到 16.6%,说明 ensemble 在弱势市场中也倾向于持有风险资产(因为长窗口记忆了之前的上涨趋势),导致回撤增大
-
创业板指上升的原因:2024-2025 年创业板有持续上涨趋势,ensemble 的长窗口恰好捕捉到了这个趋势,但这不是"持续性偏好",而是"恰好匹配"
核心问题:
这个策略的 alpha 来源是中短期轮动(25 天窗口),不是长期趋势跟踪。ensemble 把因子变成了半趋势跟踪因子,与策略的核心逻辑冲突。
5.3 最终决策
维持现有配置:
factor:
n_days: 25
type: slope_r2
理由:
- 25 天窗口与策略的轮动逻辑匹配
- 多周期融合与策略核心逻辑冲突
- IDM 融合虽然有效,但提升有限,暂不启用
六、参考资料
学术文献
-
Jegadeesh, N., & Titman, S. (1993). Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency. Journal of Finance, 48(1), 65-91.
-
Asness, C. S., Moskowitz, T. J., & Pedersen, L. H. (2013). Value and Momentum Everywhere. Journal of Finance, 68(3), 929-985.
-
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
- Systematic Trend-Following with Adaptive Portfolio Construction
- Value and Momentum Everywhere - AQR Capital Management
七、附录:代码实现
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
实验状态:已完成