Files
etf/docs/strategy_summaries/strategy_summary_20260606_ca933e4.md
aszerW 7b229ced14 docs: add strategy summary snapshot (2026-06-06, ca933e4)
First stage summary documenting core strategy logic, key design
decisions, and select_num/weight backtest comparison results.
Stored in dedicated docs/strategy_summaries/ directory with
date + commit hash naming for reproducibility.
2026-06-06 23:59:41 +08:00

259 lines
8.6 KiB
Markdown
Raw Permalink 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.

# 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 ×25 天回看窗口。
```
slope_r2_score = 10000 × slope ×
其中:
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-Nselect_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=226.38% vs 22.57%+3.81%
- select_num=323.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=1Calmar 1.641(收益/回撤最优)
- select_num=3 + rankCalmar 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 | 回测主循环:每日迭代 + 权重锁定 |