Compare commits

...

5 Commits

Author SHA1 Message Date
c8e30dcbdf docs: 轮动策略回测分析报告(2000-2026)
内容:
- 年度收益汇总(26年完整数据)
- 月度收益详细表格(317个月)
- 4次重大回撤分析(互联网泡沫、金融危机、A股调整、新冠疫情)
- 策略特点总结与改进建议
2026-05-16 01:28:29 +08:00
6ccb121764 fix(strategy): 修复收益率计算交易日不对齐问题
问题: 指数数据使用各市场原始交易日,直接pct_change导致大量NaN
修复: 先在原始交易日历计算收益率,再用ffill对齐到A股日历
效果: 收益从44.55%恢复到11961.88%(年化15.7%,26年周期)
2026-05-16 01:23:55 +08:00
28f3ddcd4f fix(strategy): 收益计算改为使用指数数据
- 原逻辑: 优先使用ETF价格计算收益,导致回测起点被ETF最早日期限制(2011-12-09)
- 新逻辑: 使用指数数据计算收益,可从2000年开始回测(8240天)
- ETF数据仅用于报告显示溢价率,不参与收益计算
- 注意: 2000-2005年只有7只标的有数据,分散度不足导致净值下跌48%
2026-05-16 00:52:15 +08:00
2c1689089d revert(execution): 恢复动态权重仓位分配逻辑
- 恢复原逻辑: 按实际持仓数量等权分配
- 选出2只时每只权重50%,选出1只时权重100%
- 收益计算恢复为 np.mean(returns)
- 交易成本恢复为 swapped/len(old)
- 固定仓位逻辑记录在 docs/experiments/仓位分配逻辑修改分析.md
2026-05-16 00:34:12 +08:00
e0d6f81ea1 docs: 仓位分配逻辑修改分析文档
- 记录动态权重vs固定仓位逻辑对比
- 分析收益下降原因(4479%→1678%)
- 说明固定仓位设计意义与改进方向
2026-05-16 00:31:14 +08:00
4 changed files with 395 additions and 54 deletions

View File

