diff --git a/docs/轮动策略核心逻辑_v3.md b/docs/轮动策略核心逻辑_v3.md new file mode 100644 index 0000000..b5f6e13 --- /dev/null +++ b/docs/轮动策略核心逻辑_v3.md @@ -0,0 +1,415 @@ +# 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`* \ No newline at end of file