# ETF 轮动策略阶段总结

> **生成日期**：2026-06-06
> **Git Commit**：`ca933e4`
> **复现方式**：`git checkout ca933e4` 后运行 `rotation/simple_rotation.py`

---

## 1. 策略概述

**定位**：基于动量因子的跨市场 ETF 轮动策略，通过每日截面排名在 11 个全球资产中选择强势标的，利用动量效应获取超额收益。

**资产池**：11 个信号标的，覆盖 A 股、港股、美股、日股、欧洲、商品、债券七个大类。

| 大类 | 信号标的 | 交易 ETF | 说明 |
|:---|:---|:---|:---|
| A 股 | 399006.SZ 创业板指 | 159915.SZ 创业板 ETF | |
| A 股 | H30269.CSI 红利低波 | 512890.SH 红利低波 ETF | |
| 港股 | HSI 恒生指数 | 159920.SZ 恒生 ETF | |
| 港股 | HSTECH.HK 恒生科技 | 513130.SH 恒生科技 ETF | 2020-07 上市 |
| 美股 | NDX 纳指 100 | 513100.SH 纳指 ETF | |
| 日股 | N225 日经 225 | 513520.SH 日经 ETF | |
| 欧洲 | GDAXI 德国 DAX | 513030.SH 德国 ETF | |
| 商品 | GC=F 黄金期货 | 518880.SH 黄金 ETF | |
| 商品 | HG=F 铜期货 | 159980.SZ 有色 ETF | |
| 商品 | CL=F 原油期货 | 160723.SZ 嘉实原油 | |
| 债券 | 931862.CSI 短债指数 | 931862.CSI 短债指数 | 防御配置 |

**回测区间**：2020-01-10 ~ 2026-06-05，共 1549 个交易日。

---

## 2. 核心逻辑流程

### 2.1 每日时间线

```
T 日 09:00  信号生成（使用 T-1 日及之前的收盘价）
     ↓
T 日 09:30  执行调仓（如触发）
     ↓
T 日 15:00  计算当日收益，更新 NAV
```

### 2.2 信号生成

**动量因子**：当前使用 `slope_r2`（slope × R²），25 天回看窗口。

```
slope_r2_score = 10000 × slope × R²

其中：
  slope: 对归一化价格 p/p[0] 做线性回归的斜率
  R²:    回归决定系数（拟合优度）
```

该因子的设计意图：
- **slope** 捕捉趋势方向和强度
- **R²** 衡量趋势质量（高 R² = 趋势稳定，低 R² = 噪声大）
- 二者相乘实现"趋势强度 × 拟合质量"的双重过滤

**其他可用因子**（通过配置切换）：

| 因子 | 公式 | 特点 |
|:---|:---|:---|
| `momentum` | (p[-1] / p[0]) - 1 | 简单收益率 |
| `weighted_momentum` | annualized_return × R² | 加权回归 |
| `slope_r2` | 10000 × slope × R² | 当前默认 |
| `standardized_slope` | slope / SE(slope) | t 统计量 |
| `vol_adjusted_momentum` | (return / vol) × R² | 类夏普构造 |

### 2.3 持仓选择

信号生成采用**大类竞争 + 动态阈值 + 债券填充**三层机制：

1. **大类竞争**：每个 group 内选动量 score 最高的标的作为 winner
2. **动态阈值**：非 BOND 组 winner 的 raw_momentum 必须 >= bond_momentum × ratio（当前 ratio=1.0），否则被过滤
3. **截面排名**：所有通过阈值的 winner 按 score 降序排列，选 top-N（select_num）
4. **债券填充**：若 winner 数量不足 select_num，用债券填充剩余 slot

**债券的双重角色**：
- **阈值过滤器**：债券动量作为其他标的入选的动态门槛
- **仓位填充**：市场弱势时（多数标的动量低于债券），作为防御性持仓

### 2.4 调仓判定

```
is_rebalance = (sorted(new_holdings) != sorted(current_holdings)) and len(current_holdings) > 0
```

当新选出的持仓与当前持仓不同时触发调仓。调仓时扣除交易成本（默认 0.1%）。

### 2.5 仓位加权

支持两种模式，仅在调仓日更新权重（权重锁定机制）：

**equal（等权）**：
```
weight_i = 1/N
```

**rank（排名加权）**：
```
weight_i = (N - i) / triangular(N)

其中 triangular(N) = N × (N + 1) / 2
```

以 select_num=3 为例：第 1 名 50%，第 2 名 33%，第 3 名 17%。

**权重锁定机制**：

```
_generate_signals()  →  计算 _pending_weights（每天，仅用于信号选择）
         ↓
run() 主循环         →  is_rebalance?
                       ├─ 是: active_weights = _pending_weights  （锁定新权重）
                       └─ 否: 保持 active_weights 不变           （权重不变）
```

这确保了持仓不变时，仓位权重不会因排名顺序变化而波动。

### 2.6 收益计算

| 场景 | 计算方式 |
|:---|:---|
| 调仓日 - 卖出 | (open - prev_close) / prev_close × weight |
| 调仓日 - 买入 | (close - open) / open × weight |
| 调仓日 - 调仓成本 | -0.1% |
| 非调仓日 - 持有 | (close - prev_close) / prev_close × weight |

---

