# 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`*