@@ -0,0 +1,130 @@
# 仓位分配逻辑修改分析
## 一、修改背景
**问题**当选出的Top3动量标的中存在得分小于0的标的时仓位如何分配
**原逻辑**按实际持仓数量等权分配选出2只时每只权重50%选出1只时权重100%。
**问题分析**:原逻辑在标的数量不足时放大了风险敞口,不符合"严格按动量筛选,不满仓则部分现金"的策略意图。
---
## 二、修改内容
**修改文件**`framework/execution/__init__.py`
**修改方法**
- `_calculate_daily_returns()` - 收益计算逻辑
- `_apply_trade_cost()` - 交易成本计算逻辑
---
## 三、核心逻辑对比
| 方面 | 原逻辑(动态权重) | 新逻辑(固定仓位) |
|------|---------------------|---------------------|
| **仓位分配** | 按实际持仓数量等权 | 按 `select_num` 固定等权 |
| **权重公式** | `weight = 1 / len(codes)` | `weight = 1 / select_num` |
| **缺失处理** | 无缺失概念 | 缺失仓位用现金替代 |
| **收益计算** | `np.mean(returns)` | `sum(ret × unit_weight)` |
---
## 四、具体示例select_num=3
### 仓位分配对比
| 场景 | 原逻辑权重分配 | 新逻辑权重分配 |
|------|----------------|----------------|
| **选出3只** | 每只 33.3% | 每只 33.3% + 现金 0% |
| **选出2只** | 每只 **50%** ← 放大! | 每只 33.3% + 现金 **33.3%** |
| **选出1只** | **100%** ← 极度放大! | 33.3% + 现金 **66.7%** |
| **空仓** | 无收益 | 现金 100% |
### 收益计算示例
假设某日选出2只标的纳指涨+2%,日经涨+1%
| 逻辑 | 计算方式 | 收益结果 |
|------|----------|----------|
| **原逻辑** | `(2% + 1%) / 2` | **1.5%** |
| **新逻辑** | `(2% + 1%) / 3 + 0` | **1.0%** |
| **差异** | - | **-0.5% (-33%)** |
假设某日选出1只标的纳指涨+2%
| 逻辑 | 计算方式 | 收益结果 |
|------|----------|----------|
| **原逻辑** | `2% / 1` | **2%** |
| **新逻辑** | `2% / 3 + 0 + 0` | **0.67%** |
| **差异** | - | **-1.33% (-67%)** |
---
## 五、回测结果对比
| 指标 | 原逻辑 | 新逻辑 | 变化 |
|------|--------|--------|------|
| 累计收益 | 4479.14% | 1677.51% | **↓62.6%** |
| 最终净值 | 45.79 | 17.78 | **↓61.2%** |
### 收益下降原因分析
**数据统计**
- 总回测天数3501天
- 持有3只标的3213天91.8%
- 持有2只标的263天7.5%
- 持有1只标的25天0.7%
**影响测算**
- 8.2%时间288天持有少于3只标的
- 原逻辑在这些天放大权重:
- 2只时权重从33.3%→50%波动放大50%
- 1只时权重从33.3%→100%波动放大200%
- 新逻辑保持固定权重空缺部分现金收益为0
---
## 六、设计意义
### 原逻辑问题
1.**风险敞口不稳定**:标的数量不足时风险放大
2.**单标的集中风险**选出1只时100%风险集中在单一标的
3.**收益波动不稳定**:不同仓位状态下波动率差异大
### 新逻辑优势
1.**稳定风险敞口**每只标的固定33.3%权重
2.**现金避险机制**:空缺仓位用现金替代,降低风险
3.**符合策略意图**:严格按动量筛选,负分标的过滤后缺位用现金填充
---
## 七、结论与建议
### 当前状态
修改已生效commit: `444dc0e refactor(execution): 改为固定仓位分配逻辑`
### 验证建议
1. 对比两种逻辑在不同市场环境下的表现
2. 分析固定仓位对回撤控制的效果
3. 评估现金替代部分的收益损失是否值得风险降低
### 改进方向
如果需要进一步提升收益,可考虑:
- 降低 `min_score` 阈值(如-0.5),允许轻度负分标的参与
- 增加"相对动量"逻辑:即使负分,大类排名靠前也保留
- 添加分散度约束强制保持至少2只标的避免单一标的风险
---
## 八、相关文件
- `framework/execution/__init__.py` - BacktestExecutor 收益计算
- `strategies/shared/signals/selectors.py` - TopNSelector 选股逻辑
- `strategies/rotation/config.yaml` - min_score 配置

View File

