- simple_rotation.py: 新增 standardized_slope_score 函数 (slope/SE) - config_loader.py: FactorType 枚举新增 STANDARDIZED_SLOPE - 对比实验结果: standardized_slope 年化 13.73% vs slope_r2 19.84% - 结论: t-statistic 过度惩罚高波动资产的有效趋势信号,不适合本场景 - 文档更新: 动量因子对比调研报告新增 3.3 节详细分析
11 KiB
动量因子对比调研报告
调研日期:2026-06-06
回测区间:2020-01-10 ~ 2026-06-05
当前结论:slope_r2作为默认因子(年化 19.84%,夏普 1.14)
1. 问题背景
原始策略使用 weighted_momentum 因子,通过加权线性回归计算动量得分。诊断分析发现三个信号质量瓶颈:
- 跨市场动量不可比:同组换仓 alpha=+0.47%,跨组换仓仅 +0.03%
- IC 极低:IC=0.07,ICIR=0.17
- T+1 执行噪声:跨市场约 50% 翻转率
核心问题是:不同资产的动量得分量纲差异巨大(如 CL=F 原油极端值可达 375,527,068),导致跨资产比较失真。
2. 四种因子公式对比
2.1 weighted_momentum(加权线性回归动量)
\text{Score} = (e^{\beta \times 250} - 1) \times R^2
计算逻辑:
- 价格变换:$y = \ln(\text{prices})$,取对数
- 回归权重:$w = \text{linspace}(1, 2, n)$,近期权重线性递增
- 加权线性回归:
y = \beta x + \alpha - 年化收益:
e^{\beta \times 250} - 1 - $R^2$:加权决定系数
特点:近期加权强调趋势延续性,对数收益使跨资产量纲部分可比。
2.2 vol_adjusted_momentum(波动率调整动量)
\text{Score} = \frac{\text{年化收益}}{\text{年化波动率}} \times R^2
计算逻辑:
- 回归逻辑与 weighted_momentum 完全相同
- 额外除以已实现波动率:
\sigma = \text{std}(\text{daily\_returns}) \times \sqrt{250} - 最小波动率保护:
\sigma \geq 0.01
学术来源:Moskowitz, Ooi & Pedersen (2012) Time Series Momentum,类夏普比率构造。
特点:理论上使跨资产更可比,但实践中波动率计算对极端值敏感。
2.3 slope_r2(斜率×R²,归一化价格)
\text{Score} = 10000 \times \text{slope} \times R^2
计算逻辑:
- 价格变换:$y = \text{prices} / \text{prices}[0]$,归一化而非取对数
- 无权重,普通最小二乘回归:
y = \text{slope} \cdot x + \text{intercept} - $R^2$:无权重决定系数
- 乘以 10000 使数值范围便于阅读
特点:归一化天然消除量纲差异,所有资产起点为 1.0,斜率在同尺度上可比。
2.4 momentum(简单收益率)
\text{Score} = \frac{\text{prices}[-1]}{\text{prices}[0]} - 1
计算逻辑:
- 最朴素:期末价格 / 期初价格 - 1
- 不考虑趋势质量(无 $R^2$)、不考虑波动率
特点:简单但缺乏趋势质量过滤,对短期噪声敏感。
3. 回测对比结果
3.1 核心指标
| 因子类型 | 年化收益 | 夏普比率 | 最大回撤 | Calmar | 调仓次数 | 胜率 |
|---|---|---|---|---|---|---|
| weighted_momentum | 18.36% | 1.02 | -16.36% | 1.12 | 405 | 54.0% |
| vol_adjusted_momentum | 13.16% | 0.85 | -18.61% | 0.71 | 393 | 55.9% |
| slope_r2(当前默认) | 19.84% | 1.14 | -15.35% | 1.29 | 394 | 54.1% |
| momentum | 9.27% | 0.57 | -17.42% | 0.53 | 729 | 53.3% |
| standardized_slope | 13.73% | 1.01 | -13.52% | 1.02 | 335 | 54.5% |
结论:slope_r2 全面胜出,年化 +1.48%,夏普 +0.12,回撤改善 +1.01%。
注:
standardized_slope(t-statistic)回撤更小但收益大幅落后(年化 -6.11%),说明统计显著性过滤在高波动资产上过度惩罚趋势信号,不适合本场景(详见 3.3)。
3.2 数值尺度分析(2024-06-03 截面)
| 因子 | 最大值 | 最小正值 | max/min 比值 |
|---|---|---|---|
| weighted_momentum | 0.633 | 0.0003 | 2179x |
| vol_adjusted_momentum | 4.812 | 0.0014 | 3378x |
| slope_r2 | 23.18 | 0.745 | 31x |
| momentum | 0.046 | 0.0005 | 90x |
slope_r2 的跨资产数值差距仅 31 倍,远小于其他因子的 2000~3000 倍,这是其跨市场可比的根本原因。
3.3 standardized_slope(t-statistic)实验
公式:
\text{Score} = \frac{\hat{\beta}}{\text{SE}(\hat{\beta})}, \quad \text{SE}(\hat{\beta}) = \sqrt{\frac{\text{MSE}}{S_{xx}}}
学术动机:t-statistic 同时考虑了斜率大小和估计的统计显著性,理论上比 slope × R² 更严格。
实验结果:
| 指标 | slope_r2 | standardized_slope | Δ |
|---|---|---|---|
| 年化收益 | 19.84% | 13.73% | -6.11% |
| 夏普比率 | 1.14 | 1.01 | -0.12 |
| 最大回撤 | -15.35% | -13.52% | +1.83% |
| Calmar | 1.29 | 1.02 | -0.27 |
| 调仓次数 | 394 | 335 | -59 |
失败原因分析:
- 绝对度量 vs 相对度量:
SE(β)是绝对度量(量纲同斜率),而R²是相对度量(无量纲)。在跨资产比较中,SE 对高波动资产(如 CL=F、HSTECH)惩罚过重,即使趋势方向正确,score 也会被压低。 - 过度过滤:调仓次数减少 59 次,说明 t-statistic 把大量"方向对但波动大"的有效信号过滤掉了,反而错失趋势行情。
- 数学等价性:
slope / SE(slope) = slope × √(Sxx / MSE),而slope × R² = slope × (1 - SS_res/SS_tot)。前者惩罚的是残差方差绝对值,后者惩罚的是偏离趋势线的比例——后者更适合作为趋势质量因子。
结论:t-statistic 不适合本场景,保持 slope_r2 为默认因子。
4. slope_r2 胜出的原因分析
4.1 跨市场可比性:归一化消除量纲
| 设计选择 | weighted 的做法 | slope_r2 的做法 | 为什么 slope_r2 更好 |
|---|---|---|---|
| 价格变换 | \ln(p) |
p/p_0 |
归一化后所有资产同尺度 |
| 回归权重 | 近期加权(1→2) | 无权重 | 25 天窗口已短,等权更抗噪 |
R^2 质量因子 |
✓ | ✓ | 都保留了趋势质量过滤 |
4.2 不加权 > 加权:降噪效应
25 天窗口本身就不长,加权(权重 1→2)相当于把有效窗口缩到约 17 天,增加了随机性。等权回归对 25 天趋势的估计更稳健。
4.3 负价格免疫力
WTI 原油 2020-04-21 出现 -37.63 美元/桶的负价格。各因子处理方式:
| 因子 | 处理方式 | 问题 |
|---|---|---|
| weighted | \ln(\text{clip}(0.01)) = -4.6 |
虚假平台拉偏回归 |
| vol_adjusted | 对数收益率跳跃 | vol 被放大,score 压低 |
| slope_r2 | 0.01/60 \approx 0.000167 |
跌幅压缩但不爆炸 |
| momentum | 25/0.01 - 1 = 2499 |
荒谬收益率,严重误判 |
5. 业界学界方法调研
5.1 Moskowitz, Ooi & Pedersen (2012) — TSMOM
核心公式:
Signal = sign(r_{t-12, t}) // 仅取过去12月超额收益的符号
Position = (1/σ_t) × Signal // 仓位反比于波动率
关键设计:
- 使用期货超额收益(简单收益),不用对数收益
- 只用 sign(正负号)决定方向,不用收益率幅度
- 仓位大小由波动率倒数控制
对负价格的态度:简单收益天然兼容负价格,(−5−60)/60 = −108\% 合法。
5.2 Baltas & Kosowski (2012) — 线性趋势拟合
"Momentum trading signals generated by fitting a linear trend on the asset price path maximise the out-of-sample performance while minimizing the portfolio turnover."
这正是 slope_r2 的思路——直接拟合价格路径而非计算收益率。
结论:线性趋势拟合 > 简单收益率 > 加权收益率。
5.3 Dudler, Gmuer, Malamud (2014) — Risk-Adjusted Momentum
\text{RAMOM} = \text{mean}\left(\frac{r_1}{\sigma_1}, \frac{r_2}{\sigma_2}, \ldots, \frac{r_n}{\sigma_n}\right)
每天先算风险调整收益 $r_t/\sigma_t$,再取均值作为信号。比 TSMOM 整体更优。
5.4 AQR / Man Group / Winton — 实盘 CTA 做法
| 方法 | 做法 | 代表机构 |
|---|---|---|
| 简单收益替代对数 | r = (P_t - P_{t-1}) / P_{t-1} |
AQR, Man AHL |
| 排除/跳过 | 窗口内出现非正价格时信号设为 0 | 多数 CTA |
| 连续合约 | 使用 roll-adjusted futures 价格 | 所有正规 CTA |
| 波动率缩放 | 只用 sign + vol 倒数,不用幅度 | Moskowitz 方案 |
6. 负价格处理机制分析
6.1 当前实现的问题
四个因子均使用 np.clip(prices, 0.01, None) 处理负价格:
prices = np.clip(prices, 0.01, None) # 负值 → 0.01
问题:这是粗暴的掩盖而非正确处理。
| 因子 | clip 后的核心问题 | 严重程度 |
|---|---|---|
| weighted | \log(0.01) = -4.6 形成假平台,拉偏回归 |
中 |
| vol_adjusted | 对数收益率剧烈跳跃,vol 被放大 | 高 |
| slope_r2 | 跌幅被压缩,但不产生数值爆炸 | 低 |
| momentum | 首尾任一被 clip 就产生荒谬比值 | 极高 |
6.2 推荐改进方案
方案 A:排除法(推荐)
窗口内出现非正价格 → 返回 None,该资产不参与排名。
def _compute_momentum(self, signal_code: str, date: pd.Timestamp) -> Optional[float]:
# ... 获取 prices ...
if np.any(prices <= 0):
return None # 负价格期间不参与交易
# ... 计算 score ...
优点:
- 负油价在历史上只出现几天,排除影响极小
- 避免所有 clip 导致的信号扭曲
- 实现成本极低
方案 B:简单收益替代对数
把 weighted_momentum 和 vol_adjusted 的 log(prices) 改为简单收益率。但 slope_r2 已在价格空间操作,无需修改。
方案 C:保持现状
slope_r2 已是当前最优(年化 19.84%),且对负价格抵抗力最强。clip 只在极端情况触发,实际影响有限。
7. 结论与建议
7.1 当前决策
采用 slope_r2 作为默认因子,配置已切换至 config_simple.yaml:
factor:
type: slope_r2
n_days: 25
7.2 理论依据
- 跨市场可比:归一化价格天然消除量纲差异
- 趋势质量:
R^2过滤噪声趋势 - 抗极端值:不使用对数,对负价格免疫力最强
- 学术支持:Baltas & Kosowski (2012) 证实线性趋势拟合优于简单收益率
7.3 后续优化方向
| 方向 | 描述 | 优先级 |
|---|---|---|
| 负价格排除 | 窗口内出现非正价格时返回 None | 低(实际影响极小) |
| 多窗口融合 | 结合 5/25/60 天信号 | 中 |
| 截面 rank | 动量值转截面百分位排名 | 低(slope_r2 已天然可比) |
| slope/SE(slope),已验证不适合(详见 3.3) | 已排除 |
附录:实验代码
对比实验脚本:rotation/experiments/factor_comparison.py、rotation/experiments/std_slope_test.py
运行方式:
cd /Users/aszer/code/etf
set -a && source .env && set +a
python rotation/experiments/factor_comparison.py
python rotation/experiments/std_slope_test.py
结果输出:
rotation/experiments/output/factor_comparison_results.jsonrotation/experiments/output/std_slope_test_results.json