核心内容: - V2 vs V3核心差异对比表 - 动态阈值机制详解(三层守卫) - 短债双重角色:阈值参考 + 空余仓位填充 - 三步筛选流程:动态阈值 → 类内竞争 → 跨类排序+填充 - 具体示例:正常/危机/极端危机三种场景 - V3关键修复:重复代码计分 + 数据守卫 + 空信号状态保持 - 三次修复历程:74a664d →be8ca02→18bc9b8- V3配置示例与回滚方案 回测绩效: - CAGR: 27.81% - 最大回撤: -24.36% - 夏普: 1.40 - Calmar: 1.14
415 lines
14 KiB
Markdown
415 lines
14 KiB
Markdown
# ETF轮动策略核心逻辑 V3
|
||
|
||
## 📊 策略概览
|
||
|
||
基于**加权动量因子**的跨市场ETF轮动策略,通过量化评分自动选择表现最优的ETF组合进行投资。V3版本核心升级:**动态阈值机制**,以短债动量作为过滤阈值,实现自适应风控——动量低于短债的标的自动排除,空余仓位用短债填充,构建"永不空仓"的防御体系。
|
||
|
||
---
|
||
|
||
## 🔄 V2 vs V3 核心差异
|
||
|
||
| 维度 | V2(固定阈值) | V3(动态阈值) |
|
||
|---|---|---|
|
||
| **阈值来源** | `min_score=0.0` 固定值 | 短债动量 × ratio(每日动态) |
|
||
| **阈值计算** | 常量 0 | `scores['931862.CSI'] × 1.0` |
|
||
| **BOND角色** | 普通候选,参与Top3竞争 | 双重角色:阈值参考 + 空余仓位填充 |
|
||
| **不足3只时** | 返回少于3只(仓位不满) | 用短债填充至3只(满仓保障) |
|
||
| **空信号处理** | 清仓(仓位归零) | 保持旧持仓(状态保持) |
|
||
| **信号格式** | `"NDX,GC=F"` (2只) | `"NDX,931862.CSI,931862.CSI"` (3只,含填充) |
|
||
| **仓位效果** | 2只各50% | NDX 33%, 短债 67%(等权分配) |
|
||
| **防御机制** | 无(纯动量选股) | 短债作为"现金底仓"自动填充 |
|
||
|
||
---
|
||
|
||
## 🎯 候选池配置
|
||
|
||
### 覆盖市场(7大类,11只标的)
|
||
|
||
| 大类 (market) | 标的 | 信号源代码 | 交易ETF | 数据来源 |
|
||
|---|---|---|---|---|
|
||
| **A股 (A)** | 创业板指 | 399006.SZ | 159915.SZ | Tushare |
|
||
| **A股 (A)** | 中证红利低波 | H30269.CSI | 512890.SH | Tushare |
|
||
| **美股 (US)** | 纳指100 | NDX | 513100.SH | YFinance |
|
||
| **日本 (JP)** | 日经225 | N225 | 513520.SH | YFinance |
|
||
| **欧洲 (EU)** | 德国DAX | GDAXI | 513030.SH | YFinance |
|
||
| **港股 (HK)** | 恒生指数 | HSI | 159920.SZ | YFinance |
|
||
| **港股 (HK)** | 恒生科技 | HSTECH.HK | 513130.SH | YFinance |
|
||
| **商品 (COMMODITY)** | 黄金 | AU.SHF | 518880.SH | Tushare/期货 |
|
||
| **商品 (COMMODITY)** | 原油 | CL.NYM | 160723.SZ | YFinance |
|
||
| **商品 (COMMODITY)** | 有色金属(铜) | CU.SHF | 159980.SZ | Tushare/期货 |
|
||
| **固收 (BOND)** | 短债指数 | 931862.CSI | 511090.SH | Tushare |
|
||
|
||
### ⚠️ V3特殊说明:BOND大类角色转变
|
||
|
||
在V3中,短债指数(931862.CSI)不再是普通候选标的,而是承担双重角色:
|
||
|
||
1. **角色A:动态阈值参考**
|
||
- 其动量得分决定当日过滤阈值
|
||
- 其他标的得分 < 短债动量 → 自动排除
|
||
|
||
2. **角色B:空余仓位填充物**
|
||
- 选出不足3只时,用短债代码填充剩余仓位
|
||
- 确保信号永远为3只标的(满仓保障)
|
||
|
||
**关键:BOND大类不参与跨大类竞争**,不会与其他大类冠军一起排名选Top3。
|
||
|
||
### 数据范围说明
|
||
|
||
- **短债指数数据**:2007-12-31 起(Tushare限制)
|
||
- **2002-2007年策略行为**:退化至V2模式(无动态阈值,无短债填充)
|
||
|
||
---
|
||
|
||
## 🧮 得分计算逻辑(与V2相同)
|
||
|
||
### 因子类型:加权线性回归动量 (weighted_momentum)
|
||
|
||
#### 核心公式
|
||
```
|
||
得分 = 年化收益率 × R²
|
||
```
|
||
|
||
#### 计算步骤
|
||
1. **取对数收益**:`y = ln(价格序列)`(过去25个交易日)
|
||
2. **构造权重**:`weights = linspace(1, 2, 25)`(近期权重2.0,远期权重1.0,线性递增)
|
||
3. **加权线性回归**:`slope, intercept = polyfit(x, y, 1, w=weights)`
|
||
4. **年化收益率**:`annualized_returns = exp(slope × 250) - 1`
|
||
5. **加权R²**:
|
||
```
|
||
y_pred = slope × x + intercept
|
||
ss_res = Σ(weights × (y - y_pred)²)
|
||
ss_tot = Σ(weights × (y - weighted_mean(y))²)
|
||
R² = 1 - ss_res / ss_tot
|
||
```
|
||
6. **最终得分**:`score = annualized_returns × R²`
|
||
|
||
#### 得分含义
|
||
- **正值**:上升趋势,值越大代表趋势越强且越稳定
|
||
- **负值**:下降趋势(会被过滤,不参与选股)
|
||
- **接近0**:无明显趋势或趋势不稳定
|
||
|
||
### 崩盘过滤机制
|
||
在得分计算后,对最近3个交易日进行崩盘检测:
|
||
- **条件1**:最近3天中任一天跌幅 > 5%
|
||
- **条件2**:连续3天下跌,且累计跌幅 > 5%
|
||
|
||
满足任一条件 → **得分强制清零**,该标的当日不参与选股。
|
||
|
||
---
|
||
|
||
## 📦 选股逻辑:V3动态阈值分组选股
|
||
|
||
### 核心机制:三步筛选
|
||
|
||
#### 第一步:动态阈值过滤
|
||
|
||
```python
|
||
threshold = 短债动量得分 × ratio # ratio默认1.0
|
||
scores = {k: v for k, v in scores.items() if v >= threshold}
|
||
```
|
||
|
||
**阈值退化机制**:
|
||
- 短债无数据(2002-2007) → `threshold = min_score = 0.0`
|
||
- 短债动量 < 0 → `threshold = min_score = 0.0`
|
||
- 短债动量 ≥ 0 → `threshold = 短债动量 × 1.0`
|
||
|
||
#### 第二步:类内竞争(BOND不参与)
|
||
|
||
每个 `market` 大类只保留得分最高的1只标的(大类冠军),**但BOND大类被排除**:
|
||
|
||
```
|
||
A股: 创业板指 vs 红利低波 → 选1只冠军
|
||
港股: 恒生指数 vs 恒生科技 → 选1只冠军
|
||
商品: 黄金 vs 原油 vs 有色金属 → 选1只冠军
|
||
美股/日本/欧洲: 各1只,自动成为冠军
|
||
固收(BOND): ⚠️ 不参与竞争(作为阈值+填充)
|
||
```
|
||
|
||
#### 第三步:跨类排序 + 短债填充
|
||
|
||
从最多6个大类冠军中(排除BOND),按得分从高到低选 Top 3:
|
||
|
||
```
|
||
选出标的 = Top 3 大类冠军
|
||
空余仓位 = 3 - len(选出标的)
|
||
|
||
if 空余仓位 > 0 AND 短债有数据:
|
||
用短债代码填充空余仓位(可能重复)
|
||
```
|
||
|
||
### 具体示例
|
||
|
||
#### 示例1:正常行情(选出3只风险资产)
|
||
|
||
某日各标的得分:
|
||
|
||
| 大类 | 标的 | 得分 | 动态阈值过滤 | 类内排名 |
|
||
|---|---|---|---|---|
|
||
| BOND | 短债指数 | 0.05 | —(阈值参考) | ⚠️ 不参与 |
|
||
| A | 创业板指 | 2.5 | ✓ > 0.05 | ✓ 冠军 |
|
||
| US | 纳指100 | 4.7 | ✓ > 0.05 | ✓ 冠军 |
|
||
| JP | 日经225 | 3.5 | ✓ > 0.05 | ✓ 冠军 |
|
||
| COMMODITY | 黄金 | 3.1 | ✓ > 0.05 | ✓ 冠军 |
|
||
|
||
- 动态阈值 = 0.05(短债得分)
|
||
- 过滤后冠军:纳指100(4.7) > 日经225(3.5) > 黄金(3.1) > 创业板指(2.5)
|
||
- 选 Top 3:**纳指100 + 日经225 + 黄金**(各33%)
|
||
- 空余仓位 = 0 → 无需填充
|
||
- 信号:`"NDX,N225,GC=F"`
|
||
|
||
#### 示例2:危机行情(仅选出1只,短债填充2份)
|
||
|
||
| 大类 | 标的 | 得分 | 动态阈值过滤 | 类内排名 |
|
||
|---|---|---|---|---|
|
||
| BOND | 短债指数 | 0.02 | —(阈值参考) | ⚠️ 不参与 |
|
||
| A | 创业板指 | -0.5 | ✗ < 0.02 | 过滤 |
|
||
| US | 纳指100 | 0.03 | ✓ > 0.02 | ✓ 冠军 |
|
||
| JP | 日经225 | 0.01 | ✗ < 0.02 | 过滤 |
|
||
| COMMODITY | 黄金 | -0.2 | ✗ < 0.02 | 过滤 |
|
||
|
||
- 动态阈值 = 0.02(短债得分)
|
||
- 过滤后冠军:纳指100(0.03)
|
||
- 选出1只:**纳指100**
|
||
- 空余仓位 = 2 → 填充2份短债
|
||
- 信号:`"NDX,931862.CSI,931862.CSI"`
|
||
- 仓位:纳指100 33%,短债 67%(防御主导)
|
||
|
||
#### 示例3:极端危机(选出0只,短债填充3份)
|
||
|
||
| 大类 | 标的 | 得分 | 动态阈值过滤 |
|
||
|---|---|---|---|
|
||
| BOND | 短债指数 | 0.01 | —(阈值参考) |
|
||
| A | 创业板指 | -0.5 | ✗ < 0.01 |
|
||
| US | 纳指100 | 0.005 | ✗ < 0.01 |
|
||
| JP | 日经225 | -0.3 | ✗ < 0.01 |
|
||
|
||
- 动态阈值 = 0.01
|
||
- 过滤后冠军:无(全部低于阈值)
|
||
- 选出0只
|
||
- 空余仓位 = 3 → 填充3份短债
|
||
- 信号:`"931862.CSI,931862.CSI,931862.CSI"`
|
||
- 仓位:100% 短债(全防御模式)
|
||
|
||
---
|
||
|
||
## ⏱️ 调仓控制(V3增强)
|
||
|
||
### 调仓触发条件
|
||
|
||
每日检查,同时满足以下条件才触发调仓:
|
||
|
||
1. **最低持仓期**:距上次调仓 ≥ 1个交易日(`rebalance_days = 1`)
|
||
2. **得分改善检查**:
|
||
```python
|
||
# V3修复:按实际份数计算得分(支持重复代码)
|
||
old_total = sum(row.get(c, 0) for c in old_codes) # 遍历持仓列表
|
||
new_total = sum(row.get(c, 0) for c in new_codes) # 含重复短债
|
||
|
||
if old_total > 0:
|
||
return (new_total / old_total - 1) >= rebalance_threshold
|
||
```
|
||
3. **目标信号处理**:
|
||
- V2:target为空 → 清仓
|
||
- V3:target为空 → **保持旧持仓**(状态保持)
|
||
|
||
### V3关键修复:重复代码计分
|
||
|
||
当信号中出现 `"NDX,931862.CSI,931862.CSI"` 时:
|
||
- **错误(V2逻辑)**:短债只计一次得分
|
||
- **正确(V3修复)**:短债计两次得分(按实际份数)
|
||
|
||
```python
|
||
# 错误(遍历factor_cols做in判断)
|
||
new_total = sum(row[col] for col in factor_cols if col in new_codes)
|
||
# 短债出现2次,但只匹配1次 → 得分被低估
|
||
|
||
# 正确(直接遍历持仓列表)
|
||
new_total = sum(row[c] for c in new_codes)
|
||
# 短债出现2次 → 得分正确累加
|
||
```
|
||
|
||
### T+1执行机制
|
||
- T日收盘后计算因子得分,生成信号(`signal_raw`)
|
||
- 信号向后移位1天(`shift(1)`)作为T+1日的执行信号
|
||
- **运行时间**:T+1日上午9:00(北京时间),因美股/期货数据凌晨才可用
|
||
|
||
### 交易成本
|
||
- **成本率**:0.1%(双边,含佣金+滑点)
|
||
- **扣除方式**:按换手率比例扣除
|
||
- `换手率 = 新增品种数 / 旧持仓品种数`
|
||
- `成本 = 换手率 × 0.1%`
|
||
|
||
---
|
||
|
||
## 🛡️ V3防御机制详解
|
||
|
||
### 动态阈值的三层守卫
|
||
|
||
```python
|
||
def _get_dynamic_threshold(scores):
|
||
if not bond_threshold.enabled:
|
||
return min_score # 层1:V2退化
|
||
|
||
bond_score = scores.get('931862.CSI', None)
|
||
|
||
if bond_score is None or bond_score < 0:
|
||
return min_score # 层2:无数据或负动量
|
||
|
||
return bond_score × ratio # 层3:动态阈值
|
||
```
|
||
|
||
### 短债填充的数据守卫
|
||
|
||
```python
|
||
def _grouped_selection(scores, bond_has_data):
|
||
# 类内竞争(排除BOND)
|
||
...
|
||
|
||
# 空余仓位填充
|
||
if fill_bond AND bond_code AND bond_has_data:
|
||
# 只在短债有数据时填充
|
||
# 2002-2007年无数据 → 不填充
|
||
for _ in range(n_bond_slots):
|
||
selected.append(bond_code)
|
||
```
|
||
|
||
### 空信号状态保持
|
||
|
||
```python
|
||
def _apply_rebalance_control(daily_target):
|
||
for target in daily_target:
|
||
if not target:
|
||
# V3:保持当前持仓不变
|
||
# V2:清仓(current_held = '')
|
||
pass # 不更新current_held
|
||
```
|
||
|
||
**为什么V3这样做?**
|
||
- 有短债数据时,target永远不会为空(会被填充)
|
||
- target为空仅发生在2002-2007无短债数据期
|
||
- 保持旧持仓 = 用最近有效信号继续执行,比突然清仓更平滑
|
||
|
||
---
|
||
|
||
## 📈 回测绩效(2002-01 ~ 2026-05)
|
||
|
||
### V2 vs V3 全周期对比
|
||
|
||
| 指标 | V2(固定阈值) | V3(动态阈值) | 差异 |
|
||
|---|---|---|---|
|
||
| **累计收益** | — | 39,380% | — |
|
||
| **CAGR** | — | 27.81% | — |
|
||
| **夏普比率** | — | 1.40 | — |
|
||
| **最大回撤** | — | -24.36% | — |
|
||
| **Calmar** | — | 1.14 | — |
|
||
| **最大回撤区间** | — | 2015-05~2016-06 | — |
|
||
|
||
### V3持仓特征分析
|
||
|
||
| 持仓状态 | 占比 | 说明 |
|
||
|---|---|---|
|
||
| 3只风险资产 | 71.6% | 正常行情,短债不参与 |
|
||
| 1只风险+2只短债 | 12.2% | 部分防御,短债填充2份 |
|
||
| 2只风险+1只短债 | 12.0% | 轻度防御,短债填充1份 |
|
||
| 3只短债 | 4.5% | 全防御模式 |
|
||
| 不足3只 | <1% | 仅2002-2007无数据期 |
|
||
|
||
---
|
||
|
||
## 📋 策略特点总结
|
||
|
||
### V1 → V2 → V3演进对比
|
||
|
||
| 维度 | V1配置 | V2配置 | V3配置 |
|
||
|---|---|---|---|
|
||
| **候选池** | 22只(A股为主) | 11只(全球7大类) | 11只(BOND角色转变) |
|
||
| **因子类型** | slope_r2 | weighted_momentum | weighted_momentum |
|
||
| **崩盘过滤** | 无 | 3天跌>5%清零 | 3天跌>5%清零 |
|
||
| **持仓数量** | 5只(等权20%) | 3只(等权33%) | 3只(含短债填充) |
|
||
| **选股模式** | 纯Top N | 跨大类分散化 | 动态阈值+短债填充 |
|
||
| **阈值类型** | 固定min_score=0 | 固定min_score=0 | **动态阈值(短债动量)** |
|
||
| **空信号处理** | 清仓 | 清仓 | **保持旧持仓** |
|
||
| **防御机制** | 无 | 无 | **短债自动填充** |
|
||
| **数据源** | Tushare+YFinance+CCXT | Tushare+YFinance | Tushare+YFinance |
|
||
| **基准指数** | 沪深300 | 沪深300 | 沨深300 |
|
||
|
||
---
|
||
|
||
## 💡 V3核心优势
|
||
|
||
1. **自适应风控**:短债动量作为动态阈值,危机时刻自动收紧过滤标准
|
||
2. **永不空仓**:空余仓位用短债填充,避免"持币观望"的机会成本
|
||
3. **平滑过渡**:空信号保持旧持仓,避免突然清仓的冲击
|
||
4. **满仓保障**:信号永远为3只标的,仓位分配更稳定
|
||
5. **防御主导**:危机时刻短债占比可达67%~100%,有效控制回撤
|
||
6. **数据退化**:2002-2007无短债数据时正确退化为V2行为
|
||
|
||
---
|
||
|
||
## 📝 V3配置示例
|
||
|
||
```yaml
|
||
# strategies/rotation/config.yaml
|
||
|
||
# ==================== 轮动参数 ====================
|
||
select_num: 3
|
||
diversified: true
|
||
|
||
# V3: 动态阈值配置(替代固定 min_score: 0.0)
|
||
bond_threshold:
|
||
enabled: true # true=V3动态阈值, false=退化为V2固定阈值
|
||
bond_code: "931862.CSI" # 阈值参考标的(短债指数)
|
||
ratio: 1.0 # 阈值 = 短债动量 × ratio
|
||
fill_bond: true # 选出不足select_num只时,用短债填充空余仓位
|
||
|
||
# 保留 min_score 作为 fallback
|
||
min_score: 0.0
|
||
|
||
# ==================== 调仓控制 ====================
|
||
rebalance_days: 1
|
||
rebalance_threshold: 0.0
|
||
trade_cost: 0.001
|
||
```
|
||
|
||
---
|
||
|
||
## 🔄 V3回滚方案
|
||
|
||
如需回退至V2行为,只需修改一行配置:
|
||
|
||
```yaml
|
||
bond_threshold:
|
||
enabled: false # 关闭动态阈值
|
||
```
|
||
|
||
代码自动退化:
|
||
- `_get_dynamic_threshold` 返回 `min_score = 0.0`
|
||
- `_grouped_selection` 中BOND排除逻辑不生效
|
||
- `_apply_rebalance_control` 空信号仍保持旧持仓(可额外修改)
|
||
|
||
---
|
||
|
||
## 🐛 V3修复历程
|
||
|
||
### 三次关键修复
|
||
|
||
| 版本 | Commit | 修复内容 | 效果 |
|
||
|---|---|---|---|
|
||
| V3落地 | `74a664d` | 初始实现动态阈值 | 净值292,回撤-27% |
|
||
| 首次修复 | `be8ca02` | 根因1+根因2(部分) | 净值334,回撤**-32%** |
|
||
| 最终修复 | `18bc9b8` | 根因2子问题+根因3 | 净值395,回撤**-24%** |
|
||
|
||
### 三个根因详解
|
||
|
||
| 根因 | 问题 | 修复 | 影响 |
|
||
|---|---|---|---|
|
||
| **根因1** | `_check_rebalance`重复代码只计一次得分 | 直接遍历持仓列表 | 278天应防御未切 |
|
||
| **根因2子问题** | bond无数据(2002-2007)时错误填充 | 添加`bond_has_data`守卫 | 2002年异常回撤 |
|
||
| **根因3** | 空target时清仓导致错过反弹 | 保持旧持仓(`pass`) | 2002年回撤加深 |
|
||
|
||
---
|
||
|
||
*文档版本:V3.0*
|
||
*更新时间:2026-05-19*
|
||
*对应配置:strategies/rotation/config.yaml*
|
||
*关键Commit:`18bc9b8`* |