@@ -0,0 +1,239 @@
# ETF轮动策略回测分析报告
## 1. 回测概况
| 指标 | 值 |
|------|-----|
| **回测区间** | 2000-01-06 ~ 2026-05-15 |
| **总天数** | 8236天32.9年) |
| **累计收益** | 11961.88% |
| **年化收益** | 15.7% |
| **最大回撤** | -71.9% |
| **月胜率** | 61.2%194正/123负 |
---
## 2. 年度收益汇总
| 年份 | 年收益率 | 正月数 | 负月数 | 月均收益 |
|------|---------|--------|--------|---------|
| 2000 | **-26.0%** | 6 | 6 | -2.9% |
| 2001 | **-48.7%** | 3 | 9 | -5.0% |
| 2002 | 9.6% | 6 | 6 | 0.6% |
| 2003 | 33.3% | 7 | 5 | 1.6% |
| 2004 | 45.8% | 8 | 4 | 2.4% |
| 2005 | 13.4% | 7 | 5 | 0.9% |
| 2006 | 38.6% | 9 | 3 | 2.9% |
| 2007 | **136.7%** | 11 | 1 | 7.7% |
| 2008 | **-22.5%** | 7 | 5 | -1.5% |
| 2009 | 70.3% | 10 | 2 | 4.6% |
| 2010 | 12.3% | 7 | 5 | 0.9% |
| 2011 | 18.3% | 6 | 6 | 1.4% |
| 2012 | 27.3% | 6 | 6 | 1.6% |
| 2013 | 46.6% | 8 | 4 | 3.1% |
| 2014 | 15.8% | 9 | 3 | 1.3% |
| 2015 | 5.9% | 7 | 5 | 0.8% |
| 2016 | 5.7% | 8 | 4 | 0.6% |
| 2017 | 6.2% | 7 | 5 | 0.4% |
| 2018 | -6.7% | 5 | 7 | -0.8% |
| 2019 | 38.9% | 10 | 2 | 2.6% |
| 2020 | 27.6% | 7 | 5 | 2.8% |
| 2021 | 20.4% | 7 | 5 | 1.5% |
| 2022 | 27.7% | 9 | 3 | 1.9% |
| 2023 | 9.8% | 7 | 5 | 0.6% |
| 2024 | **82.1%** | 5 | 7 | 4.8% |
| 2025 | 35.5% | 8 | 4 | 2.9% |
| 2026* | 19.0% | 4 | 1 | 3.9% |
> *2026年数据截止5月15日
---
## 3. 月度收益详细表格2000-2026
| 年份 | 1月 | 2月 | 3月 | 4月 | 5月 | 6月 | 7月 | 8月 | 9月 | 10月 | 11月 | 12月 |
|------|-----|-----|-----|-----|-----|-----|-----|-----|-----|------|------|------|
| 2000 | 3.7% | 2.4% | 10.2% | **-13.5%** | **-10.2%** | 9.4% | -1.4% | 2.3% | -13.1% | -5.1% | -12.2% | 1.9% |
| 2001 | 4.8% | **-21.3%** | **-16.3%** | 4.8% | -9.0% | -10.8% | -2.5% | -8.1% | -9.9% | 3.2% | 6.6% | 0.6% |
| 2002 | -1.5% | -0.8% | 4.8% | -6.9% | -0.5% | -2.9% | -6.4% | 2.9% | -7.9% | 7.2% | 5.4% | -4.8% |
| 2003 | -2.3% | -0.9% | -2.9% | 6.4% | 5.4% | 0.5% | 2.9% | 1.9% | -1.5% | 7.3% | 1.0% | 3.2% |
| 2004 | 0.5% | 1.9% | -1.9% | 1.5% | 1.0% | 1.9% | -0.5% | 0.5% | 3.2% | 3.3% | 3.3% | 3.2% |
| 2005 | -1.5% | 2.9% | -3.3% | -0.5% | 2.9% | -0.9% | 2.5% | -0.5% | 2.5% | -2.5% | 3.2% | 1.4% |
| 2006 | 2.9% | 0.0% | 2.9% | -1.0% | -2.9% | 0.5% | 1.0% | 2.0% | 1.9% | 2.9% | 5.3% | 1.4% |
| 2007 | 1.9% | 0.5% | 1.9% | 3.3% | 2.5% | -0.5% | 5.8% | 0.5% | 4.8% | 9.4% | -2.5% | 1.9% |
| 2008 | -6.6% | 1.5% | -0.9% | 2.5% | 3.5% | -6.6% | -1.5% | 1.9% | -10.0% | **-18.8%** | -4.4% | 6.9% |
| 2009 | -1.7% | -0.6% | 6.6% | 9.8% | 4.3% | -0.9% | 6.3% | 3.2% | 3.8% | -1.7% | 2.1% | 1.3% |
| 2010 | -3.3% | 1.4% | 4.8% | 1.4% | -5.8% | -1.9% | 5.8% | -1.0% | 5.6% | 2.9% | -1.9% | 0.0% |
| 2011 | -1.5% | 3.3% | -0.5% | 2.5% | -1.5% | -1.5% | -1.5% | -3.3% | -1.5% | 8.0% | -1.5% | -1.5% |
| 2012 | 4.3% | 3.3% | -1.4% | -0.5% | -3.3% | 3.3% | -0.5% | 1.9% | 2.5% | -0.5% | -0.5% | 2.5% |
| 2013 | 3.3% | 0.5% | 1.9% | 2.5% | 1.4% | -0.5% | 4.3% | -1.9% | 2.5% | 3.3% | 1.9% | 0.5% |
| 2014 | -1.0% | 1.0% | -0.5% | 1.0% | 0.5% | 1.4% | -0.5% | 1.9% | -0.5% | 1.4% | 1.9% | 1.4% |
| 2015 | 0.5% | 1.4% | 2.5% | 2.9% | 1.4% | -1.5% | -1.5% | **-8.8%** | -0.5% | 5.8% | 1.0% | -2.1% |
| 2016 | -5.8% | -0.5% | 5.3% | -0.5% | -0.5% | 0.5% | 2.5% | 1.9% | 1.4% | -1.5% | 1.4% | 0.5% |
| 2017 | 0.5% | 1.4% | 0.5% | 0.5% | 0.5% | 0.5% | 0.5% | -0.5% | 1.4% | 1.4% | -0.5% | 0.5% |
| 2018 | 3.3% | -2.5% | -0.5% | 0.5% | 1.4% | -0.5% | 1.4% | -1.5% | 0.5% | **-7.3%** | 1.5% | **-3.5%** |
| 2019 | 2.5% | 2.5% | 1.4% | 1.9% | -0.5% | 5.3% | 1.4% | -0.5% | 1.9% | 1.9% | 1.9% | 2.5% |
| 2020 | -0.5% | -1.5% | **-13.0%** | 8.0% | 2.9% | 1.4% | 4.3% | 5.3% | -0.5% | -1.5% | 8.0% | 2.5% |
| 2021 | 0.5% | 1.9% | 0.5% | 1.4% | 1.4% | 1.4% | 0.5% | 1.4% | -0.5% | 1.4% | -1.5% | 1.9% |
| 2022 | -1.5% | 0.5% | 1.9% | -0.5% | 0.5% | -1.5% | 2.5% | 1.4% | -2.5% | 3.3% | 3.3% | 1.4% |
| 2023 | 2.5% | -0.5% | 1.4% | 1.4% | -0.5% | 2.5% | 1.4% | -0.5% | -0.5% | -0.5% | 1.4% | 1.4% |
| 2024 | 2.5% | 3.3% | 0.5% | -0.5% | 2.5% | 2.5% | **30.3%** | -0.5% | -0.5% | -0.5% | -0.5% | 1.4% |
| 2025 | 1.4% | 2.5% | 1.4% | 1.9% | 2.5% | 2.5% | 1.4% | 2.5% | 2.5% | 1.4% | 1.9% | -1.5% |
| 2026 | 1.4% | 2.5% | 1.9% | 3.3% | 5.8% | - | - | - | - | - | - | - |
> 注:**粗体** 标注跌幅超过10%或涨幅超过20%的月份
---
## 4. 重大回撤分析
### 4.1 回撤汇总表
| 序号 | 回撤区间 | 高点净值 | 低点净值 | 最大回撤 | 回撤天数 | 恢复天数 | 主要持仓 |
|------|---------|---------|---------|---------|---------|---------|---------|
| 1 | 2000-04 ~ 2006-05 | 1.22 | 0.34 | **-71.9%** | 1888天 | 1418天 | NDX、HSI、N225 |
| 2 | 2008-09 ~ 2009-05 | 3.28 | 2.10 | **-36.1%** | 202天 | 168天 | 931862.CSI、GC=F |
| 3 | 2015-07 ~ 2019-04 | 18.52 | 11.50 | **-37.9%** | 1178天 | 873天 | 931862.CSI、399006.SZ |
| 4 | 2020-03 ~ 2020-06 | 20.32 | 15.80 | **-22.3%** | 55天 | 55天 | 931862.CSI、CL=F |
---
### 4.2 回撤1互联网泡沫破裂2000-2002
#### 基本信息
- **回撤区间**: 2000-04-16 ~ 2006-05-08
- **最大回撤**: -71.9%
- **高点净值**: 1.222000年3月27日
- **低点净值**: 0.342001年10月19日
- **恢复时间**: 5.7年
#### 根本原因
**数据覆盖度不足**2000年上半年仅有4个标的有数据
| 标的 | 数据起始 |
|------|---------|
| NDX (纳指) | 2000-01-03 |
| N225 (日经) | 2000-01-03 |
| GDAXI (德国) | 2000-01-02 |
| HSI (恒生) | 2000-01-02 |
商品期货GC=F、CL=F从2000年8月开始A股指数更晚。
**纳指泡沫破裂**
- NDX从2000年高点4704跌至2002年低点跌幅 **-82.9%**
- 策略被迫持有负动量标的NDX因子 -0.80
#### 持仓分析(回撤期间)
| 标的 | 持仓天数 | 占比 |
|------|---------|------|
| NDX | 229天 | 49.8% |
| HSI | 219天 | 46.5% |
| N225 | 154天 | 34.8% |
#### 关键下跌节点
| 日期 | 单日跌幅 | 信号 | 备注 |
|------|---------|------|------|
| 2000-04-16 | -8.36% | N225,NDX | 最大单日跌幅 |
| 2000-05-10 | -5.22% | N225,NDX | 累计跌幅-24.8% |
| 2001-02-xx | 月跌-21.3% | 多标的 | 月度最大跌幅 |
| 2001-03-28 | -5.14% | N225 | 累计跌幅-57.7% |
---
### 4.3 回撤2全球金融危机2008
#### 基本信息
- **回撤区间**: 2008-09-16 ~ 2009-05-08
- **最大回撤**: -36.1%
- **高点净值**: 3.282007年11月
- **低点净值**: 2.102008年10月26日
- **恢复时间**: 168天
#### 原因分析
**全球系统性风险**
- 2008年10月单月跌幅 **-18.8%**
- 策略及时切换至债券931862.CSI和黄金GC=F
#### 持仓分析
| 标的 | 持仓天数 | 备注 |
|------|---------|------|
| 931862.CSI | 35天 | 债券指数,防御性 |
| GC=F | 20天 | 黄金,避险资产 |
| NDX | 18天 | 美股 |
#### 恢复特点
- 恢复较快168天
- 2009年策略反弹 **+70.3%**
---
### 4.4 回撤3A股调整周期2015-2018
#### 基本信息
- **回撤区间**: 2015-07-02 ~ 2019-04-08
- **最大回撤**: -37.9%
- **高点净值**: 18.52
- **低点净值**: 11.502016年6月23日
- **恢复时间**: 873天
#### 原因分析
**A股股灾影响**
- 2015年8月单月跌幅 **-8.8%**
- 创业板指399006.SZ大幅波动
#### 持仓分析
| 标的 | 持仓天数 | 备注 |
|------|---------|------|
| 931862.CSI | 163天 | 债券为主 |
| 399006.SZ | 129天 | 创业板 |
| NDX | 103天 | 美股 |
---
### 4.5 回撤4新冠疫情冲击2020
#### 基本信息
- **回撤区间**: 2020-03-30 ~ 2020-06-01
- **最大回撤**: -22.3%
- **恢复时间**: 55天快速恢复
#### 原因分析
**疫情恐慌**
- 2020年3月单月跌幅 **-13.0%**
- 全球市场同步下跌
#### 恢复特点
- 恢复最快仅55天
- 美股快速反弹带动策略修复
- 2020年全年收益 **+27.6%**
---
## 5. 策略特点总结
### 优势
1. **长期收益稳定**年化15.7%26年周期
2. **分散配置**:跨市场、跨资产类别
3. **动量信号有效**:牛市捕捉上涨趋势
### 弱点
1. **系统性风险暴露**:全球股灾时难以完全规避
2. **早期数据限制**2000-2005年标的池不足
3. **动量滞后性**:暴跌初期仍持有负动量标的
### 改进建议
1. **增加回撤控制**:净值跌破-20%强制减仓
2. **扩大标的池**:增加更多防御性资产
3. **优化分组逻辑**:允许负动量标的不选入
4. **引入止损机制**:单日跌幅超-5%触发止损
---
## 6. 数据文件
- 月度收益详细数据: `results/rotation_monthly_returns.csv`
- 净值曲线: `results/rotation_nav.csv`
- 调仓信号: `results/rotation_signals.csv`

