Compare commits
2 Commits
49b623931b
...
6e7087a543
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e7087a543 | |||
| 8c3ae2269a |
470
docs/experiments/007_momentum_window_optimization.md
Normal file
470
docs/experiments/007_momentum_window_optimization.md
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
# 实验 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 信息离散动量融合
|
||||||
|
|
||||||
|
#### 方法
|
||||||
|
|
||||||
|
**IDM(Information 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
|
||||||
|
**实验状态**:已完成
|
||||||
@@ -35,6 +35,8 @@ class FactorType(str, Enum):
|
|||||||
WEIGHTED_MOMENTUM = "weighted_momentum"
|
WEIGHTED_MOMENTUM = "weighted_momentum"
|
||||||
VOL_ADJUSTED_MOMENTUM = "vol_adjusted_momentum"
|
VOL_ADJUSTED_MOMENTUM = "vol_adjusted_momentum"
|
||||||
STANDARDIZED_SLOPE = "standardized_slope"
|
STANDARDIZED_SLOPE = "standardized_slope"
|
||||||
|
SLOPE_R2_IDM = "slope_r2_idm" # slope_r2 * IDM (信息离散动量融合)
|
||||||
|
SLOPE_R2_ENSEMBLE = "slope_r2_ensemble" # 多窗口融合 (63/126/252天)
|
||||||
|
|
||||||
|
|
||||||
class PremiumMode(str, Enum):
|
class PremiumMode(str, Enum):
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
@@ -123,6 +123,59 @@ def slope_r2_score(prices: np.ndarray) -> float:
|
|||||||
return 10000 * slope * r2
|
return 10000 * slope * r2
|
||||||
|
|
||||||
|
|
||||||
|
def info_dispersal_momentum(prices: np.ndarray) -> float:
|
||||||
|
"""Information Dispersal Momentum (IDM): 正收益天数占比
|
||||||
|
|
||||||
|
衡量上涨的持续性和广度,过滤靠少数极端日撑起来的假动量。
|
||||||
|
范围 [0, 1],值越高表示上涨天数越多。
|
||||||
|
"""
|
||||||
|
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: 趋势强度 × 拟合质量 × 上涨持续性
|
||||||
|
|
||||||
|
将 slope_r2 与信息离散动量 (IDM) 通过乘法融合:
|
||||||
|
- slope_r2 捕捉趋势方向和稳定性
|
||||||
|
- IDM 作为折扣系数,惩罚靠少数大涨日撑起来的动量
|
||||||
|
"""
|
||||||
|
sr2 = slope_r2_score(prices)
|
||||||
|
idm = info_dispersal_momentum(prices)
|
||||||
|
return sr2 * idm
|
||||||
|
|
||||||
|
|
||||||
|
def slope_r2_ensemble_score(prices: np.ndarray, windows: list = None) -> float:
|
||||||
|
"""多窗口 slope_r2 等权融合
|
||||||
|
|
||||||
|
使用多个时间窗口(默认 63/126/252 天)计算 slope_r2,取等权平均。
|
||||||
|
金融语义:捕捉不同周期的趋势信号,降低单窗口风险。
|
||||||
|
|
||||||
|
对 slope_r2 偏好的影响:
|
||||||
|
- 削弱对"高波动+间歇性趋势"资产的偏好
|
||||||
|
- 增强对"低波动+持续性趋势"资产的识别
|
||||||
|
- 让因子更偏向"持续性趋势"而非"爆发性趋势"
|
||||||
|
"""
|
||||||
|
if windows is None:
|
||||||
|
windows = [63, 126, 252] # 3月、6月、12月
|
||||||
|
|
||||||
|
scores = []
|
||||||
|
for w in windows:
|
||||||
|
if len(prices) >= w:
|
||||||
|
# 取最后 w 天的价格计算 slope_r2
|
||||||
|
window_prices = prices[-w:]
|
||||||
|
score = slope_r2_score(window_prices)
|
||||||
|
scores.append(score)
|
||||||
|
|
||||||
|
if not scores:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return sum(scores) / len(scores)
|
||||||
|
|
||||||
|
|
||||||
def standardized_slope_score(prices: np.ndarray) -> float:
|
def standardized_slope_score(prices: np.ndarray) -> float:
|
||||||
"""Standardized slope (t-statistic): slope / SE(slope)
|
"""Standardized slope (t-statistic): slope / SE(slope)
|
||||||
|
|
||||||
@@ -459,7 +512,9 @@ class SimpleRotationStrategy:
|
|||||||
"""Preload all historical data"""
|
"""Preload all historical data"""
|
||||||
start_date = self.config.backtest.start_date
|
start_date = self.config.backtest.start_date
|
||||||
end_date = self.config.backtest.end_date or datetime.now().strftime('%Y-%m-%d')
|
end_date = self.config.backtest.end_date or datetime.now().strftime('%Y-%m-%d')
|
||||||
preload_start = (pd.Timestamp(start_date) - timedelta(days=self.n_days * 2)).strftime('%Y-%m-%d')
|
# ensemble 因子需要更长的历史(252天窗口,预加载 2 倍)
|
||||||
|
preload_days = 504 if self.config.factor.type == FactorType.SLOPE_R2_ENSEMBLE else self.n_days * 2
|
||||||
|
preload_start = (pd.Timestamp(start_date) - timedelta(days=preload_days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
print("\n[1/4] Preloading signal sources (index raw)...")
|
print("\n[1/4] Preloading signal sources (index raw)...")
|
||||||
for code in self.signal_codes:
|
for code in self.signal_codes:
|
||||||
@@ -498,13 +553,17 @@ class SimpleRotationStrategy:
|
|||||||
df = self.index_data[signal_code]
|
df = self.index_data[signal_code]
|
||||||
mask = df.index <= date
|
mask = df.index <= date
|
||||||
recent = df.loc[mask]
|
recent = df.loc[mask]
|
||||||
if len(recent) < self.n_days:
|
|
||||||
|
# ensemble 因子需要更长的窗口(252天)
|
||||||
|
required_days = 252 if self.config.factor.type == FactorType.SLOPE_R2_ENSEMBLE else self.n_days
|
||||||
|
|
||||||
|
if len(recent) < required_days:
|
||||||
return None
|
return None
|
||||||
prices = recent['close'].values[-self.n_days:]
|
prices = recent['close'].values[-required_days:]
|
||||||
|
|
||||||
base_momentum = self._compute_base_momentum(prices)
|
base_momentum = self._compute_base_momentum(prices)
|
||||||
|
|
||||||
if is_crash(prices):
|
if is_crash(prices[-self.n_days:]): # crash filter 仍然用 n_days 窗口
|
||||||
return 0.0
|
return 0.0
|
||||||
return base_momentum
|
return base_momentum
|
||||||
|
|
||||||
@@ -517,17 +576,21 @@ class SimpleRotationStrategy:
|
|||||||
df = self.index_data[signal_code]
|
df = self.index_data[signal_code]
|
||||||
mask = df.index <= date
|
mask = df.index <= date
|
||||||
recent = df.loc[mask]
|
recent = df.loc[mask]
|
||||||
if len(recent) < self.n_days:
|
|
||||||
|
# ensemble 因子需要更长的窗口(252天)
|
||||||
|
required_days = 252 if self.config.factor.type == FactorType.SLOPE_R2_ENSEMBLE else self.n_days
|
||||||
|
|
||||||
|
if len(recent) < required_days:
|
||||||
return None
|
return None
|
||||||
prices = recent['close'].values[-self.n_days:]
|
prices = recent['close'].values[-required_days:]
|
||||||
|
|
||||||
if self.config.factor.type == FactorType.VOL_ADJUSTED_MOMENTUM:
|
if self.config.factor.type == FactorType.VOL_ADJUSTED_MOMENTUM:
|
||||||
base_momentum = weighted_momentum_score(prices)
|
base_momentum = weighted_momentum_score(prices[-self.n_days:])
|
||||||
else:
|
else:
|
||||||
# Use the same score function as ranking
|
# Use the same score function as ranking
|
||||||
base_momentum = self._compute_base_momentum(prices)
|
base_momentum = self._compute_base_momentum(prices)
|
||||||
|
|
||||||
if is_crash(prices):
|
if is_crash(prices[-self.n_days:]):
|
||||||
return 0.0
|
return 0.0
|
||||||
return base_momentum
|
return base_momentum
|
||||||
|
|
||||||
@@ -538,6 +601,10 @@ class SimpleRotationStrategy:
|
|||||||
return vol_adjusted_momentum_score(prices)
|
return vol_adjusted_momentum_score(prices)
|
||||||
elif ft == FactorType.SLOPE_R2:
|
elif ft == FactorType.SLOPE_R2:
|
||||||
return slope_r2_score(prices)
|
return slope_r2_score(prices)
|
||||||
|
elif ft == FactorType.SLOPE_R2_IDM:
|
||||||
|
return slope_r2_idm_score(prices)
|
||||||
|
elif ft == FactorType.SLOPE_R2_ENSEMBLE:
|
||||||
|
return slope_r2_ensemble_score(prices)
|
||||||
elif ft == FactorType.STANDARDIZED_SLOPE:
|
elif ft == FactorType.STANDARDIZED_SLOPE:
|
||||||
return standardized_slope_score(prices)
|
return standardized_slope_score(prices)
|
||||||
elif ft == FactorType.MOMENTUM:
|
elif ft == FactorType.MOMENTUM:
|
||||||
|
|||||||
Reference in New Issue
Block a user