## 3. 关键设计决策

| 决策 | 选择 | 原因 |
|:---|:---|:---|
| 动量因子 | slope × R² | slope 捕捉趋势方向，R² 过滤噪声趋势，双重过滤提升信噪比 |
| 回看窗口 | 25 天 | 中期动量（约 1 个月），平衡响应速度与噪声过滤 |
| 债券阈值 | 使用配置的因子函数 | 保持阈值两边量纲一致，仅在 VOL_ADJUSTED_MOMENTUM 时 fallback |
| 权重锁定 | 仅调仓日更新 active_weights | 避免持仓不变时权重随排名波动，减少无效换手 |
| 大类竞争 | 每组选 top 1 | 天然实现大类分散，避免同一大类集中持仓 |
| 交易成本 | 0.1% / 次 | 模拟真实 ETF 交易摩擦成本 |
| 信号时间 | T-1 日收盘价 | 模拟 T 日 9:00 前可获取的信息，避免未来数据 |

---

## 4. 对比实验结果

### 4.1 实验矩阵

| 维度 | 取值 |
|:---|:---|
| select_num | 1, 2, 3 |
| weight | equal, rank |
| 其他参数 | 与默认配置一致 |

共 6 组配置（select_num=1 时 equal/rank 等价，实际 5 种独立结果）。

### 4.2 结果汇总

| select_num | weight | 年化收益 | 夏普比率 | 最大回撤 | Calmar |
|:---:|:---:|:---:|:---:|:---:|:---:|
| 1 | equal/rank | **43.20%** | 1.246 | -26.33% | **1.641** |
| 2 | equal | 22.57% | 1.082 | -18.34% | 1.230 |
| 2 | rank | 26.38% | 1.117 | -19.09% | 1.382 |
| 3 | equal | 20.39% | **1.160** | **-14.65%** | 1.392 |
| 3 | rank | 23.52% | 1.150 | -16.27% | 1.446 |

### 4.3 关键发现

**1. rank 加权系统性提升年化收益**

在 select_num >= 2 时，rank 加权相比 equal 加权：
- select_num=2：26.38% vs 22.57%（+3.81%）
- select_num=3：23.52% vs 20.39%（+3.13%）

原因：rank 加权将更多仓位集中在排名最高的标的上，放大了动量最强的资产的收益贡献。

**2. select_num=1 是收益天花板**

单标的集中持仓实现了 43.20% 的年化收益，但代价是 -26.33% 的最大回撤。select_num=1 时 equal/rank 等价，因为只选 1 个标的。

**3. equal 加权夏普比率略高**

| select_num | equal 夏普 | rank 夏普 | 差值 |
|:---:|:---:|:---:|:---:|
| 2 | 1.082 | 1.117 | rank +0.035 |
| 3 | **1.160** | 1.150 | equal +0.010 |

select_num=3 + equal 的夏普比率最高（1.160），同时回撤最小（-14.65%），体现了分散化的风险调整优势。

**4. Calmar 比率视角**

- select_num=1：Calmar 1.641（收益/回撤最优）
- select_num=3 + rank：Calmar 1.446（多标的中最优）

**5. 回撤控制**

select_num 越大，回撤越小：
- 1 标的：-26.33%
- 2 标的：-18% ~ -19%
- 3 标的：-14% ~ -16%

分散化有效降低了极端回撤。

---

## 5. 当前默认配置

```yaml
rotation:
  diversified: true       # 大类竞争（每组选 top 1）
  select_num: 3           # 持仓数量
  weight: rank            # 排名加权（50%/33%/17%）
  threshold:
    mode: dynamic
    dynamic:
      reference: 931862.CSI   # 债券作为动态阈值参考
      ratio: 1.0              # 阈值 = bond_momentum × 1.0
      fallback_enabled: true
      fallback_value: 0.0

factor:
  type: slope_r2          # 动量因子类型
  n_days: 25              # 回看窗口

rebalance:
  min_hold_days: 1
  trade_cost: 0.001       # 0.1% 交易成本

backtest:
  start_date: '2020-01-10'

benchmark:
  code: 000300.SH         # 沪深300作为基准
```

---

## 6. 代码文件索引

| 文件 | 职责 |
|:---|:---|
| `rotation/simple_rotation.py` | 核心策略引擎：动量因子、信号生成、回测主循环、收益计算 |
| `rotation/config_loader.py` | 配置加载：Pydantic Schema、枚举类型、YAML 解析 |
| `rotation/config_simple.yaml` | 策略配置文件：资产池、因子、轮动、回测参数 |
| `rotation/backtest_viewer.html` | 回测可视化：NAV 曲线、持仓明细、交易记录 |

### 核心函数速查

| 函数 | 位置 | 说明 |
|:---|:---|:---|
| `slope_r2_score()` | simple_rotation.py | 默认动量因子：10000 × slope × R² |
| `compute_position_weights()` | simple_rotation.py | 仓位加权：equal / rank 两种模式 |
| `_generate_signals()` | simple_rotation.py | 信号生成：大类竞争 + 阈值过滤 + 债券填充 |
| `_calculate_daily_return()` | simple_rotation.py | 收益计算：调仓/持有两种场景 |
| `run()` | simple_rotation.py | 回测主循环：每日迭代 + 权重锁定 |