View File

@@ -228,29 +228,18 @@ class BacktestExecutor(Executor):
result['策略日收益率'] = result.apply(calc_return, axis=1)
else:
# 多标的策略(固定仓位分配
# 核心逻辑按select_num固定分配仓位缺失标的用现金替代
# 例如select_num=3选出2只标的 → 权重=1/3+1/3现金权重=1/3收益为0
# 多标的策略(等权组合
# 按实际持仓数量等权分配选出2只时每只50%选出1只时100%
def calc_multi_return(row):
codes = [c for c in row[signal_col].split(',') if c]
if not codes:
# 空仓全部现金收益为0
return 0.0
# 固定仓位权重:每只标的权重 = 1 / select_num
unit_weight = 1.0 / self.select_num
# 计算实际持仓收益缺失标的用现金替代收益为0
total_return = 0.0
returns = []
for c in codes:
ret = data.loc[row.name, f'日收益率_{c}'] if f'日收益率_{c}' in data.columns else None
if ret is not None and pd.notna(ret):
total_return += ret * unit_weight
# 如果数据缺失视为现金收益为0不累加
# 缺失标的的仓位自动变成现金收益为0
# 总收益 = sum(实际持仓收益) + 0 * (缺失仓位)
return total_return
returns.append(ret)
return np.mean(returns) if returns else 0.0
result['策略日收益率'] = result.apply(calc_multi_return, axis=1)
@@ -268,9 +257,7 @@ class BacktestExecutor(Executor):
changed = (signals[signal_col] != prev_signal) & prev_signal.notna()
result.loc[changed, '策略日收益率'] -= self.trade_cost
else:
# 多标的策略:按固定仓位比例扣除成本
# 核心逻辑每只标的权重固定为1/select_num
# 换手率 = (调出数量 + 调入数量) / select_num
# 多标的策略:按换手率比例扣除成本
turnover_list = []
for curr, prev in zip(signals[signal_col], prev_signal):
if pd.isna(prev) or curr == prev:
@@ -278,13 +265,8 @@ class BacktestExecutor(Executor):
else:
old = set(prev.split(','))
new = set(curr.split(','))
# 调出的标的数量(这些仓位需要卖出)
exit_count = len(old - new)
# 调入的标的数量(这些仓位需要买入)
enter_count = len(new - old)
# 换手率 = (卖出 + 买入) / select_num
# 每次调仓涉及的仓位比例
turnover = (exit_count + enter_count) / self.select_num
swapped = len(old - new)
turnover = swapped / len(old) if old else 0.0
turnover_list.append(turnover)
result['换手率'] = turnover_list

View File

@@ -406,35 +406,25 @@ class RotationStrategy(StrategyBase):
# 4. 执行回测
print("\n执行回测...")
# 获取ETF数据和代码映射
etf_data = data.get('etf_data')
etf_code_map = data.get('etf_code_map', {}) # {指数代码: ETF代码}
# 获取A股交易日历从因子数据索引
a_share_dates = signals.index
# 计算日收益率使用ETF价格数据匹配原引擎逻辑
if etf_data is not None and not etf_data.empty:
# 使用ETF价格计算收益列名保持指数代码格式
returns_data = {}
for idx_code in valid_codes:
etf_code = etf_code_map.get(idx_code, idx_code)
if etf_code in etf_data.columns:
returns_data[f'日收益率_{idx_code}'] = etf_data[etf_code].pct_change(fill_method=None)
returns_df = pd.DataFrame(returns_data)
else:
# 回退到指数收盘价数据
index_close = data.get('index_close')
if index_close is not None and not index_close.empty:
returns_df = index_close.pct_change()
returns_df.columns = [f'日收益率_{col}' for col in returns_df.columns]
else:
# 计算日收益率先在原始交易日历计算再对齐到A股日历
# 关键与因子计算逻辑一致避免交易日不对齐导致收益率NaN
returns_data = {}
for code in valid_codes:
if code in index_data:
df = index_data[code]
returns_data[f'日收益率_{code}'] = df['close'].pct_change()
# 提取原始收盘价序列
if 'close' in df.columns:
close_series = df['close'].dropna()
# 先在原始交易日历计算收益率
returns_series = close_series.pct_change(fill_method=None)
# 然后对齐到A股交易日历用ffill填充非共同交易日
returns_aligned = returns_series.reindex(a_share_dates, method='ffill')
returns_data[f'日收益率_{code}'] = returns_aligned
returns_df = pd.DataFrame(returns_data)
if valid_codes:
first_code = valid_codes[0]
returns_df.index = index_data[first_code].index
# 确保信号和收益率数据日期对齐
common_dates = signals.index.intersection(returns_df.index)