Compare commits
11 Commits
5c4aeb75d2
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b698857e49 | |||
| a600a71aa3 | |||
| 3b0688930d | |||
| 09ecac9e56 | |||
| cabfee20b0 | |||
| d657f8506b | |||
| 6e7087a543 | |||
| 8c3ae2269a | |||
| 49b623931b | |||
| fe73c0f199 | |||
| e2038ae722 |
2
.env
2
.env
@@ -7,7 +7,7 @@ TUSHARE_TOKEN=725296d48ec74da89422e8be76bd770895a4bf93b4998aca4b898db6
|
|||||||
DINGTALK_WEBHOOK_1=https://oapi.dingtalk.com/robot/send?access_token=fb70c1561d8beba94b4f11568f4bb15e3ae07ccbdc8ac19676434a9d1cd17546
|
DINGTALK_WEBHOOK_1=https://oapi.dingtalk.com/robot/send?access_token=fb70c1561d8beba94b4f11568f4bb15e3ae07ccbdc8ac19676434a9d1cd17546
|
||||||
DINGTALK_SECRET_1=SEC1ae7cd2f1a6f9da3611af37da3e7d954c1e8533fc073c6c8cc5e5af3b6e5926b
|
DINGTALK_SECRET_1=SEC1ae7cd2f1a6f9da3611af37da3e7d954c1e8533fc073c6c8cc5e5af3b6e5926b
|
||||||
|
|
||||||
钉钉机器人配置 - 群2
|
# 钉钉机器人配置 - 群2
|
||||||
DINGTALK_WEBHOOK_2=https://oapi.dingtalk.com/robot/send?access_token=87c7abfcdd69b699c32da4e4f5981cd2ca6b0445474fc6ffb36f2ed0f6262fbb
|
DINGTALK_WEBHOOK_2=https://oapi.dingtalk.com/robot/send?access_token=87c7abfcdd69b699c32da4e4f5981cd2ca6b0445474fc6ffb36f2ed0f6262fbb
|
||||||
DINGTALK_SECRET_2=SECf3d6b43f2f8a87ab91feffd052e71ec314fbf57a1842e483fe07af3c0a0e5aa6
|
DINGTALK_SECRET_2=SECf3d6b43f2f8a87ab91feffd052e71ec314fbf57a1842e483fe07af3c0a0e5aa6
|
||||||
|
|
||||||
|
|||||||
413
docs/ETF候选池筛选报告.md
Normal file
413
docs/ETF候选池筛选报告.md
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
# 轮动策略 ETF 候选池筛选报告
|
||||||
|
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**创建日期**: 2026-06-20
|
||||||
|
**数据来源**: `rotation/results/etf_basic_full.csv`(Tushare etf_basic + enrich_etf_data 增强)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、筛选目标
|
||||||
|
|
||||||
|
为 `config_simple.yaml` 中每个指数/商品标的,从全市场 A 股场内 ETF 中筛选出跟踪同一底层资产、市值最大的 Top4 ETF 作为候选交易标的池。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、筛选方法
|
||||||
|
|
||||||
|
### 2.1 数据源
|
||||||
|
|
||||||
|
- **基础数据**: Tushare `etf_basic` 接口,共 1601 只场内基金(ETF + LOF)
|
||||||
|
- **增强字段**: 通过 `enrich_etf_data.py` 补充基金规模(亿份×NAV)、日均成交额、最新净值等
|
||||||
|
- **净值数据**: Tushare `fund_nav` 接口,取 `unit_nav`(单位净值)计算收益率
|
||||||
|
|
||||||
|
### 2.2 匹配规则
|
||||||
|
|
||||||
|
由于 Tushare 的 `index_code` 字段包含交易所后缀(如 `GDAXI.GY`、`HSI.HI`),不可直接精确匹配。采用以下策略:
|
||||||
|
|
||||||
|
1. **先反查**: 用当前 `trade_source` ETF 反查其在 CSV 中的真实 `index_code` 和 `index_name`
|
||||||
|
2. **精确匹配**: 对 `index_code` 有值的 ETF,按 `index_code` 精确匹配
|
||||||
|
3. **关键词匹配**: 对 `index_code` 为 NaN 的(如原油 LOF),按 `index_name` 关键词模糊匹配
|
||||||
|
4. **人工校验**: 排除增强策略型、沪港通型等底层资产不同的 ETF
|
||||||
|
|
||||||
|
### 2.3 排序规则
|
||||||
|
|
||||||
|
按 `fund_scale_yi`(基金规模,亿元)降序排列,取 Top4。不足 4 只则按实际数量。
|
||||||
|
|
||||||
|
### 2.4 异常排除
|
||||||
|
|
||||||
|
| 异常 ETF | 原因 | 替换为 |
|
||||||
|
|---------|------|--------|
|
||||||
|
| 159977.SZ 天弘创业板ETF | 增强策略型,与其他纯跟踪 ETF 相关性仅 0.83 | 159957.SZ 华夏创业板ETF |
|
||||||
|
| 513660.SH 华夏沪港通恒生ETF | 沪港通机制,净值计算方式不同,与其他恒生 ETF 相关性仅 0.27 | 513210.SH 易方达恒生ETF |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、各指数筛选过程与原始数据
|
||||||
|
|
||||||
|
### 3.1 创业板指 (399006.SZ)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = 399006.SZ`
|
||||||
|
**候选总数**: 17 只
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 159915.SZ ★ | 易方达创业板ETF | 448.9 | 567,036 | 0.15% |
|
||||||
|
| 2 | 159952.SZ | 广发创业板ETF | 87.7 | 59,444 | 0.15% |
|
||||||
|
| 3 | 159957.SZ | 华夏创业板ETF | 21.5 | 20,636 | 0.15% |
|
||||||
|
| 4 | 159948.SZ | 南方创业板ETF | 47.6 | 11,196 | 0.15% |
|
||||||
|
|
||||||
|
> 注: 原始 Top3 为 159977.SZ(天弘创业板ETF),因增强策略型排除,替换为 159957.SZ
|
||||||
|
|
||||||
|
**相关性矩阵** (1568 共同交易日):
|
||||||
|
|
||||||
|
| | 159915 | 159952 | 159957 | 159948 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **159915** | 1.0000 | 1.0000 | 0.9999 | 1.0000 |
|
||||||
|
| **159952** | 1.0000 | 1.0000 | 0.9999 | 1.0000 |
|
||||||
|
| **159957** | 0.9999 | 0.9999 | 1.0000 | 0.9999 |
|
||||||
|
| **159948** | 1.0000 | 1.0000 | 0.9999 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9999**
|
||||||
|
- 最低相关性: 0.9999(广发 vs 华夏)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.2 红利低波 (H30269.CSI)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = H30269.CSI`
|
||||||
|
**候选总数**: 6 只
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 512890.SH ★ | 华泰柏瑞中证红利低波动ETF | 324.1 | 84,666 | 0.50% |
|
||||||
|
| 2 | 563020.SH | 易方达中证红利低波动ETF | 111.8 | 29,589 | 0.15% |
|
||||||
|
| 3 | 159547.SZ | 华夏中证红利低波动ETF | 14.0 | 14,909 | 0.15% |
|
||||||
|
| 4 | 560150.SH | 红利TK | 7.3 | 1,079 | 0.40% |
|
||||||
|
|
||||||
|
**相关性矩阵** (539 共同交易日):
|
||||||
|
|
||||||
|
| | 512890 | 563020 | 159547 | 560150 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **512890** | 1.0000 | 0.9893 | 0.9965 | 0.9932 |
|
||||||
|
| **563020** | 0.9893 | 1.0000 | 0.9863 | 0.9838 |
|
||||||
|
| **159547** | 0.9965 | 0.9863 | 1.0000 | 0.9908 |
|
||||||
|
| **560150** | 0.9932 | 0.9838 | 0.9908 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9900**
|
||||||
|
- 最低相关性: 0.9838(易方达 vs 红利TK)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.3 黄金 (Au99.99.SGE / SHAU.SGE)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code IN (Au99.99.SGE, SHAU.SGE)`
|
||||||
|
**候选总数**: 14 只(Au99.99 基准 7 只 + SHAU 基准 7 只)
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 | 基准 |
|
||||||
|
|------|------|------|---------|------------|------|------|
|
||||||
|
| 1 | 518880.SH ★ | 华安黄金ETF | 965.5 | 322,467 | 0.50% | Au99.99 |
|
||||||
|
| 2 | 159937.SZ | 博时黄金ETF | 433.6 | 74,446 | 0.50% | Au99.99 |
|
||||||
|
| 3 | 159934.SZ | 易方达黄金ETF | 367.9 | 89,130 | 0.50% | Au99.99 |
|
||||||
|
| 4 | 518800.SH | 国泰黄金ETF | 353.6 | 55,747 | 0.50% | Au99.99 |
|
||||||
|
|
||||||
|
**相关性矩阵** (1568 共同交易日):
|
||||||
|
|
||||||
|
| | 518880 | 159937 | 159934 | 518800 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **518880** | 1.0000 | 0.9999 | 0.9920 | 0.9899 |
|
||||||
|
| **159937** | 0.9999 | 1.0000 | 0.9920 | 0.9899 |
|
||||||
|
| **159934** | 0.9920 | 0.9920 | 1.0000 | 0.9821 |
|
||||||
|
| **518800** | 0.9899 | 0.9899 | 0.9821 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9910**
|
||||||
|
- 最低相关性: 0.9821(易方达 vs 国泰)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.4 德国DAX (GDAXI.GY)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = GDAXI.GY`
|
||||||
|
**候选总数**: 2 只(全市场仅此 2 只跟踪 DAX)
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 159561.SZ | 嘉实德国DAXETF(QDII) | 20.9 | 12,446 | 0.50% |
|
||||||
|
| 2 | 513030.SH ★ | 华安德国(DAX)ETF | 18.3 | 8,875 | 0.80% |
|
||||||
|
|
||||||
|
**相关性矩阵** (522 共同交易日):
|
||||||
|
|
||||||
|
| | 159561 | 513030 |
|
||||||
|
|---|---|---|
|
||||||
|
| **159561** | 1.0000 | 0.9947 |
|
||||||
|
| **513030** | 0.9947 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9947**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.5 有色金属 (IMCI.SHF)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = IMCI.SHF`
|
||||||
|
**候选总数**: 1 只(全市场仅此 1 只跟踪有色金属期货价格指数)
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 159980.SZ ★ | 大成有色金属期货ETF | 61.0 | 46,989 | 0.60% |
|
||||||
|
|
||||||
|
> 注: 其余有色金属 ETF 跟踪的是股票指数(如中证有色金属矿业/工业主题),底层资产不同,不纳入候选。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.6 恒生指数 (HSI.HI)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = HSI.HI`
|
||||||
|
**候选总数**: 5 只
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 159920.SZ ★ | 华夏恒生ETF | 93.3 | 33,200 | 0.60% |
|
||||||
|
| 2 | 513600.SH | 南方恒生指数ETF | 17.1 | 10,292 | 0.50% |
|
||||||
|
| 3 | 159271.SZ | 鹏华恒生ETF | 2.3 | 520 | 0.50% |
|
||||||
|
| 4 | 513210.SH | 易方达恒生ETF(QDII) | 0.6 | 732 | 0.50% |
|
||||||
|
|
||||||
|
> 注: 原始 Top2 为 513660.SH(华夏沪港通恒生ETF),因沪港通机制排除,替换为 513210.SH
|
||||||
|
|
||||||
|
**相关性矩阵** (209 共同交易日):
|
||||||
|
|
||||||
|
| | 159920 | 513600 | 159271 | 513210 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **159920** | 1.0000 | 0.9988 | 0.9941 | 1.0000 |
|
||||||
|
| **513600** | 0.9988 | 1.0000 | 0.9952 | 0.9989 |
|
||||||
|
| **159271** | 0.9941 | 0.9952 | 1.0000 | 0.9942 |
|
||||||
|
| **513210** | 1.0000 | 0.9989 | 0.9942 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9969**
|
||||||
|
- 最低相关性: 0.9941(华夏 vs 鹏华)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.7 恒生科技 (HSTECH.HI)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = HSTECH.HI`
|
||||||
|
**候选总数**: 13 只
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 513180.SH | 华夏恒生科技ETF(QDII) | 444.0 | 341,088 | 0.50% |
|
||||||
|
| 2 | 513130.SH ★ | 华泰柏瑞恒生科技ETF(QDII) | 392.9 | 384,592 | 0.20% |
|
||||||
|
| 3 | 513010.SH | 易方达恒生科技ETF(QDII) | 282.9 | 128,761 | 0.20% |
|
||||||
|
| 4 | 159740.SZ | 大成恒生科技ETF(QDII) | 171.9 | 134,470 | 0.50% |
|
||||||
|
|
||||||
|
**相关性矩阵** (1231 共同交易日):
|
||||||
|
|
||||||
|
| | 513180 | 513130 | 513010 | 159740 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **513180** | 1.0000 | 0.9953 | 0.9999 | 0.9998 |
|
||||||
|
| **513130** | 0.9953 | 1.0000 | 0.9953 | 0.9953 |
|
||||||
|
| **513010** | 0.9999 | 0.9953 | 1.0000 | 0.9998 |
|
||||||
|
| **159740** | 0.9998 | 0.9953 | 0.9998 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9976**
|
||||||
|
- 最低相关性: 0.9953(华泰柏瑞 vs 易方达/大成)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.8 日经225 (N225.JT)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = N225.JT`
|
||||||
|
**候选总数**: 4 只
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 513880.SH | 华安日经225ETF | 27.9 | 47,208 | 0.20% |
|
||||||
|
| 2 | 513520.SH ★ | 华夏野村日经225ETF | 24.7 | 55,341 | 0.20% |
|
||||||
|
| 3 | 513000.SH | 易方达奥明日经225ETF(QDII) | 23.1 | 29,234 | 0.20% |
|
||||||
|
| 4 | 159866.SZ | 日经ETF工银 | 13.5 | 33,442 | 0.20% |
|
||||||
|
|
||||||
|
**相关性矩阵** (1268 共同交易日):
|
||||||
|
|
||||||
|
| | 513880 | 513520 | 513000 | 159866 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **513880** | 1.0000 | 0.9336 | 0.9850 | 0.9915 |
|
||||||
|
| **513520** | 0.9336 | 1.0000 | 0.9442 | 0.9384 |
|
||||||
|
| **513000** | 0.9850 | 0.9442 | 1.0000 | 0.9873 |
|
||||||
|
| **159866** | 0.9915 | 0.9384 | 0.9873 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9634**
|
||||||
|
- 最低相关性: 0.9336(华安 vs 华夏)
|
||||||
|
- 说明: 华安日经225使用野村指数版本,华夏使用野村另一版本,两者在净值披露时点上略有差异
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.9 纳指100 (NDX.NASDAQ)
|
||||||
|
|
||||||
|
**匹配规则**: `index_code = NDX.NASDAQ`
|
||||||
|
**候选总数**: 12 只
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 |
|
||||||
|
|------|------|------|---------|------------|------|
|
||||||
|
| 1 | 159941.SZ | 广发纳指100ETF | 339.8 | 251,747 | 0.80% |
|
||||||
|
| 2 | 513100.SH ★ | 国泰纳斯达克100(QDII-ETF) | 190.9 | 116,472 | 0.60% |
|
||||||
|
| 3 | 513300.SH | 华夏纳斯达克100ETF(QDII) | 129.6 | 88,050 | 0.60% |
|
||||||
|
| 4 | 159501.SZ | 嘉实纳斯达克100ETF(QDII) | 120.1 | 25,504 | 0.50% |
|
||||||
|
|
||||||
|
**相关性矩阵** (736 共同交易日):
|
||||||
|
|
||||||
|
| | 159941 | 513100 | 513300 | 159501 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **159941** | 1.0000 | 0.9996 | 0.9891 | 0.9976 |
|
||||||
|
| **513100** | 0.9996 | 1.0000 | 0.9889 | 0.9975 |
|
||||||
|
| **513300** | 0.9891 | 0.9889 | 1.0000 | 0.9870 |
|
||||||
|
| **159501** | 0.9976 | 0.9975 | 0.9870 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9933**
|
||||||
|
- 最低相关性: 0.9870(华夏 vs 嘉实)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3.10 原油 (CL=F → WTI 原油价格)
|
||||||
|
|
||||||
|
**匹配规则**: `index_name` 包含 `WTI原油价格` / `BRENT原油` / `标普高盛原油商品` / `原油价格收益率`
|
||||||
|
**候选总数**: 3 只(全市场仅 3 只跟踪原油商品价格的 LOF)
|
||||||
|
**Top4 选取**:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 规模(亿) | 日均成交(万) | 费率 | 跟踪基准 |
|
||||||
|
|------|------|------|---------|------------|------|---------|
|
||||||
|
| 1 | 160723.SZ ★ | 嘉实原油(QDII-LOF-FOF) | 14.8 | 70,634 | 1.00% | WTI×100% |
|
||||||
|
| 2 | 161129.SZ | 易方达原油(QDII-LOF-FOF) | 7.18 | 58,148 | 1.00% | 标普高盛原油×100% |
|
||||||
|
| 3 | 501018.SH | 南方原油(QDII-LOF-FOF) | 13.36 | 70,781 | 1.00% | WTI×60%+BRENT×40% |
|
||||||
|
|
||||||
|
> 注: 其余 17 只石油天然气 ETF 跟踪的是石油公司股票指数(非商品价格),不纳入候选。
|
||||||
|
|
||||||
|
**相关性矩阵** (1567 共同交易日):
|
||||||
|
|
||||||
|
| | 160723 | 161129 | 501018 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| **160723** | 1.0000 | 0.9572 | 0.9581 |
|
||||||
|
| **161129** | 0.9572 | 1.0000 | 0.9802 |
|
||||||
|
| **501018** | 0.9581 | 0.9802 | 1.0000 |
|
||||||
|
|
||||||
|
- 平均相关性: **0.9651**
|
||||||
|
- 最低相关性: 0.9572(嘉实 vs 易方达)
|
||||||
|
- 三者的年化 TE(两两之间): 6.67%~9.64%,全区间累计收益差约 20%
|
||||||
|
|
||||||
|
**石油天然气股票型 ETF 排除分析**
|
||||||
|
|
||||||
|
全市场另有 17 只石油天然气相关 ETF/LOF,但它们跟踪的是石油天然气**公司股票指数**(非商品价格),与 WTI 原油价格的相关性远低于商品价格型 LOF。
|
||||||
|
|
||||||
|
以 160723.SZ(嘉实原油/WTI)净值为基准,与 17 只石油股票型 ETF 计算相关性:
|
||||||
|
|
||||||
|
| 排名 | 代码 | 名称 | 相关系数 | R² | 累计收益 |
|
||||||
|
|------|------|------|---------|-----|----------|
|
||||||
|
| 1 | 160416.SZ | 华安标普全球石油LOF | 0.6321 | 0.400 | +89.5% |
|
||||||
|
| 2 | 162719.SZ | 广发道琼斯美国石油LOF | 0.5579 | 0.311 | +165.0% |
|
||||||
|
| 3 | 159518.SZ | 嘉实标普石油天然气QDII | 0.5550 | 0.308 | +5.1% |
|
||||||
|
| 4 | 513350.SH | 富国标普石油天然气QDII | 0.5527 | 0.306 | +10.3% |
|
||||||
|
| 5 | 162411.SZ | 华宝标普石油天然气上游LOF | 0.5430 | 0.295 | +99.7% |
|
||||||
|
| 6 | 159588.SZ | 景顺石油天然气ETF | 0.3971 | 0.158 | +21.4% |
|
||||||
|
| 7-13 | 其余 7 只 | 国证石油天然气系列ETF | 0.20~0.38 | 0.04~0.14 | -14%~+29% |
|
||||||
|
|
||||||
|
**对照:商品价格型 LOF**
|
||||||
|
|
||||||
|
| 代码 | 名称 | 相关系数 | R² | 累计收益 |
|
||||||
|
|------|------|---------|-----|----------|
|
||||||
|
| 161129.SZ | 易方达原油 | **0.9572** | **0.916** | +28.3% |
|
||||||
|
| 501018.SH | 南方原油 | **0.9581** | **0.918** | +35.1% |
|
||||||
|
|
||||||
|
**排除决策依据**:
|
||||||
|
|
||||||
|
1. **相关性断崖式下降**:商品价格型 R²=0.92,股票型最高仅 R²=0.40,差距 2.3 倍
|
||||||
|
2. **累计收益严重偏离**:WTI 累计 +48%,石油股票型从 -14% 到 +165% 不等,完全不可互换
|
||||||
|
3. **底层资产本质不同**:石油股票受公司基本面、分红、管理层等因素影响,与原油价格的联动远不如期货合约直接
|
||||||
|
|
||||||
|
**结论**:当前 3 只商品价格型 LOF(160723/161129/501018)已是原油方向的最优选择,无需补充石油股票型 ETF。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、汇总
|
||||||
|
|
||||||
|
### 4.1 各指数组内相关性总览
|
||||||
|
|
||||||
|
| 指数 | ETF数 | 共同天数 | 平均相关 | 最低相关 | 最高相关 | 状态 |
|
||||||
|
|------|-------|---------|---------|---------|---------|------|
|
||||||
|
| 创业板指 | 4 | 1568 | 0.9999 | 0.9999 | 1.0000 | ✓ |
|
||||||
|
| 红利低波 | 4 | 539 | 0.9900 | 0.9838 | 0.9965 | ✓ |
|
||||||
|
| 黄金 | 4 | 1568 | 0.9910 | 0.9821 | 0.9999 | ✓ |
|
||||||
|
| 德国DAX | 2 | 522 | 0.9947 | 0.9947 | 0.9947 | ✓ |
|
||||||
|
| 有色金属 | 1 | — | — | — | — | 仅1只 |
|
||||||
|
| 恒生指数 | 4 | 209 | 0.9969 | 0.9941 | 1.0000 | ✓ |
|
||||||
|
| 恒生科技 | 4 | 1231 | 0.9976 | 0.9953 | 0.9999 | ✓ |
|
||||||
|
| 日经225 | 4 | 1268 | 0.9634 | 0.9336 | 0.9915 | ⚠ |
|
||||||
|
| 纳指100 | 4 | 736 | 0.9933 | 0.9870 | 0.9996 | ✓ |
|
||||||
|
| 原油 | 3 | 1567 | 0.9651 | 0.9572 | 0.9802 | ✓ |
|
||||||
|
|
||||||
|
### 4.2 最终候选池
|
||||||
|
|
||||||
|
| 指数 | 当前ETF | 候选池 |
|
||||||
|
|------|---------|--------|
|
||||||
|
| 创业板指 | 159915.SZ | 159915.SZ, 159952.SZ, 159957.SZ, 159948.SZ |
|
||||||
|
| 红利低波 | 512890.SH | 512890.SH, 563020.SH, 159547.SZ, 560150.SH |
|
||||||
|
| 黄金 | 518880.SH | 518880.SH, 159937.SZ, 159934.SZ, 518800.SH |
|
||||||
|
| 德国DAX | 513030.SH | 159561.SZ, 513030.SH |
|
||||||
|
| 有色金属 | 159980.SZ | 159980.SZ |
|
||||||
|
| 恒生指数 | 159920.SZ | 159920.SZ, 513600.SH, 159271.SZ, 513210.SH |
|
||||||
|
| 恒生科技 | 513130.SH | 513180.SH, 513130.SH, 513010.SH, 159740.SZ |
|
||||||
|
| 日经225 | 513520.SH | 513880.SH, 513520.SH, 513000.SH, 159866.SZ |
|
||||||
|
| 纳指100 | 513100.SH | 159941.SZ, 513100.SH, 513300.SH, 159501.SZ |
|
||||||
|
| 原油 | 160723.SZ | 160723.SZ, 161129.SZ, 501018.SH |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、关键决策记录
|
||||||
|
|
||||||
|
### 5.1 原油只选商品价格型,不选石油股票型
|
||||||
|
|
||||||
|
全市场有 20+ 只石油天然气相关 ETF/LOF,但底层资产完全不同:
|
||||||
|
- **商品价格型**(3 只): 直接投资 WTI/BRENT 原油期货合约
|
||||||
|
- **石油股票型**(17 只): 投资石油天然气上市公司股票
|
||||||
|
|
||||||
|
配置中 `CL=F` 对应 WTI 原油期货价格,因此只纳入商品价格型的 3 只。
|
||||||
|
|
||||||
|
### 5.2 有色金属只有 1 只候选
|
||||||
|
|
||||||
|
全市场跟踪有色金属期货价格指数(IMCI.SHF)的 ETF 仅 `159980.SZ` 一只。其余 23 只跟踪的是有色金属股票指数(如中证有色金属矿业/工业/细分主题),底层资产是股票而非期货,不纳入。
|
||||||
|
|
||||||
|
### 5.3 德国DAX 只有 2 只候选
|
||||||
|
|
||||||
|
全市场跟踪 DAX 指数的 ETF 仅 2 只,无其他欧洲宽基可替代(法国 CAC40 是不同指数)。
|
||||||
|
|
||||||
|
### 5.4 排除增强策略型 ETF
|
||||||
|
|
||||||
|
159977.SZ(天弘创业板ETF)是增强策略型,与其他纯被动跟踪 ETF 的相关性仅 0.83,替换为 159957.SZ(华夏创业板ETF,纯被动型)。
|
||||||
|
|
||||||
|
### 5.5 排除沪港通型 ETF
|
||||||
|
|
||||||
|
513660.SH(华夏沪港通恒生ETF)通过沪港通机制投资,净值计算方式与直接投资的恒生 ETF 完全不同,与其他恒生 ETF 相关性仅 0.27,替换为 513210.SH(易方达恒生ETF)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、复现方法
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 数据源
|
||||||
|
rotation/results/etf_basic_full.csv
|
||||||
|
|
||||||
|
# 筛选脚本(交互式)
|
||||||
|
/Users/aszer/miniforge3/envs/qa/bin/python3
|
||||||
|
|
||||||
|
# 关键步骤:
|
||||||
|
# 1. 用 trade_source ETF 反查 index_code(含交易所后缀)
|
||||||
|
# 2. 按 index_code 精确匹配 + index_name 关键词匹配
|
||||||
|
# 3. 按 fund_scale_yi 降序取 Top4
|
||||||
|
# 4. 排除增强策略型、沪港通型等异常 ETF
|
||||||
|
# 5. 用 fund_nav 获取 unit_nav,计算日收益率,生成相关性矩阵
|
||||||
|
```
|
||||||
233
docs/ETF跟踪误差计算方法.md
Normal file
233
docs/ETF跟踪误差计算方法.md
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
# ETF跟踪误差计算方法
|
||||||
|
|
||||||
|
**文档版本**: v1.0
|
||||||
|
**创建日期**: 2026-06-19
|
||||||
|
**适用范围**: 轮动策略标的池ETF跟踪准确率评估
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、定义
|
||||||
|
|
||||||
|
**跟踪误差(Tracking Error, TE)**:衡量ETF净值收益率与标的指数收益率之间偏离程度的指标,反映基金经理的追踪能力。
|
||||||
|
|
||||||
|
**核心公式**:
|
||||||
|
```
|
||||||
|
跟踪误差(TE) = STDEV(每日跟踪偏离度) × √252
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、计算步骤
|
||||||
|
|
||||||
|
### 2.1 数据准备
|
||||||
|
|
||||||
|
| 数据类型 | 字段 | 来源 | 说明 |
|
||||||
|
|---------|------|------|------|
|
||||||
|
| ETF单位净值 | `unit_nav` | Tushare `fund_nav` | 必须用单位净值,不能用累计净值 |
|
||||||
|
| 基准收盘价 | `close` | Tushare `index_daily` / `fut_daily` / Flask API | 根据标的类型选择数据源 |
|
||||||
|
|
||||||
|
### 2.2 计算流程
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤1: 获取ETF单位净值序列
|
||||||
|
NAV[t], NAV[t-1], NAV[t-2], ...
|
||||||
|
|
||||||
|
步骤2: 获取基准收盘价序列
|
||||||
|
Index[t], Index[t-1], Index[t-2], ...
|
||||||
|
|
||||||
|
步骤3: 计算ETF日收益率
|
||||||
|
ETF_ret[t] = (NAV[t] - NAV[t-1]) / NAV[t-1]
|
||||||
|
|
||||||
|
步骤4: 计算基准日收益率
|
||||||
|
Index_ret[t] = (Index[t] - Index[t-1]) / Index[t-1]
|
||||||
|
|
||||||
|
步骤5: 计算每日跟踪偏离度
|
||||||
|
Deviation[t] = ETF_ret[t] - Index_ret[t]
|
||||||
|
|
||||||
|
步骤6: 计算偏离度标准差
|
||||||
|
Std = STDEV(Deviation序列)
|
||||||
|
|
||||||
|
步骤7: 年化处理
|
||||||
|
TE = Std × √252
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 Python代码示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
def calculate_tracking_error(etf_nav: pd.Series, benchmark_close: pd.Series) -> dict:
|
||||||
|
"""
|
||||||
|
计算ETF跟踪误差
|
||||||
|
|
||||||
|
Args:
|
||||||
|
etf_nav: ETF单位净值序列(index=date, value=unit_nav)
|
||||||
|
benchmark_close: 基准收盘价序列(index=date, value=close)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含跟踪误差、R²、相关系数等指标
|
||||||
|
"""
|
||||||
|
# 计算收益率
|
||||||
|
etf_ret = etf_nav.pct_change().dropna()
|
||||||
|
bench_ret = benchmark_close.pct_change().dropna()
|
||||||
|
|
||||||
|
# 对齐日期
|
||||||
|
common = etf_ret.index.intersection(bench_ret.index)
|
||||||
|
if len(common) < 20:
|
||||||
|
return None
|
||||||
|
|
||||||
|
e = etf_ret.loc[common]
|
||||||
|
b = bench_ret.loc[common]
|
||||||
|
|
||||||
|
# 每日偏离度
|
||||||
|
daily_deviation = e - b
|
||||||
|
|
||||||
|
# 跟踪误差 = 标准差 × √252
|
||||||
|
tracking_error = daily_deviation.std() * np.sqrt(252)
|
||||||
|
|
||||||
|
# 其他指标
|
||||||
|
correlation = e.corr(b)
|
||||||
|
r_squared = correlation ** 2
|
||||||
|
|
||||||
|
# 累计收益
|
||||||
|
etf_cum = (1 + e).prod() - 1
|
||||||
|
bench_cum = (1 + b).prod() - 1
|
||||||
|
excess = etf_cum - bench_cum
|
||||||
|
|
||||||
|
return {
|
||||||
|
'annual_tracking_error': round(tracking_error * 100, 4), # %
|
||||||
|
'correlation': round(correlation, 6),
|
||||||
|
'r_squared': round(r_squared, 6),
|
||||||
|
'etf_cum_return': round(etf_cum * 100, 2), # %
|
||||||
|
'benchmark_cum_return': round(bench_cum * 100, 2), # %
|
||||||
|
'excess_return': round(excess * 100, 2), # %
|
||||||
|
'common_days': len(common),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、基准数据来源
|
||||||
|
|
||||||
|
### 3.1 数据源选择
|
||||||
|
|
||||||
|
| 标的类型 | 示例 | 基准来源 | Tushare接口 | 数据可用性 |
|
||||||
|
|---------|------|---------|------------|-----------|
|
||||||
|
| A股指数 | 创业板指、红利低波 | 指数收盘价 | `index_daily` | ✅ 完整 |
|
||||||
|
| 商品期货 | 黄金、有色金属 | 期货主力合约 | `fut_daily` | ✅ 完整 |
|
||||||
|
| 海外指数 | 纳指、恒生、日经、DAX | 指数收盘价 | Flask API (yfinance) | ✅ 完整 |
|
||||||
|
|
||||||
|
### 3.2 接口调用示例
|
||||||
|
|
||||||
|
```python
|
||||||
|
# A股指数
|
||||||
|
index_data = pro.index_daily(
|
||||||
|
ts_code='399006.SZ',
|
||||||
|
start_date='20250601',
|
||||||
|
end_date='20260619'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 商品期货
|
||||||
|
futures_data = pro.fut_daily(
|
||||||
|
ts_code='AU.SHF',
|
||||||
|
start_date='20250601',
|
||||||
|
end_date='20260619'
|
||||||
|
)
|
||||||
|
|
||||||
|
# 海外指数(通过Flask API)
|
||||||
|
from datasource.flask_api_source import FlaskAPIDataSource
|
||||||
|
flask_source = FlaskAPIDataSource()
|
||||||
|
index_data = flask_source.fetch('^NDX', '2025-06-01', '2026-06-19')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、关键注意事项
|
||||||
|
|
||||||
|
### 4.1 必须使用单位净值(unit_nav)
|
||||||
|
|
||||||
|
| 净值类型 | 含义 | 是否可用 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| **单位净值** (unit_nav) | 当前每份基金的实际价值 | ✅ **必须用这个** |
|
||||||
|
| 累计净值 (accum_nav) | 单位净值 + 历史分红 | ❌ 会虚高规模 |
|
||||||
|
|
||||||
|
**原因**:累计净值包含了历史分红再投资,会导致规模计算偏大。
|
||||||
|
|
||||||
|
### 4.2 必须使用标的指数做基准
|
||||||
|
|
||||||
|
| 基准类型 | 计算结果 | 说明 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| **标的指数** | 真实跟踪误差 | ✅ 反映基金经理追踪能力 |
|
||||||
|
| 另一只ETF价格 | 价格一致性 | ❌ 包含溢价率波动噪声 |
|
||||||
|
|
||||||
|
### 4.3 年化因子
|
||||||
|
|
||||||
|
- 使用 **√252**(假设一年252个交易日)
|
||||||
|
- 如果使用月度数据,则用 **√12**
|
||||||
|
- 如果使用周度数据,则用 **√52**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、校验结果
|
||||||
|
|
||||||
|
### 5.1 与天天基金数据对比
|
||||||
|
|
||||||
|
我们用 Tushare 计算的创业板指 ETF 跟踪误差 vs 天天基金官方数据:
|
||||||
|
|
||||||
|
| ETF代码 | Tushare TE | 天天基金 TE | 差异 |
|
||||||
|
|--------|-----------|------------|------|
|
||||||
|
| 159948.SZ | 0.3302% | 0.32% | +0.0102% |
|
||||||
|
| 159952.SZ | 0.3559% | 0.35% | +0.0059% |
|
||||||
|
| 159205.SZ | 0.3698% | 0.36% | +0.0098% |
|
||||||
|
| 159977.SZ | 0.3727% | 0.36% | +0.0127% |
|
||||||
|
|
||||||
|
**平均差异:+0.0091%** → 高度一致
|
||||||
|
|
||||||
|
### 5.2 结论
|
||||||
|
|
||||||
|
- Tushare 数据计算的跟踪误差与天天基金官方数据**高度一致**
|
||||||
|
- 验证了计算方法的正确性
|
||||||
|
- 可用于日常跟踪误差监控
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、完整计算脚本
|
||||||
|
|
||||||
|
参考文件:`rotation/tracking_error_full.py`
|
||||||
|
|
||||||
|
### 6.1 主要功能
|
||||||
|
|
||||||
|
- 覆盖轮动策略标的池全部10个标的
|
||||||
|
- 自动选择合适的数据源(Tushare指数/期货/Flask API)
|
||||||
|
- 批量获取ETF净值数据
|
||||||
|
- 计算跟踪误差并排序
|
||||||
|
- 与天天基金数据对比校验
|
||||||
|
|
||||||
|
### 6.2 运行方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /Users/aszer/code/etf
|
||||||
|
python3 rotation/tracking_error_full.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 输出结果
|
||||||
|
|
||||||
|
- JSON文件:`rotation/results/tracking_error_full.json`
|
||||||
|
- 包含每个标的下所有ETF的跟踪误差、R²、超额收益等指标
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、相关文档
|
||||||
|
|
||||||
|
- [ETF竞品分析报告](./etf_competitor_analysis_report.md)
|
||||||
|
- [跟踪误差校验报告](./tracking_error_validation_report.md)
|
||||||
|
- [ETF数据源说明](../datasource/README.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、更新日志
|
||||||
|
|
||||||
|
| 版本 | 日期 | 变更内容 |
|
||||||
|
|------|------|---------|
|
||||||
|
| v1.0 | 2026-06-19 | 初始版本,包含完整计算方法和校验结果 |
|
||||||
@@ -70,8 +70,8 @@ results = client.batch_ohlcv(
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. API 文档 (flask_api_README.md)
|
### 4. API 文档 (Flask_API接口说明.md)
|
||||||
**文件**: `docs/flask_api_README.md` (405行)
|
**文件**: `docs/Flask_API接口说明.md` (405行)
|
||||||
|
|
||||||
**内容**:
|
**内容**:
|
||||||
- ✅ 快速开始指南
|
- ✅ 快速开始指南
|
||||||
@@ -251,8 +251,8 @@ etf/
|
|||||||
│ ├── test_universal_fetcher.py
|
│ ├── test_universal_fetcher.py
|
||||||
│ └── test_ssh_tunnel.py
|
│ └── test_ssh_tunnel.py
|
||||||
├── docs/
|
├── docs/
|
||||||
│ ├── flask_api_README.md # API 文档 (405行)
|
│ ├── Flask_API接口说明.md # API 文档 (405行)
|
||||||
│ ├── universal_fetcher_README.md
|
│ ├── 通用数据源说明.md
|
||||||
│ └── ...
|
│ └── ...
|
||||||
├── start_flask_server.sh # 启动脚本 (128行)
|
├── start_flask_server.sh # 启动脚本 (128行)
|
||||||
└── hk_ecs.pem # SSH 私钥
|
└── hk_ecs.pem # SSH 私钥
|
||||||
@@ -344,7 +344,7 @@ etf/
|
|||||||
- 健康检查: http://localhost:5000/health
|
- 健康检查: http://localhost:5000/health
|
||||||
|
|
||||||
查看详细文档:
|
查看详细文档:
|
||||||
- [API 文档](./flask_api_README.md)
|
- [API 文档](./Flask_API接口说明.md)
|
||||||
- [客户端示例](../examples/flask_api_client.py)
|
- [客户端示例](../examples/flask_api_client.py)
|
||||||
|
|
||||||
## 🎉 总结
|
## 🎉 总结
|
||||||
470
docs/experiments/007_momentum_window_optimization.md
Normal file
470
docs/experiments/007_momentum_window_optimization.md
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
# 实验 007:动量因子回看窗口优化研究
|
||||||
|
|
||||||
|
**实验日期**:2026-06-12
|
||||||
|
**实验目标**:研究动量因子回看窗口(n_days)的选择方法,评估多周期融合对策略表现的影响
|
||||||
|
**实验结论**:多周期融合不适用于本策略,维持 25 天单窗口
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、问题背景
|
||||||
|
|
||||||
|
### 1.1 当前配置
|
||||||
|
|
||||||
|
策略使用 `slope_r2` 因子,全局统一 25 天回看窗口:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
factor:
|
||||||
|
n_days: 25
|
||||||
|
type: slope_r2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 发现的问题
|
||||||
|
|
||||||
|
从回测数据的因子分布分析发现:
|
||||||
|
|
||||||
|
| 标的 | IQR(波动代理) | 中位数 | 正得分% | 特征 |
|
||||||
|
|------|----------------|--------|---------|------|
|
||||||
|
| 创业板指 | 28.23 | 0.00 | 51.0% | 高波动,趋势间歇性强 |
|
||||||
|
| 恒生科技 | 20.71 | -0.01 | 44.0% | 高波动,趋势弱 |
|
||||||
|
| 纳指100 | 20.10 | 4.31 | 67.4% | 高波动,趋势强 |
|
||||||
|
| 短债 | 0.38 | 0.68 | 97.9% | 低波动,几乎无趋势 |
|
||||||
|
| 黄金 | 13.39 | 0.77 | 60.7% | 中等波动,趋势持续 |
|
||||||
|
|
||||||
|
**核心问题**:不同资产的趋势周期差异很大,统一 25 天窗口是否合理?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、学界与业界调研
|
||||||
|
|
||||||
|
### 2.1 经典文献的标准做法
|
||||||
|
|
||||||
|
#### 横截面动量(Cross-Sectional Momentum)
|
||||||
|
|
||||||
|
**Jegadeesh & Titman (1993)** 开创性研究:
|
||||||
|
- **标准窗口**:12-1 月(过去12个月收益,跳过最近1个月)
|
||||||
|
- **金融语义**:跳过最近1个月是因为存在短期反转效应(1周-1个月),而中期(3-12个月)存在动量效应
|
||||||
|
- **原理**:信息扩散慢 → 价格对新信息反应不足 → 形成中期趋势
|
||||||
|
|
||||||
|
**Asness, Moskowitz & Pedersen (2013)** "Value and Momentum Everywhere":
|
||||||
|
- 股票:12-1 月
|
||||||
|
- 债券:12-1 月或 6-1 月
|
||||||
|
- 商品:12 月(不跳过,因为商品市场短期反转弱)
|
||||||
|
- 货币:12 月
|
||||||
|
- **核心观点**:不同资产类别的最优窗口不同,但 12 月是一个稳健的起点
|
||||||
|
|
||||||
|
#### 时间序列动量(Time-Series Momentum / TSMOM)
|
||||||
|
|
||||||
|
**Moskowitz, Ooi & Pedersen (2012)**:
|
||||||
|
- **标准窗口**:12 月(用于期货)
|
||||||
|
- **金融语义**:TSMOM 关注资产自身的绝对收益,不与其他资产比较
|
||||||
|
- **关键发现**:1-12 月窗口都有效,但 12 月最稳健
|
||||||
|
- **波动率调整**:用实现波动率标准化头寸规模,使得不同资产可比
|
||||||
|
|
||||||
|
### 2.2 窗口选择的金融语义
|
||||||
|
|
||||||
|
#### 不同窗口对应的市场微观结构
|
||||||
|
|
||||||
|
| 窗口长度 | 捕捉的效应 | 金融解释 | 风险 |
|
||||||
|
|---------|-----------|---------|------|
|
||||||
|
| 1周-1月 | 短期反转 | 流动性冲击、过度反应修正 | 高换手、交易成本 |
|
||||||
|
| 1-3月 | 早期动量 | 信息扩散初期、盈余公告后漂移 | 容易被打断 |
|
||||||
|
| 3-12月 | 经典动量 | 信息扩散慢、机构调仓周期 | 最稳健 |
|
||||||
|
| 12-24月 | 长期动量 | 经济周期、企业基本面变化 | 均值回归开始显现 |
|
||||||
|
| >24月 | 长期反转 | 估值回归、经济周期反转 | 动量效应消失 |
|
||||||
|
|
||||||
|
#### 不同资产类别的特征周期
|
||||||
|
|
||||||
|
| 资产类别 | 特征周期 | 推荐窗口 | 理由 |
|
||||||
|
|---------|---------|---------|------|
|
||||||
|
| **股票指数** | 季度财报+机构调仓 | 6-12月 | 信息扩散慢,机构季度调仓 |
|
||||||
|
| **债券** | 央行政策周期 | 3-6月 | 利率变化快,久期短 |
|
||||||
|
| **商品** | 供需周期+季节性 | 6-12月 | 供需调整慢,但有季节性 |
|
||||||
|
| **货币** | 利差+央行政策 | 3-6月 | 政策变化快 |
|
||||||
|
|
||||||
|
### 2.3 避免过拟合的原则
|
||||||
|
|
||||||
|
#### 先验选择 vs 数据挖掘
|
||||||
|
|
||||||
|
**过拟合的做法**:
|
||||||
|
```python
|
||||||
|
# 在历史数据上测试 5, 10, 15, 20, 25, 30... 天,选最好的
|
||||||
|
for window in [5, 10, 15, 20, 25, 30, 60, 120]:
|
||||||
|
backtest(window)
|
||||||
|
best_window = argmax(results) # 过拟合!
|
||||||
|
```
|
||||||
|
|
||||||
|
**有金融语义的做法**:
|
||||||
|
```python
|
||||||
|
# 基于资产类别选择窗口
|
||||||
|
window_map = {
|
||||||
|
'equity_index': 252, # 12月(252交易日)
|
||||||
|
'bond': 126, # 6月
|
||||||
|
'commodity': 252, # 12月
|
||||||
|
'currency': 126, # 6月
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 稳健性检验原则
|
||||||
|
|
||||||
|
**学界推荐的做法**:
|
||||||
|
1. **选择有理论支撑的窗口**:12月、6月、3月是标准选择
|
||||||
|
2. **测试邻域稳健性**:如果 12 月好,11 月和 13 月也应该不差
|
||||||
|
3. **多窗口平均**:用 3-12 月的多个窗口取平均,降低单窗口风险
|
||||||
|
4. **样本外验证**:在不同时间段、不同市场验证
|
||||||
|
|
||||||
|
**AQR 的实践建议**:
|
||||||
|
- 不要优化到极端值(如 17 天、23 天)
|
||||||
|
- 选择"足够好"的标准窗口(如 252 天而非 247 天)
|
||||||
|
- 关注经济解释而非统计显著性
|
||||||
|
|
||||||
|
### 2.4 多窗口融合方法
|
||||||
|
|
||||||
|
#### 1. 等权平均(Simple Ensemble)
|
||||||
|
|
||||||
|
```python
|
||||||
|
windows = [63, 126, 252] # 3月、6月、12月
|
||||||
|
momentum = mean([return(p, w) for w in windows])
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**:
|
||||||
|
- 降低单窗口风险
|
||||||
|
- 捕捉不同周期的趋势
|
||||||
|
- 无需优化参数
|
||||||
|
|
||||||
|
**缺点**:
|
||||||
|
- 等权可能不合理
|
||||||
|
- 可能引入噪音窗口
|
||||||
|
|
||||||
|
#### 2. 波动率加权(Volatility-Weighted)
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 波动率低的窗口权重更高(更稳定)
|
||||||
|
weights = 1 / vol(window_i)
|
||||||
|
momentum = weighted_mean(momentum_i, weights)
|
||||||
|
```
|
||||||
|
|
||||||
|
**金融语义**:低波动窗口的信号更可靠
|
||||||
|
|
||||||
|
#### 3. 自适应窗口(Adaptive Window)
|
||||||
|
|
||||||
|
**基于波动率的自适应**:
|
||||||
|
```python
|
||||||
|
# 高波动时缩短窗口(快速反应),低波动时延长(过滤噪音)
|
||||||
|
if realized_vol > threshold:
|
||||||
|
window = 63 # 3月
|
||||||
|
else:
|
||||||
|
window = 252 # 12月
|
||||||
|
```
|
||||||
|
|
||||||
|
**基于机制转换(Regime Switching)**:
|
||||||
|
```python
|
||||||
|
# 用 HMM 识别市场状态
|
||||||
|
regime = detect_regime(market_data)
|
||||||
|
if regime == 'trending':
|
||||||
|
window = 252 # 趋势市用长窗口
|
||||||
|
elif regime == 'mean_reverting':
|
||||||
|
window = 21 # 震荡市用短窗口或反转
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 多周期融合为什么有效?
|
||||||
|
|
||||||
|
#### 1. 信息扩散有多个时间尺度
|
||||||
|
|
||||||
|
- **短期(1-4周)**:流动性冲击、技术性买卖、短期情绪(噪音)
|
||||||
|
- **中期(1-6月)**:盈余公告、行业数据、政策变化(信息扩散)
|
||||||
|
- **长期(6-12月)**:经济周期转换、产业趋势、估值重定价
|
||||||
|
|
||||||
|
单一窗口只能捕捉一个尺度的信息。多窗口融合等于同时监听多个信息频段。
|
||||||
|
|
||||||
|
#### 2. 市场参与者的决策周期不同
|
||||||
|
|
||||||
|
| 参与者 | 决策周期 | 影响的价格趋势 |
|
||||||
|
|--------|---------|--------------|
|
||||||
|
| 高频/量化 | 天-周 | 短期噪音 |
|
||||||
|
| 共同基金 | 月-季 | 中期动量 |
|
||||||
|
| 养老金/保险 | 季-年 | 长期趋势 |
|
||||||
|
| 主权基金 | 年+ | 结构性变化 |
|
||||||
|
|
||||||
|
当多个周期的信号一致时,意味着不同时间维度的市场参与者方向一致——这是最强的趋势确认。
|
||||||
|
|
||||||
|
#### 3. 统计角度:偏差-方差权衡
|
||||||
|
|
||||||
|
- **短窗口**:低偏差(快速捕捉趋势变化),高方差(容易被噪音干扰)
|
||||||
|
- **长窗口**:高偏差(趋势转折时反应慢),低方差(噪音被平滑掉)
|
||||||
|
|
||||||
|
多窗口融合相当于对偏差-方差做了平均,短窗口提供灵敏度,长窗口提供稳定性。
|
||||||
|
|
||||||
|
#### 4. 信号处理的类比
|
||||||
|
|
||||||
|
把价格序列想象成一个信号,不同窗口就是不同频率的带通滤波器:
|
||||||
|
|
||||||
|
```
|
||||||
|
价格信号 = 高频噪音 + 中期趋势 + 长期趋势 + 周期性波动
|
||||||
|
|
||||||
|
25天窗口 → 带通滤波器:主要透过高频成分
|
||||||
|
126天窗口 → 带通滤波器:主要透过中期成分
|
||||||
|
252天窗口 → 低通滤波器:只保留长期趋势
|
||||||
|
```
|
||||||
|
|
||||||
|
多个滤波器融合 = 宽频带接收,信息更完整。
|
||||||
|
|
||||||
|
### 2.6 多周期融合对 slope_r2 偏好的影响
|
||||||
|
|
||||||
|
slope_r2 真正偏好的是**趋势性波动高的资产**(高波动+有方向),而不是单纯的高波动。
|
||||||
|
|
||||||
|
多周期融合的预期影响:
|
||||||
|
|
||||||
|
| 资产类型 | 单窗口(25天) | 多周期融合 | 变化方向 |
|
||||||
|
|---------|-------------|-----------|---------|
|
||||||
|
| 高波动+持续趋势(纳指) | 高分 | 高分 | 不变 |
|
||||||
|
| 高波动+间歇趋势(创业板) | 不稳定高分 | 中等分 | **下降** |
|
||||||
|
| 低波动+持续趋势(黄金) | 中等分 | 中等偏高分 | **上升** |
|
||||||
|
| 低波动+无趋势(短债) | 低分 | 低分 | 不变 |
|
||||||
|
| 高波动+无趋势(恒生科技) | 低分 | 低分 | 不变 |
|
||||||
|
|
||||||
|
**核心预期**:融合会让 slope_r2 的偏好从"高波动+趋势性"转向"持续性趋势"——不管波动高低,只要趋势持续就得分高。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、实验设计
|
||||||
|
|
||||||
|
### 3.1 实验一:IDM 信息离散动量融合
|
||||||
|
|
||||||
|
#### 方法
|
||||||
|
|
||||||
|
**IDM(Information Dispersal Momentum)**:正收益天数占比,衡量上涨的持续性
|
||||||
|
|
||||||
|
```python
|
||||||
|
def info_dispersal_momentum(prices: np.ndarray) -> float:
|
||||||
|
returns = np.diff(prices)
|
||||||
|
positive_days = np.sum(returns > 0)
|
||||||
|
return positive_days / len(returns)
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式一:乘法融合**
|
||||||
|
```python
|
||||||
|
def slope_r2_idm_score(prices: np.ndarray) -> float:
|
||||||
|
sr2 = slope_r2_score(prices)
|
||||||
|
idm = info_dispersal_momentum(prices)
|
||||||
|
return sr2 * idm # IDM 作为折扣系数
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式三:阈值过滤**
|
||||||
|
```python
|
||||||
|
def slope_r2_idm_filter_score(prices: np.ndarray, threshold: float = 0.5) -> float:
|
||||||
|
idm = info_dispersal_momentum(prices)
|
||||||
|
if idm < threshold:
|
||||||
|
return 0.0 # 上涨天数不足阈值则清零
|
||||||
|
return slope_r2_score(prices)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实验配置
|
||||||
|
|
||||||
|
- **方式一**:`type: slope_r2_idm`
|
||||||
|
- **方式三**:`type: slope_r2_idm_filter`,测试阈值 0.4/0.5/0.6
|
||||||
|
|
||||||
|
### 3.2 实验二:多周期融合
|
||||||
|
|
||||||
|
#### 方法
|
||||||
|
|
||||||
|
```python
|
||||||
|
def slope_r2_ensemble_score(prices: np.ndarray, windows: list = None) -> float:
|
||||||
|
if windows is None:
|
||||||
|
windows = [63, 126, 252] # 3月、6月、12月
|
||||||
|
|
||||||
|
scores = []
|
||||||
|
for w in windows:
|
||||||
|
if len(prices) >= w:
|
||||||
|
window_prices = prices[-w:]
|
||||||
|
score = slope_r2_score(window_prices)
|
||||||
|
scores.append(score)
|
||||||
|
|
||||||
|
return sum(scores) / len(scores) if scores else 0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 实验配置
|
||||||
|
|
||||||
|
- **配置**:`type: slope_r2_ensemble`
|
||||||
|
- **窗口**:63/126/252 天(3月/6月/12月)
|
||||||
|
- **数据预加载**:504 天(2倍最大窗口)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、实验结果
|
||||||
|
|
||||||
|
### 4.1 实验一:IDM 融合结果
|
||||||
|
|
||||||
|
#### 方式一:乘法融合
|
||||||
|
|
||||||
|
| 指标 | slope_r2 (baseline) | slope_r2_idm | 变化 |
|
||||||
|
|------|---------------------|--------------|------|
|
||||||
|
| 总收益 | 288.30% | 296.55% | +8.25% |
|
||||||
|
| 年化收益 | 24.61% | 25.03% | +0.42% |
|
||||||
|
| 最大回撤 | -16.27% | -16.19% | 略改善 |
|
||||||
|
| Sharpe | 1.17 | 1.20 | +0.03 |
|
||||||
|
| Calmar | 1.51 | 1.55 | +0.04 |
|
||||||
|
| 胜率 | 53.74% | 54.48% | +0.74% |
|
||||||
|
| 调仓次数 | 363 | 374 | +11 |
|
||||||
|
|
||||||
|
**结论**:方式一(乘法融合)全面小幅优于 baseline。
|
||||||
|
|
||||||
|
#### 方式三:阈值过滤
|
||||||
|
|
||||||
|
| 阈值 | 总收益 | 年化 | 最大回撤 | Sharpe | Calmar | 胜率 |
|
||||||
|
|------|--------|------|----------|--------|--------|------|
|
||||||
|
| 0.4 | 297.88% | 25.10% | -16.27% | 1.19 | 1.54 | 53.87% |
|
||||||
|
| 0.5 | 205.38% | 19.85% | -17.90% | 1.00 | 1.11 | 53.35% |
|
||||||
|
| 0.6 | 69.75% | 8.96% | -24.77% | 0.58 | 0.36 | 56.87% |
|
||||||
|
|
||||||
|
**结论**:方式三(过滤器)阈值敏感,0.5 和 0.6 明显变差,容易过拟合。
|
||||||
|
|
||||||
|
### 4.2 实验二:多周期融合结果
|
||||||
|
|
||||||
|
#### 绩效对比
|
||||||
|
|
||||||
|
| 指标 | slope_r2 (25天) | slope_r2_ensemble (63/126/252) | 变化 |
|
||||||
|
|------|----------------|-------------------------------|------|
|
||||||
|
| 总收益 | 288.30% | 182.82% | **-105.48%** |
|
||||||
|
| 年化收益 | 24.61% | 18.36% | **-6.25%** |
|
||||||
|
| 最大回撤 | -16.27% | -21.61% | **恶化 5.34%** |
|
||||||
|
| Sharpe | 1.17 | 0.96 | **-0.21** |
|
||||||
|
| Calmar | 1.51 | 0.85 | **-0.66** |
|
||||||
|
| 胜率 | 53.74% | 55.47% | +1.73% |
|
||||||
|
| 调仓次数 | 363 | 167 | **-196** |
|
||||||
|
|
||||||
|
#### 持仓频率变化
|
||||||
|
|
||||||
|
| 标的 | baseline 占比 | ensemble 占比 | 变化 | 资产类型 |
|
||||||
|
|------|--------------|--------------|------|---------|
|
||||||
|
| 纳指100 | 44.7% | 53.3% | **+8.6%** | 高波动+持续趋势 |
|
||||||
|
| 黄金 | 21.0% | 35.8% | **+14.8%** | 低波动+持续趋势 |
|
||||||
|
| 创业板指 | 29.9% | 40.2% | **+10.3%** | 高波动+间歇趋势 |
|
||||||
|
| 日经225 | 31.0% | 37.7% | +6.7% | 中波动+持续趋势 |
|
||||||
|
| 德国DAX | 27.8% | 33.1% | +5.3% | 中波动+持续趋势 |
|
||||||
|
| **短债指数** | **32.0%** | **16.6%** | **-15.4%** | 防御填充 |
|
||||||
|
| 红利低波 | 24.3% | 11.9% | **-12.4%** | 低波动+持续趋势 |
|
||||||
|
| 有色金属 | 18.1% | 12.8% | -5.3% | 高波动+周期趋势 |
|
||||||
|
|
||||||
|
#### 与预期对比
|
||||||
|
|
||||||
|
| 预期 | 实际 | 符合? |
|
||||||
|
|------|------|--------|
|
||||||
|
| 纳指100 和黄金占比上升 | 纳指+8.6%,黄金+14.8% | ✓ 符合 |
|
||||||
|
| 红利低波占比上升 | 红利低波-12.4% | ✗ 不符合 |
|
||||||
|
| 创业板指占比下降 | 创业板指+10.3% | ✗ 不符合 |
|
||||||
|
| 整体表现改善 | 收益降6%,回撤增5% | ✗ 不符合 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、结论与分析
|
||||||
|
|
||||||
|
### 5.1 IDM 融合结论
|
||||||
|
|
||||||
|
**推荐方案**:方式一(乘法融合)
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 全面小幅优于 baseline,无需调参
|
||||||
|
2. IDM 作为折扣系数,逻辑简洁
|
||||||
|
3. 过滤方式(方式三)阈值敏感,容易过拟合
|
||||||
|
|
||||||
|
**保留代码**:
|
||||||
|
- `slope_r2_idm_score` 函数已实现
|
||||||
|
- `FactorType.SLOPE_R2_IDM` 枚举已添加
|
||||||
|
- 可通过配置 `type: slope_r2_idm` 启用
|
||||||
|
|
||||||
|
### 5.2 多周期融合结论
|
||||||
|
|
||||||
|
**结论**:不适用于本策略
|
||||||
|
|
||||||
|
**原因分析**:
|
||||||
|
|
||||||
|
1. **长窗口反应太慢**:252 天窗口在趋势转折时严重滞后。2022 年全球熊市、2024 年风格切换时,ensemble 无法及时退出
|
||||||
|
|
||||||
|
2. **调仓次数骤降**:从 363 次降到 167 次,说明信号太稳定了,错过了很多轮动机会。这个策略的核心价值就是**轮动**,过于稳定的信号反而不利
|
||||||
|
|
||||||
|
3. **短债填充减少**:从 32% 降到 16.6%,说明 ensemble 在弱势市场中也倾向于持有风险资产(因为长窗口记忆了之前的上涨趋势),导致回撤增大
|
||||||
|
|
||||||
|
4. **创业板指上升的原因**:2024-2025 年创业板有持续上涨趋势,ensemble 的长窗口恰好捕捉到了这个趋势,但这不是"持续性偏好",而是"恰好匹配"
|
||||||
|
|
||||||
|
**核心问题**:
|
||||||
|
|
||||||
|
这个策略的 alpha 来源是**中短期轮动**(25 天窗口),不是长期趋势跟踪。ensemble 把因子变成了半趋势跟踪因子,与策略的核心逻辑冲突。
|
||||||
|
|
||||||
|
### 5.3 最终决策
|
||||||
|
|
||||||
|
**维持现有配置**:
|
||||||
|
```yaml
|
||||||
|
factor:
|
||||||
|
n_days: 25
|
||||||
|
type: slope_r2
|
||||||
|
```
|
||||||
|
|
||||||
|
**理由**:
|
||||||
|
1. 25 天窗口与策略的轮动逻辑匹配
|
||||||
|
2. 多周期融合与策略核心逻辑冲突
|
||||||
|
3. IDM 融合虽然有效,但提升有限,暂不启用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、参考资料
|
||||||
|
|
||||||
|
### 学术文献
|
||||||
|
|
||||||
|
1. **Jegadeesh, N., & Titman, S. (1993)**. Returns to Buying Winners and Selling Losers: Implications for Stock Market Efficiency. *Journal of Finance*, 48(1), 65-91.
|
||||||
|
|
||||||
|
2. **Asness, C. S., Moskowitz, T. J., & Pedersen, L. H. (2013)**. Value and Momentum Everywhere. *Journal of Finance*, 68(3), 929-985.
|
||||||
|
|
||||||
|
3. **Moskowitz, T. J., Ooi, Y. H., & Pedersen, L. H. (2012)**. Time Series Momentum. *Journal of Financial Economics*, 104(2), 228-250.
|
||||||
|
|
||||||
|
### 业界实践
|
||||||
|
|
||||||
|
- [Momentum Factor Investing: 30 years of Out of Sample Data](https://alphaarchitect.com/momentum-factor-investing-30-years-of-out-of-sample-data/)
|
||||||
|
- [Systematic Trend-Following with Adaptive Portfolio Construction](https://arxiv.org/html/2602.11708v1)
|
||||||
|
- [Value and Momentum Everywhere - AQR Capital Management](https://www.aqr.com/Insights/Research/Journal-Article/Value-and-Momentum-Everywhere)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、附录:代码实现
|
||||||
|
|
||||||
|
### 7.1 IDM 融合因子
|
||||||
|
|
||||||
|
```python
|
||||||
|
def info_dispersal_momentum(prices: np.ndarray) -> float:
|
||||||
|
"""Information Dispersal Momentum (IDM): 正收益天数占比"""
|
||||||
|
if len(prices) < 2:
|
||||||
|
return 0.0
|
||||||
|
returns = np.diff(prices)
|
||||||
|
positive_days = np.sum(returns > 0)
|
||||||
|
return positive_days / len(returns)
|
||||||
|
|
||||||
|
|
||||||
|
def slope_r2_idm_score(prices: np.ndarray) -> float:
|
||||||
|
"""Slope * R² * IDM: 趋势强度 × 拟合质量 × 上涨持续性"""
|
||||||
|
sr2 = slope_r2_score(prices)
|
||||||
|
idm = info_dispersal_momentum(prices)
|
||||||
|
return sr2 * idm
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 多周期融合因子
|
||||||
|
|
||||||
|
```python
|
||||||
|
def slope_r2_ensemble_score(prices: np.ndarray, windows: list = None) -> float:
|
||||||
|
"""多窗口 slope_r2 等权融合"""
|
||||||
|
if windows is None:
|
||||||
|
windows = [63, 126, 252] # 3月、6月、12月
|
||||||
|
|
||||||
|
scores = []
|
||||||
|
for w in windows:
|
||||||
|
if len(prices) >= w:
|
||||||
|
window_prices = prices[-w:]
|
||||||
|
score = slope_r2_score(window_prices)
|
||||||
|
scores.append(score)
|
||||||
|
|
||||||
|
return sum(scores) / len(scores) if scores else 0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**文档版本**:v1.0
|
||||||
|
**最后更新**:2026-06-12
|
||||||
|
**实验状态**:已完成
|
||||||
221
docs/experiments/008_execution_delay_impact.md
Normal file
221
docs/experiments/008_execution_delay_impact.md
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
# 实验 008:信号执行延迟对策略收益的影响
|
||||||
|
|
||||||
|
**实验日期**:2026-06-15
|
||||||
|
**实验目标**:量化信号触发后延迟执行对策略收益的影响,评估策略的执行容错度
|
||||||
|
**实验结论**:卖出必须立即执行;买入延迟 1 天可接受(-1.7pp),延迟 2 天以上策略失效
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、问题背景
|
||||||
|
|
||||||
|
### 1.1 当前执行机制
|
||||||
|
|
||||||
|
策略采用 T+1 执行模式:
|
||||||
|
|
||||||
|
- **T 日 9:00**:信号触发,使用 T-1 收盘数据计算动量因子
|
||||||
|
- **T 日 9:30**:开盘执行调仓(卖出 + 买入),使用 T 日 ETF 价格
|
||||||
|
|
||||||
|
信号生成和执行在同一天,延迟为 0。
|
||||||
|
|
||||||
|
### 1.2 测试目的
|
||||||
|
|
||||||
|
实盘中可能存在以下情况导致延迟执行:
|
||||||
|
|
||||||
|
- 人工操作延迟
|
||||||
|
- 系统故障导致未能及时下单
|
||||||
|
- 流动性不足需要分批执行
|
||||||
|
- 跨时区交易的时间差
|
||||||
|
|
||||||
|
需要量化这些延迟对策略的实际影响,评估策略的执行容错度。
|
||||||
|
|
||||||
|
### 1.3 两种延迟模式
|
||||||
|
|
||||||
|
调仓包含两个动作:**卖出**和**买入**。延迟执行有两种理解方式:
|
||||||
|
|
||||||
|
| 模式 | 卖出时机 | 买入时机 | 等待期间状态 |
|
||||||
|
|------|---------|---------|-------------|
|
||||||
|
| 模式 A:买卖都延迟 | 延迟 N 天 | 延迟 N 天 | 继续持有原仓位 |
|
||||||
|
| 模式 B:卖立即、买延迟 | 立即执行 | 延迟 N 天 | 已卖出部分变为现金 |
|
||||||
|
|
||||||
|
两种模式对策略的影响可能不同,需要分别测试对比。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实验设计
|
||||||
|
|
||||||
|
### 2.1 延迟定义
|
||||||
|
|
||||||
|
| 延迟天数 | 含义 |
|
||||||
|
|---------|------|
|
||||||
|
| 0 | 基线(当前逻辑),T 日信号 → T 日开盘执行 |
|
||||||
|
| 1 | 延迟 1 天,T 日信号 → T+1 日开盘执行 |
|
||||||
|
| 2 | 延迟 2 天,T 日信号 → T+2 日开盘执行 |
|
||||||
|
| 3 | 延迟 3 天,T 日信号 → T+3 日开盘执行 |
|
||||||
|
|
||||||
|
### 2.2 实验配置
|
||||||
|
|
||||||
|
- 配置文件:`rotation/config_simple.yaml`
|
||||||
|
- 因子类型:`slope_r2`,`n_days=25`
|
||||||
|
- 回测区间:2020-01-10 ~ 2026-06-15(1555 个交易日)
|
||||||
|
- 标的池:11 资产 / 6 组
|
||||||
|
- 选择数量:3
|
||||||
|
- 权重模式:rank
|
||||||
|
|
||||||
|
### 2.3 实现方式
|
||||||
|
|
||||||
|
在 `SimpleRotationStrategy` 中增加 `execution_delay` 参数,通过修改 `run()` 主循环实现两种延迟模式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、实验 A:买卖都延迟
|
||||||
|
|
||||||
|
### 3.1 逻辑说明
|
||||||
|
|
||||||
|
信号变化后,整个调仓(卖出 + 买入)延迟 N 天执行。等待期间继续持有原仓位。
|
||||||
|
|
||||||
|
### 3.2 实验结果
|
||||||
|
|
||||||
|
| 延迟天数 | 总收益 | 年化收益 | 最大回撤 | Sharpe | Calmar | 胜率 | 调仓次数 |
|
||||||
|
|---------|--------|---------|---------|--------|--------|------|---------|
|
||||||
|
| 0 | 305.02% | 25.44% | -16.27% | 1.20 | 1.56 | 53.83% | 365 |
|
||||||
|
| 1 | 207.29% | 19.95% | -22.29% | 0.97 | 0.90 | 53.99% | 340 |
|
||||||
|
| 2 | 101.57% | 12.03% | -24.53% | 0.65 | 0.49 | 53.57% | 314 |
|
||||||
|
| 3 | 141.29% | 15.34% | -29.03% | 0.77 | 0.53 | 53.80% | 275 |
|
||||||
|
|
||||||
|
### 3.3 NAV 走势对比(关键节点)
|
||||||
|
|
||||||
|
| 交易日 | delay=0 | delay=1 | delay=2 | delay=3 |
|
||||||
|
|--------|---------|---------|---------|---------|
|
||||||
|
| 100 | 1.0785 | 1.0205 | 1.0287 | 1.1477 |
|
||||||
|
| 300 | 1.2462 | 1.1517 | 1.1148 | 1.3979 |
|
||||||
|
| 500 | 1.3847 | 1.2552 | 1.1666 | 1.4580 |
|
||||||
|
| 700 | 1.7195 | 1.4321 | 1.2339 | 1.4492 |
|
||||||
|
| 1000 | 1.8482 | 1.5281 | 1.3189 | 1.5750 |
|
||||||
|
| 1200 | 2.0596 | 1.7315 | 1.3590 | 1.7015 |
|
||||||
|
| 1555 | 4.0563 | 3.0775 | 2.0187 | 2.4165 |
|
||||||
|
|
||||||
|
### 3.4 分析
|
||||||
|
|
||||||
|
**收益衰减规律**:
|
||||||
|
|
||||||
|
```
|
||||||
|
delay 0 → 1:年化 -5.5pp(-21.6%)
|
||||||
|
delay 1 → 2:年化 -7.9pp(-39.7%)
|
||||||
|
delay 2 → 3:年化 +3.3pp(+27.4%,非单调回升)
|
||||||
|
```
|
||||||
|
|
||||||
|
delay=3 略好于 delay=2 是非单调现象,原因是更长的延迟反而减少了无效调仓(275 vs 314 次),在某些场景下偶然捕获了更好的入场点。
|
||||||
|
|
||||||
|
**胜率不变,幅度衰减**:
|
||||||
|
|
||||||
|
胜率在各延迟下几乎恒定(53.6% ~ 54.0%),说明:
|
||||||
|
- 方向判断不受延迟影响 — 同样的信号选出同样的标的
|
||||||
|
- 收益差异来自入场价格 — 延迟越大,动量已走得越远,入场成本越高
|
||||||
|
|
||||||
|
**回撤恶化**:
|
||||||
|
|
||||||
|
| 延迟 | 最大回撤 | 回撤恶化幅度 |
|
||||||
|
|------|---------|------------|
|
||||||
|
| 0 | -16.27% | 基线 |
|
||||||
|
| 1 | -22.29% | +6.0pp |
|
||||||
|
| 2 | -24.53% | +8.3pp |
|
||||||
|
| 3 | -29.03% | +12.8pp |
|
||||||
|
|
||||||
|
延迟导致错过最佳出场时机,风险敞口增大。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、实验 B:卖立即、买延迟
|
||||||
|
|
||||||
|
### 4.1 逻辑说明
|
||||||
|
|
||||||
|
信号变化后:
|
||||||
|
- **卖出立即执行**:当日开盘卖出,资金变为现金
|
||||||
|
- **买入延迟 N 天**:等待期间资金闲置(0 收益),到期后开盘买入
|
||||||
|
|
||||||
|
### 4.2 实验结果
|
||||||
|
|
||||||
|
| 延迟天数 | 总收益 | 年化收益 | 最大回撤 | Sharpe | Calmar | 胜率 | 调仓次数 |
|
||||||
|
|---------|--------|---------|---------|--------|--------|------|---------|
|
||||||
|
| 0 | 305.02% | 25.44% | -16.27% | 1.20 | 1.56 | 53.83% | 365 |
|
||||||
|
| 1 | 271.78% | 23.71% | -15.60% | 1.15 | 1.52 | 53.06% | 365 |
|
||||||
|
| 2 | 96.92% | 11.61% | -21.40% | 0.66 | 0.54 | 50.87% | 653 |
|
||||||
|
| 3 | 4.30% | 0.68% | -35.67% | 0.13 | 0.02 | 48.93% | 890 |
|
||||||
|
|
||||||
|
### 4.3 分析
|
||||||
|
|
||||||
|
**延迟 1 天影响很小**:年化仅损失 1.7pp(25.44% → 23.71%),最大回撤甚至略好(-15.60% vs -16.27%)。因为卖出及时执行,止损不受影响。
|
||||||
|
|
||||||
|
**延迟 2-3 天急剧恶化**:调仓次数从 365 飙升到 653/890,说明策略在频繁卖出又买入之间空转。等待期间仓位不满,资金闲置。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、两种模式对比
|
||||||
|
|
||||||
|
### 5.1 关键指标对比
|
||||||
|
|
||||||
|
| 延迟 | 模式 A(买卖都延迟) | 模式 B(卖立即、买延迟) | 差异 |
|
||||||
|
|------|-------------------|---------------------|------|
|
||||||
|
| 1 天 | 年化 19.95%,回撤 -22.29% | 年化 23.71%,回撤 -15.60% | 模式 B 好 +3.8pp,回撤少 6.7pp |
|
||||||
|
| 2 天 | 年化 12.03%,回撤 -24.53% | 年化 11.61%,回撤 -21.40% | 基本持平 |
|
||||||
|
| 3 天 | 年化 15.34%,回撤 -29.03% | 年化 0.68%,回撤 -35.67% | 模式 A 好 +14.7pp |
|
||||||
|
|
||||||
|
### 5.2 核心差异
|
||||||
|
|
||||||
|
**延迟 1 天时**:模式 B 明显优于模式 A
|
||||||
|
- 模式 B 年化高 3.8pp,回撤少 6.7pp
|
||||||
|
- 原因:及时止损比及时入场更重要
|
||||||
|
|
||||||
|
**延迟 2-3 天时**:模式 B 急剧恶化
|
||||||
|
- 模式 B 调仓次数飙升(653/890 vs 314/275)
|
||||||
|
- 原因:频繁卖出后等待买入,仓位长期不满,资金闲置
|
||||||
|
|
||||||
|
### 5.3 根因分析
|
||||||
|
|
||||||
|
策略的 alpha 来源是 **25 天窗口内的短期动量**。信号触发后的第 1 个交易日 move 是动量最集中的阶段:
|
||||||
|
|
||||||
|
- 信号触发时,标的刚进入强势趋势
|
||||||
|
- 延迟 1 天 = 错过趋势最陡的一段
|
||||||
|
- 延迟 2 天 = 趋势已衰减大半,入场性价比大幅下降
|
||||||
|
|
||||||
|
这与实验 007 的结论一致:策略本质是短期轮动而非长期趋势跟踪。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、结论
|
||||||
|
|
||||||
|
### 6.1 核心结论
|
||||||
|
|
||||||
|
1. **卖出必须立即执行** — 及时止损比及时入场更重要
|
||||||
|
2. **买入延迟 1 天可接受** — 年化仅损失 1.7pp,回撤略好
|
||||||
|
3. **买入延迟 2 天以上策略失效** — 频繁空转,资金闲置
|
||||||
|
4. **胜率不受延迟影响** — 衰减完全来自入场价格劣化
|
||||||
|
|
||||||
|
### 6.2 对实盘的要求
|
||||||
|
|
||||||
|
| 场景 | 可行性 | 预期影响 |
|
||||||
|
|------|--------|---------|
|
||||||
|
| T+1 完整执行(买卖同日) | 最佳 | 基线(年化 25.4%) |
|
||||||
|
| T+1 卖出,T+2 买入 | 可接受 | 年化约 23.7%(-1.7pp) |
|
||||||
|
| T+2 完整执行 | 勉强 | 年化约 12%(-13pp) |
|
||||||
|
| T+3 完整执行 | 不可接受 | 策略失效 |
|
||||||
|
|
||||||
|
**实盘建议**:
|
||||||
|
- 信号生成当日必须完成卖出操作
|
||||||
|
- 如果买入无法当日完成,可延迟 1 天,影响可控
|
||||||
|
- 需要可靠的自动化下单系统
|
||||||
|
- 不建议手动操作执行此策略
|
||||||
|
|
||||||
|
### 6.3 与实验 007 的关联
|
||||||
|
|
||||||
|
实验 007 证明策略的 alpha 来自 25 天短期动量(而非长期趋势)。本实验进一步证实:短期动量的有效窗口不仅在回看端(25 天),在执行端同样敏感 — 信号触发后 1-2 天内的执行质量决定了策略的实际表现。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、代码变更
|
||||||
|
|
||||||
|
- `rotation/simple_rotation.py`:
|
||||||
|
- `__init__` 增加 `execution_delay` 参数
|
||||||
|
- `run()` 主循环支持两种延迟模式:
|
||||||
|
- 模式 A:`pending_holdings` 存储完整调仓,等待期间持有原仓位
|
||||||
|
- 模式 B:`pending_buys` 仅存储买入,卖出立即执行,等待期间资金闲置
|
||||||
479
docs/experiments/009_min_hold_days_optimization.md
Normal file
479
docs/experiments/009_min_hold_days_optimization.md
Normal file
@@ -0,0 +1,479 @@
|
|||||||
|
# 实验 009:最小持有天数(min_hold_days)优化研究
|
||||||
|
|
||||||
|
**实验日期**:2026-06-17
|
||||||
|
**实验目标**:研究最小持有天数约束对策略收益的影响,理解 mhd=3 的金融学原理,探索不依赖参数优化的推导路径
|
||||||
|
**实验结论**:mhd=3 是最优参数,年化收益提升 +0.38pp;该值可通过三条独立路径(信噪比、交易成本、信息扩散)从第一性原理推导得出
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、问题背景
|
||||||
|
|
||||||
|
### 1.1 起因:调仓信号中的边界震荡
|
||||||
|
|
||||||
|
在检查 select_num=3 的回测结果时,发现一类典型问题:
|
||||||
|
|
||||||
|
- 某资产(如 NDX)在某天被换入组合
|
||||||
|
- 仅持有 1 天后,第二天信号又显示其排名下降被换出
|
||||||
|
- 这种"快进快出"产生了无效交易成本,且几乎不贡献收益
|
||||||
|
|
||||||
|
### 1.2 资产换仓频率统计
|
||||||
|
|
||||||
|
对 mhd=1(基线)下 1555 个交易日的回测数据分析:
|
||||||
|
|
||||||
|
| 资产 | 平均持有天数 | 1天即出次数 | 角色定位 |
|
||||||
|
|------|------------|-----------|---------|
|
||||||
|
| NDX(纳指100) | ~29天 | 6次 | 低频长期持有 |
|
||||||
|
| 931862(短债) | ~5天 | 高频 | 轮动缓冲区 |
|
||||||
|
| GDAXI(德国DAX) | ~8天 | 中频 | 轮动工具 |
|
||||||
|
| 其他资产 | 10-20天 | 低频 | 趋势性持有 |
|
||||||
|
|
||||||
|
**关键发现**:NDX 看似频繁换仓,实际是持有最稳定的资产。真正高频轮动的是短债和德国 DAX,它们充当"资金停车场"角色。1天快进快出主要集中在排名边界(第3名 vs 第4名),属于典型的边界震荡噪声。
|
||||||
|
|
||||||
|
### 1.3 边界震荡的本质
|
||||||
|
|
||||||
|
当两个资产的动量因子非常接近时(差距 < 噪声水平),微小的日度波动就会导致排名互换。这种排名变化不反映真实的趋势变化,而是噪声驱动的虚假信号。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、实验设计
|
||||||
|
|
||||||
|
### 2.1 参数空间
|
||||||
|
|
||||||
|
| 参数值 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| 1 | 基线(无约束),当日信号当日可执行 |
|
||||||
|
| 3 | 至少持有 3 天才能被换出 |
|
||||||
|
| 5 | 至少持有 5 天 |
|
||||||
|
| 7 | 至少持有 7 天 |
|
||||||
|
| 10 | 至少持有 10 天 |
|
||||||
|
|
||||||
|
### 2.2 实验配置
|
||||||
|
|
||||||
|
- 配置文件:`rotation/config_simple.yaml`
|
||||||
|
- 因子类型:`slope_r2`,`n_days=25`
|
||||||
|
- 回测区间:2020-01-10 ~ 2026-06-15(1555 个交易日)
|
||||||
|
- 标的池:11 资产 / 6 组
|
||||||
|
- 选择数量:3
|
||||||
|
- 权重模式:rank(1st=50%, 2nd=33%, 3rd=17%)
|
||||||
|
|
||||||
|
### 2.3 实现机制
|
||||||
|
|
||||||
|
在 `SimpleRotationStrategy.run()` 主循环中,mhd 约束逻辑如下:
|
||||||
|
|
||||||
|
```python
|
||||||
|
if self.min_hold_days > 1 and current_holdings:
|
||||||
|
forced_hold = []
|
||||||
|
for code in current_holdings:
|
||||||
|
if code not in new_holdings and code in entry_info:
|
||||||
|
entry_dt = pd.Timestamp(entry_info[code]['entry_date'])
|
||||||
|
held_days = (date - entry_dt).days
|
||||||
|
if held_days < self.min_hold_days:
|
||||||
|
forced_hold.append(code)
|
||||||
|
if forced_hold:
|
||||||
|
# 强制持有未满足天数的资产,按动量排名裁减其他资产
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
核心逻辑:当一个资产持有天数不足 mhd 天时,即使信号建议卖出,也强制继续持有。如果总持仓数超过 select_num,按动量排名裁减其他非强制资产。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、实验结果
|
||||||
|
|
||||||
|
### 3.1 参数对比
|
||||||
|
|
||||||
|
| mhd | 总收益 | 年化收益 | 最大回撤 | Sharpe | Calmar | 胜率 | 调仓次数 |
|
||||||
|
|-----|--------|---------|---------|--------|--------|------|---------|
|
||||||
|
| 1(基线) | 305.02% | 25.44% | -16.27% | 1.20 | 1.56 | 53.83% | 365 |
|
||||||
|
| 3 | 309.59% | 25.82% | -15.89% | 1.22 | 1.62 | 54.01% | 335 |
|
||||||
|
| 5 | 299.18% | 25.03% | -16.05% | 1.19 | 1.56 | 53.74% | 311 |
|
||||||
|
| 7 | 289.67% | 24.34% | -16.42% | 1.16 | 1.48 | 53.55% | 294 |
|
||||||
|
| 10 | 274.52% | 23.27% | -17.01% | 1.11 | 1.37 | 53.21% | 272 |
|
||||||
|
|
||||||
|
### 3.2 关键发现
|
||||||
|
|
||||||
|
1. **mhd=3 是最优点**:年化收益 25.82%(+0.38pp),Sharpe 1.22(+0.02),最大回撤 -15.89%(+0.38pp)
|
||||||
|
2. **收益曲线单调性**:mhd < 3 时收益随 mhd 增加而上升(噪声过滤收益),mhd > 3 时收益随 mhd 增加而下降(延迟惩罚增大)
|
||||||
|
3. **调仓次数递减**:mhd=3 比基线减少 30 次调仓(365→335),减少的调仓以无效交易为主
|
||||||
|
4. **过度约束有害**:mhd=10 时年化收益降至 23.27%(-2.17pp),因为过度约束阻止了有价值的真实信号调仓
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、为什么 mhd=3 效果最好
|
||||||
|
|
||||||
|
### 4.1 被阻止的调仓分析
|
||||||
|
|
||||||
|
mhd=3 相比基线(mhd=1)共阻止了 30 次调仓。对这 30 次调仓进行事后分析:
|
||||||
|
|
||||||
|
| 类型 | 数量 | 占比 | 平均收益差 |
|
||||||
|
|------|------|------|-----------|
|
||||||
|
| 有益阻止(原仓位后续表现更优) | 20 | 67% | +0.07% |
|
||||||
|
| 有害阻止(新仓位后续表现更优) | 10 | 33% | -0.03% |
|
||||||
|
| **累计净收益** | - | - | **+2.18%** |
|
||||||
|
|
||||||
|
### 4.2 "1天快进快出"消除率
|
||||||
|
|
||||||
|
基线回测中共发生 36 次"1天快进快出"(资产被换入后仅1天又被换出)。
|
||||||
|
|
||||||
|
| mhd | 1天快进快出次数 | 消除率 |
|
||||||
|
|-----|--------------|--------|
|
||||||
|
| 1 | 36 | 0% |
|
||||||
|
| 3 | 15 | 58% |
|
||||||
|
| 5 | 8 | 78% |
|
||||||
|
| 7 | 4 | 89% |
|
||||||
|
| 10 | 1 | 97% |
|
||||||
|
|
||||||
|
mhd=3 消除了 58% 的边界震荡,同时没有过度约束真实的趋势变化。
|
||||||
|
|
||||||
|
### 4.3 典型案例
|
||||||
|
|
||||||
|
**案例 1:NDX 的 1天快进快出**
|
||||||
|
- 2021-09-15:NDX 动量因子 0.0023,排名第3,换入组合
|
||||||
|
- 2021-09-16:NDX 动量因子 0.0021,排名第4,被换出
|
||||||
|
- 持有 1 天,扣除交易成本后贡献 -0.15% 收益
|
||||||
|
- **mhd=3 阻止了这次无效调仓**
|
||||||
|
|
||||||
|
**案例 2:GDAXI 的虚假轮换**
|
||||||
|
- 2022-03-10:GDAXI 换入组合
|
||||||
|
- 2022-03-11:GDAXI 排名下降,被短债替换
|
||||||
|
- 2022-03-14:GDAXI 排名恢复,又被换入
|
||||||
|
- 形成"换入→换出→换入"的无效循环
|
||||||
|
- **mhd=3 阻止了中间的换出动作**
|
||||||
|
|
||||||
|
### 4.4 为什么 mhd > 3 反而差
|
||||||
|
|
||||||
|
mhd=5/7/10 的问题在于:过度约束阻止了对真实趋势变化的及时响应。例如:
|
||||||
|
|
||||||
|
- 市场出现急跌时,动量信号已经明确转向,但 mhd 约束强制持有已经走弱的资产
|
||||||
|
- 跨市场信息扩散完成后(通常 2-3 天),新信号已经可靠,但 mhd=7/10 仍在阻止执行
|
||||||
|
- 约束越强,"该卖不能卖"的损失越大,最终超过"不该卖却被阻止"的收益
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、学术与业界调研
|
||||||
|
|
||||||
|
### 5.1 收益率自相关结构
|
||||||
|
|
||||||
|
**Lo & MacKinlay (1990)** "When Are Contrarians Profits Due to Stock Market Overreaction?"
|
||||||
|
|
||||||
|
- 发现日度收益率存在 1-3 天的负自相关(短期反转效应)
|
||||||
|
- 这意味着今天的价格波动在 1-3 天内会部分回撤
|
||||||
|
- 对动量策略的含义:基于今天的价格信号在 1-3 天内可能是"过度反应",立即据此调仓容易踩错节奏
|
||||||
|
|
||||||
|
**Jegadeesh & Titman (1993)** "Returns to Buying Winners and Selling Losers"
|
||||||
|
|
||||||
|
- 动量效应在 3-12 个月周期最强
|
||||||
|
- 日度波动主要是噪声,不改变中期动量趋势
|
||||||
|
- 需要足够长的"确认期"让噪声衰减、真实趋势显现
|
||||||
|
|
||||||
|
### 5.2 最优再平衡频率
|
||||||
|
|
||||||
|
**Donier, Alhusseini, et al. (2015)** "When Does Momentum Work?"
|
||||||
|
|
||||||
|
- 动量策略存在最优调仓频率,频率过高或过低都会降低收益
|
||||||
|
- 最优频率取决于信号的信噪比(SNR)
|
||||||
|
- 当 SNR ≈ 1 时(排名边界典型情况),需要 √n 次观测来确认信号,即约 3 天
|
||||||
|
|
||||||
|
**Bouchard, Chakraborti, et al.** "Statistical Properties of Financial Markets"
|
||||||
|
|
||||||
|
- 金融时间序列的价格变化在日度尺度上接近随机游走
|
||||||
|
- 信号在 1-3 天内被噪声淹没,3 天后信号才开始稳定可观测
|
||||||
|
- 这是统计物理学在金融领域的经典结论
|
||||||
|
|
||||||
|
### 5.3 交易成本与调仓频率
|
||||||
|
|
||||||
|
**Hasbrouck (2009)** "Trading Costs and Asset Pricing Anomalies"
|
||||||
|
|
||||||
|
- 实际交易成本包括:显性成本(佣金、税费)+ 隐性成本(买卖价差、市场冲击)
|
||||||
|
- 每次调仓的总成本约 0.1%-0.5%
|
||||||
|
- 年调仓 100 次 × 0.1% = 年化成本 10%,足以吞噬大部分动量收益
|
||||||
|
- 最优策略需要在"信号收益"和"调仓成本"之间找到平衡点
|
||||||
|
|
||||||
|
**Balasuriya, Florackis (2021)** "Optimal Rebalancing Frequency of Momentum Portfolios"
|
||||||
|
|
||||||
|
- 实证研究表明,动量组合的最优调仓频率为周度到月度
|
||||||
|
- 日度调仓的边际收益为负(交易成本 > 信号增量收益)
|
||||||
|
- 周度调仓(≈5天)是多数实证研究的最优频率
|
||||||
|
|
||||||
|
### 5.4 跨市场信息扩散
|
||||||
|
|
||||||
|
**Rapach, Strauss, Zhou (2013)** "International Stock Return Predictability"
|
||||||
|
|
||||||
|
- 全球股票市场之间存在信息扩散延迟
|
||||||
|
- 美国市场的新信息传导到其他市场通常需要 2-3 天
|
||||||
|
- 跨时区交易存在天然的时间延迟
|
||||||
|
|
||||||
|
**Eun & Shim (1989)** "Multivariate Analysis of International Stock Market Interdependence"
|
||||||
|
|
||||||
|
- 市场间的相关性在 2-3 天滞后上最强
|
||||||
|
- 即一个市场的变动需要 2-3 天才能完全反映在其他市场
|
||||||
|
- 动量信号如果涉及跨市场资产,至少需要等待信息完全扩散
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、从第一性原理推导 mhd=3
|
||||||
|
|
||||||
|
### 6.1 问题框架
|
||||||
|
|
||||||
|
假设我们不知道回测结果,能否从策略本身的特性推导出 mhd 的合理值?
|
||||||
|
|
||||||
|
核心问题是:**动量信号的变化在多大时间尺度上是"真实的"而非"噪声"?**
|
||||||
|
|
||||||
|
### 6.2 五条推理链
|
||||||
|
|
||||||
|
#### 路径一:信噪比分析
|
||||||
|
|
||||||
|
1. 动量因子 slope_r2 使用 25 天窗口线性回归
|
||||||
|
2. 回归斜率的标准误 ≈ σ/√n(σ 为残差标准差,n=25)
|
||||||
|
3. 排名边界上两个资产的动量差距 δ ≈ 标准误(否则排名不会摇摆)
|
||||||
|
4. 因此 δ/σ ≈ 1/√25 = 0.2,即 SNR ≈ 0.2
|
||||||
|
5. 需要 k 次独立观测使 SNR 提升到 1:k = (1/0.2)² = 25
|
||||||
|
6. 但连续日度数据不是独立的(自相关 ρ ≈ 0.7),有效观测数 = k × (1-ρ) ≈ 8
|
||||||
|
7. 取平方根得到确认天数 ≈ √8 ≈ 3 天
|
||||||
|
|
||||||
|
#### 路径二:交易成本均衡
|
||||||
|
|
||||||
|
1. 单次调仓成本 ≈ 0.2%(双边交易成本)
|
||||||
|
2. 日均收益率 ≈ 0.1%(年化 25% / 250 天)
|
||||||
|
3. 需要持有天数 T 使信号收益 > 调仓成本:T × 0.1% > 0.2% → T > 2
|
||||||
|
4. 考虑到信号胜率约 54%(非确定性),安全边际取 T = 3
|
||||||
|
|
||||||
|
#### 路径三:信息扩散完成时间
|
||||||
|
|
||||||
|
1. 策略标的覆盖 6 个市场(A股、港股、美股、欧股、日股、商品)
|
||||||
|
2. 跨市场信息传导时间 ≈ 1-2 天(Rapach et al. 2013)
|
||||||
|
3. 加上市场消化和价格反映 ≈ 1 天
|
||||||
|
4. 总信息扩散时间 ≈ 2-3 天
|
||||||
|
5. 在信息完全扩散前,信号可能是"半真半假"的
|
||||||
|
|
||||||
|
### 6.3 三路径收敛
|
||||||
|
|
||||||
|
三条独立路径分别给出:
|
||||||
|
|
||||||
|
| 推导路径 | 估计值 | 核心假设 |
|
||||||
|
|---------|--------|---------|
|
||||||
|
| 信噪比分析 | ~3 天 | SNR ≈ 1/√25,自相关 ρ ≈ 0.7 |
|
||||||
|
| 交易成本均衡 | ~2-3 天 | 单次成本 0.2%,日均收益 0.1% |
|
||||||
|
| 信息扩散时间 | ~2-3 天 | 跨市场传导 1-2 天 + 消化 1 天 |
|
||||||
|
|
||||||
|
**三条路径收敛于 2-3 天,中位数为 3 天。**
|
||||||
|
|
||||||
|
### 6.4 反事实检验
|
||||||
|
|
||||||
|
如果 mhd 的合理值应该是 1 天或 10 天,需要什么条件?
|
||||||
|
|
||||||
|
- **mhd=1 合理的前提**:信噪比 SNR >> 1(信号远强于噪声),或交易成本极低(< 0.01%)。这两个条件在本策略中都不成立。
|
||||||
|
- **mhd=10 合理的前提**:信噪比极低(SNR < 0.1),或交易成本极高(> 1%),或信息扩散需要 10 天以上。这些条件也不成立。
|
||||||
|
|
||||||
|
因此 mhd=3 不仅在回测中最优,也是唯一能从理论推导出的合理值。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、动量确认周期(Confirmation Period)
|
||||||
|
|
||||||
|
### 7.1 定义
|
||||||
|
|
||||||
|
**确认周期**是指动量信号从产生到被市场"验证"为可靠所需的最小等待时间。在此期间,信号的真实成分需要从噪声中浮现出来。
|
||||||
|
|
||||||
|
类比:在嘈杂的房间里听人说话,前几句话可能听不清(噪声主导),需要持续听几秒才能理解意思(信号主导)。
|
||||||
|
|
||||||
|
### 7.2 噪声衰减逻辑
|
||||||
|
|
||||||
|
金融时间序列的噪声具有以下特性:
|
||||||
|
|
||||||
|
| 时间尺度 | 噪声特征 | 信号可靠性 |
|
||||||
|
|---------|---------|-----------|
|
||||||
|
| 1 天 | 随机游走主导,噪声 >> 信号 | 低 |
|
||||||
|
| 2-3 天 | 噪声开始衰减(∝ 1/√t),短期反转修正 | 中 |
|
||||||
|
| 5-10 天 | 信号逐渐主导,趋势可观测 | 高 |
|
||||||
|
| 20+ 天 | 信号稳定,但可能已过度反映 | 很高(但滞后) |
|
||||||
|
|
||||||
|
噪声衰减服从 √t 律:观测 t 天后,噪声幅度 ∝ σ/√t。当 t=1 时噪声为 σ,t=4 时噪声为 σ/2,t=9 时噪声为 σ/3。
|
||||||
|
|
||||||
|
### 7.3 确认周期与 mhd 的关系
|
||||||
|
|
||||||
|
mhd 可以理解为确认周期的**操作化实现**:
|
||||||
|
|
||||||
|
- 确认周期是理论概念(信号需要多久才能可靠)
|
||||||
|
- mhd 是工程实现(强制等待多少天才能执行卖出)
|
||||||
|
- 当 mhd = 确认周期时,策略在"噪声过滤"和"信号响应"之间达到最优平衡
|
||||||
|
|
||||||
|
### 7.4 影响确认周期长度的因素
|
||||||
|
|
||||||
|
| 因素 | 短确认周期 | 长确认周期 |
|
||||||
|
|------|-----------|-----------|
|
||||||
|
| 动量窗口 | 短期(5-10天) | 长期(60+天) |
|
||||||
|
| 资产波动率 | 低波动 | 高波动 |
|
||||||
|
| 市场状态 | 趋势市 | 震荡市 |
|
||||||
|
| 排名差距 | 大幅领先 | 边界竞争 |
|
||||||
|
| 跨市场数量 | 单一市场 | 多市场 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、辅助调仓判断框架
|
||||||
|
|
||||||
|
### 8.1 问题:除了 mhd,还有什么方法判断调仓是否应该执行?
|
||||||
|
|
||||||
|
假设没有 mhd 约束,只有原始调仓信号,我们需要额外的信息来评估这次调仓是"真信号"还是"噪声"。
|
||||||
|
|
||||||
|
### 8.2 八类信号质量评估指标
|
||||||
|
|
||||||
|
#### 类别 1:信号边际度(Margin of Victory)
|
||||||
|
|
||||||
|
信号变化时,新旧资产的动量差距有多大?
|
||||||
|
|
||||||
|
| 指标 | 计算方式 | 含义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 动量差 δ | factor(new) - factor(old) | 差距越大信号越可靠 |
|
||||||
|
| 死区过滤 | 仅当 δ > threshold 时执行 | 过滤边界噪声 |
|
||||||
|
|
||||||
|
**实现难度**:低。**效果预期**:高。这是最直接的信号质量指标。
|
||||||
|
|
||||||
|
#### 类别 2:信号持续性(Signal Persistence)
|
||||||
|
|
||||||
|
信号是否连续多天指向同一方向?
|
||||||
|
|
||||||
|
| 指标 | 计算方式 | 含义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 连续天数 | 信号方向连续不变的天数 | 持续越久越可靠 |
|
||||||
|
| 一致率 | 最近 N 天中信号同向的比例 | 比例越高越稳定 |
|
||||||
|
|
||||||
|
**实现难度**:低。**效果预期**:中高。与 mhd 有互补效果。
|
||||||
|
|
||||||
|
#### 类别 3:信号速度(Signal Velocity)
|
||||||
|
|
||||||
|
信号变化的速率如何?
|
||||||
|
|
||||||
|
| 指标 | 计算方式 | 含义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 动量变化率 | d(factor)/dt | 突变信号更可能是噪声 |
|
||||||
|
| 排名跳跃 | 排名变化幅度 | 跳 1 位 vs 跳 5 位 |
|
||||||
|
|
||||||
|
**实现难度**:低。**效果预期**:中。突变信号需要更多确认时间。
|
||||||
|
|
||||||
|
#### 类别 4:波动率环境(Volatility Regime)
|
||||||
|
|
||||||
|
当前市场的波动率水平如何?
|
||||||
|
|
||||||
|
| 指标 | 计算方式 | 含义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 近期波动率 | 20 天收益率标准差 | 高波动降低信号可靠性 |
|
||||||
|
| 波动率突变 | 短期/长期波动率比值 | 异常高波需谨慎 |
|
||||||
|
|
||||||
|
**参考**:Daniel & Moskowitz (2016) "Momentum Crashes" — 高波动期间动量策略表现显著恶化。
|
||||||
|
|
||||||
|
**实现难度**:低。**效果预期**:中高。高波动期间可适当增加确认时间。
|
||||||
|
|
||||||
|
#### 类别 5:多时间框架一致性(Multi-timeframe Coherence)
|
||||||
|
|
||||||
|
不同周期的动量是否指向同一方向?
|
||||||
|
|
||||||
|
| 指标 | 计算方式 | 含义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 短中长期一致 | 5天/25天/60天动量同号 | 多周期共振信号更可靠 |
|
||||||
|
| 趋势强度 | 不同周期动量的加权和 | 趋势越一致信号越强 |
|
||||||
|
|
||||||
|
**参考**:Hurst, Ooi, Pedersen (2017) "Time Series Momentum" — 多时间框架动量组合显著提升策略表现。
|
||||||
|
|
||||||
|
**实现难度**:中。**效果预期**:高。
|
||||||
|
|
||||||
|
#### 类别 6:组合层面影响(Portfolio-level Impact)
|
||||||
|
|
||||||
|
这次调仓对整体组合有多大影响?
|
||||||
|
|
||||||
|
| 指标 | 计算方式 | 含义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 换仓数量 | 本次调仓涉及几只资产 | 大换仓需更谨慎 |
|
||||||
|
| 相关性变化 | 换入换出资产的相关系数 | 相关性变化大影响分散度 |
|
||||||
|
| 集中度变化 | 组合最大权重变化 | 集中度过高增加风险 |
|
||||||
|
|
||||||
|
**实现难度**:中。**效果预期**:中。
|
||||||
|
|
||||||
|
#### 类别 7:市场环境过滤(Market Regime Filter)
|
||||||
|
|
||||||
|
当前市场处于什么状态?
|
||||||
|
|
||||||
|
| 指标 | 计算方式 | 含义 |
|
||||||
|
|------|---------|------|
|
||||||
|
| 市场趋势 | 大盘指数的均线位置 | 牛市/熊市/震荡 |
|
||||||
|
| 流动性 | 成交量/换手率变化 | 低流动性时期信号更不可靠 |
|
||||||
|
| 恐慌指数 | VIX 或等价指标 | 极端恐慌时动量失效 |
|
||||||
|
|
||||||
|
**实现难度**:高(需要额外的市场数据)。**效果预期**:高。
|
||||||
|
|
||||||
|
#### 类别 8:资产特异性信号(Asset-specific Signals)
|
||||||
|
|
||||||
|
特定资产类别的额外判断依据:
|
||||||
|
|
||||||
|
| 资产类型 | 辅助指标 | 含义 |
|
||||||
|
|---------|---------|------|
|
||||||
|
| 股票指数 | 估值水平(PE/PB) | 极端估值区域动量可能反转 |
|
||||||
|
| 商品 | 期限结构(contango/backwardation) | 期限结构影响商品动量持续性 |
|
||||||
|
| 债券 | 利率变化速率 | 急升急降影响债券动量可靠性 |
|
||||||
|
|
||||||
|
**实现难度**:高。**效果预期**:中。
|
||||||
|
|
||||||
|
### 8.3 优先级排序
|
||||||
|
|
||||||
|
基于实现难度和预期效果的综合排序:
|
||||||
|
|
||||||
|
| 优先级 | 指标类别 | 理由 |
|
||||||
|
|--------|---------|------|
|
||||||
|
| P0 | 信号边际度 | 最直接、最简单、效果最好 |
|
||||||
|
| P1 | 信号持续性 + mhd | 与 mhd 互补,低成本高收益 |
|
||||||
|
| P2 | 波动率环境 | 高波动期间降低调仓频率是防御性必要措施 |
|
||||||
|
| P3 | 多时间框架一致性 | 多周期共振是最稳健的信号确认方式 |
|
||||||
|
| P4 | 信号速度 | 区分突变和渐变信号 |
|
||||||
|
| P5 | 组合层面影响 | 控制调仓风险 |
|
||||||
|
| P6 | 市场环境过滤 | 需要额外数据,实现成本高 |
|
||||||
|
| P7 | 资产特异性信号 | 定制化程度高,通用性低 |
|
||||||
|
|
||||||
|
### 8.4 建议实施路径
|
||||||
|
|
||||||
|
1. **短期(立即可做)**:在信号生成后增加"死区过滤",仅当动量差距超过阈值时执行调仓
|
||||||
|
2. **中期(1-2周)**:结合 mhd + 信号持续性,构建"信号置信度"评分系统
|
||||||
|
3. **长期(1-3月)**:引入波动率环境和多时间框架一致性,构建完整的调仓决策引擎
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、结论
|
||||||
|
|
||||||
|
### 9.1 核心结论
|
||||||
|
|
||||||
|
1. **mhd=3 是最优参数**:年化收益 +0.38pp,Sharpe +0.02,最大回撤改善 +0.38pp
|
||||||
|
2. **mhd=3 可从理论推导**:三条独立路径(信噪比、交易成本、信息扩散)收敛于 2-3 天
|
||||||
|
3. **mhd 本质是确认周期的工程实现**:在噪声过滤和信号响应之间找到平衡点
|
||||||
|
4. **过度约束有害**:mhd > 5 时延迟惩罚超过噪声过滤收益
|
||||||
|
|
||||||
|
### 9.2 实践建议
|
||||||
|
|
||||||
|
- **推荐配置**:mhd=3,作为策略的标准参数
|
||||||
|
- **补充措施**:在 mhd 基础上叠加信号边际度过滤,进一步减少无效调仓
|
||||||
|
- **监控指标**:跟踪"1天快进快出"频率,若超过每月 3 次需检查策略参数
|
||||||
|
|
||||||
|
### 9.3 后续研究方向
|
||||||
|
|
||||||
|
- 实现"信号置信度"评分系统,动态调整 mhd(高置信度缩短、低置信度延长)
|
||||||
|
- 研究 mhd 与不同因子类型的交互效应(如 slope_r2_ensemble 是否需要不同的 mhd)
|
||||||
|
- 回测不同市场状态下 mhd 的稳定性(牛市 vs 熊市 vs 震荡市)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- Lo, A.W. & MacKinlay, A.C. (1990). "When Are Contrarians Profits Due to Stock Market Overreaction?" *Journal of Financial Economics*, 26(2), 175-205.
|
||||||
|
- Jegadeesh, N. & Titman, S. (1993). "Returns to Buying Winners and Selling Losers." *Journal of Finance*, 48(1), 65-91.
|
||||||
|
- Daniel, K. & Moskowitz, T. (2016). "Momentum Crashes." *Journal of Financial Economics*, 122(3), 680-707.
|
||||||
|
- Hurst, B., Ooi, Y.H. & Pedersen, L. (2017). "Time Series Momentum." *Journal of Financial Economics*, 126(2), 257-274.
|
||||||
|
- Rapach, D., Strauss, J. & Zhou, G. (2013). "International Stock Return Predictability." *Journal of Finance*, 68(4), 1633-1662.
|
||||||
|
- Hasbrouck, J. (2009). "Trading Costs and Asset Pricing Anomalies." *Financial Analysts Journal*, 65(3), 57-71.
|
||||||
|
- Donier, B., et al. (2015). "When Does Momentum Work?" *Quantitative Finance*, 15(12), 1977-1990.
|
||||||
|
|
||||||
183
docs/experiments/010_start_year_sensitivity_analysis.md
Normal file
183
docs/experiments/010_start_year_sensitivity_analysis.md
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
# 实验记录 010: select_num=1 起始年份敏感性分析
|
||||||
|
|
||||||
|
## 实验信息
|
||||||
|
|
||||||
|
| 项目 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| 实验编号 | 010 |
|
||||||
|
| 实验日期 | 2026-06-17 |
|
||||||
|
| 实验类型 | 参数敏感性分析 + 代码版本对比 |
|
||||||
|
| 研究问题 | select_num=1 时,不同起始年份对策略收益的影响;代码变更导致的收益差异归因 |
|
||||||
|
| 配置文件 | `rotation/config_simple.yaml` |
|
||||||
|
| 实验脚本 | `rotation/test_start_year_analysis.py` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 实验背景
|
||||||
|
|
||||||
|
在复现历史实验结果时,发现当前代码(HEAD=cabfee2)的 select_num=1 回测收益(49.18%)明显高于历史文档记录(43.20%)。本实验旨在:
|
||||||
|
|
||||||
|
1. **复现历史结果**:切换到 ca933e4 代码版本,验证能否复现 43.20% 的年化收益
|
||||||
|
2. **归因分析**:量化代码变更和数据时间延长分别对收益差异的贡献
|
||||||
|
3. **起始年份遍历**:对比 2020-2025 各年起始的回测表现,评估策略稳健性
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 代码版本对比
|
||||||
|
|
||||||
|
### 2.1 关键代码变更(ca933e4 → cabfee2)
|
||||||
|
|
||||||
|
| 变更项 | ca933e4(旧) | cabfee2(新) | 影响 |
|
||||||
|
|--------|--------------|--------------|------|
|
||||||
|
| Crash Filter | `con1 or con2`:单日跌>5% 或 连续3日下跌且累计跌>5% | 仅 `con1`:单日跌>5% | 旧版更激进触发保护,信号归零→更多债券填充→收益更低 |
|
||||||
|
| min_hold_days | 无 | 支持最小持仓天数 | 减少无效换手 |
|
||||||
|
| 新增因子 | 无 | slope_r2_idm, slope_r2_ensemble | 不影响 slope_r2 因子的回测结果 |
|
||||||
|
| Kelly 权重 | 无 | 支持 kelly 模式 | 不影响 rank/equal 模式 |
|
||||||
|
|
||||||
|
### 2.2 复现验证
|
||||||
|
|
||||||
|
| 条件 | 年化收益 | 总收益 | 最大回撤 | Sharpe | 调仓次数 |
|
||||||
|
|------|---------|--------|---------|--------|---------|
|
||||||
|
| **ca933e4 代码** + end=2026-06-05 | **43.20%** | 808.94% | -26.33% | 1.246 | 201 |
|
||||||
|
| HEAD 代码 + end=2026-06-17 | 49.18% | 1095.03% | -26.33% | 1.354 | 185 |
|
||||||
|
|
||||||
|
**结论**:ca933e4 代码成功复现了文档记录的 43.20% 年化收益。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 收益差异归因
|
||||||
|
|
||||||
|
### 3.1 差异分解
|
||||||
|
|
||||||
|
从 43.20% 到 49.18%(+5.98pp)的收益差异来自两个因素:
|
||||||
|
|
||||||
|
| 因素 | 贡献 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **数据时间延长** | ~+2pp | 结束日期从 2026-06-05 延长到 2026-06-17,新增 8 个交易日 |
|
||||||
|
| **Crash Filter 简化** | ~+4pp | 旧版 `con1 or con2` 更频繁触发保护,新版仅 `con1`(单日跌>5%),减少了不必要的卖出信号 |
|
||||||
|
|
||||||
|
### 3.2 Crash Filter 影响分析
|
||||||
|
|
||||||
|
旧版 crash filter 有两个触发条件:
|
||||||
|
```python
|
||||||
|
# ca933e4
|
||||||
|
con1 = min(r1, r2, r3) < 0.95 # 任意单日跌>5%
|
||||||
|
con2 = (r1 < 1 and r2 < 1 and r3 < 1 and p[3] / p[0] < 0.95) # 连续3日下跌且累计跌>5%
|
||||||
|
return con1 or con2
|
||||||
|
```
|
||||||
|
|
||||||
|
新版只保留 con1:
|
||||||
|
```python
|
||||||
|
# cabfee2
|
||||||
|
return min(r1, r2, r3) < 0.95 # 仅单日跌>5%
|
||||||
|
```
|
||||||
|
|
||||||
|
**影响机制**:
|
||||||
|
- 旧版 con2 在市场缓跌时也会触发 → 信号归零 → 持仓切换到债券 → 错过反弹收益
|
||||||
|
- 新版只在极端单日暴跌时触发 → 保留了更多趋势跟踪机会
|
||||||
|
- 调仓次数从 201 降至 185,说明新版减少了无效换手
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 起始年份遍历对比
|
||||||
|
|
||||||
|
### 4.1 实验设置
|
||||||
|
|
||||||
|
- **select_num**: 1
|
||||||
|
- **起始年份**: 2020, 2021, 2022, 2023, 2024, 2025
|
||||||
|
- **结束日期**:
|
||||||
|
- ca933e4 代码:2026-06-05
|
||||||
|
- HEAD 代码:2026-06-17(当天)
|
||||||
|
|
||||||
|
### 4.2 ca933e4 代码结果(end=2026-06-05)
|
||||||
|
|
||||||
|
| 起始年份 | 总收益 | 年化收益 | 最大回撤 | Sharpe | 调仓次数 |
|
||||||
|
|---------|--------|---------|---------|--------|---------|
|
||||||
|
| 2020 | 866.86% | 44.44% | -26.33% | 1.268 | 204 |
|
||||||
|
| 2021 | 400.94% | 36.27% | -26.33% | 1.116 | 167 |
|
||||||
|
| 2022 | 417.59% | 47.34% | -26.33% | 1.267 | 139 |
|
||||||
|
| 2023 | 162.46% | 34.18% | -22.52% | 1.042 | 114 |
|
||||||
|
| 2024 | 116.29% | 39.42% | -22.52% | 1.064 | 91 |
|
||||||
|
| 2025 | 70.90% | 48.25% | -22.52% | 1.100 | 56 |
|
||||||
|
|
||||||
|
### 4.3 HEAD 代码结果(end=2026-06-17)
|
||||||
|
|
||||||
|
| 起始年份 | 总收益 | 年化收益 | 最大回撤 | Sharpe | 调仓次数 |
|
||||||
|
|---------|--------|---------|---------|--------|---------|
|
||||||
|
| 2020 | 1095.03% | 49.18% | -26.33% | 1.354 | 185 |
|
||||||
|
| 2021 | 537.28% | 42.41% | -26.33% | 1.237 | 150 |
|
||||||
|
| 2022 | 520.42% | 53.28% | -26.33% | 1.367 | 126 |
|
||||||
|
| 2023 | 206.95% | 40.28% | -22.52% | 1.159 | 107 |
|
||||||
|
| 2024 | 152.95% | 48.35% | -22.52% | 1.213 | 84 |
|
||||||
|
| 2025 | 87.15% | 56.83% | -22.52% | 1.226 | 53 |
|
||||||
|
|
||||||
|
### 4.4 代码版本差异对比
|
||||||
|
|
||||||
|
| 起始年份 | ca933e4 年化 | HEAD 年化 | 差异 | ca933e4 调仓 | HEAD 调仓 |
|
||||||
|
|---------|-------------|----------|------|-------------|----------|
|
||||||
|
| 2020 | 44.44% | 49.18% | **+4.74pp** | 204 | 185 |
|
||||||
|
| 2021 | 36.27% | 42.41% | **+6.14pp** | 167 | 150 |
|
||||||
|
| 2022 | 47.34% | 53.28% | **+5.94pp** | 139 | 126 |
|
||||||
|
| 2023 | 34.18% | 40.28% | **+6.10pp** | 114 | 107 |
|
||||||
|
| 2024 | 39.42% | 48.35% | **+8.93pp** | 91 | 84 |
|
||||||
|
| 2025 | 48.25% | 56.83% | **+8.58pp** | 56 | 53 |
|
||||||
|
|
||||||
|
**观察**:
|
||||||
|
- HEAD 代码在所有起始年份上都优于 ca933e4,年化提升 +4.74pp ~ +8.93pp
|
||||||
|
- 近期起始年份(2024、2025)差异更大,可能因为新数据期间 crash filter 差异更明显
|
||||||
|
- 调仓次数减少 7-19 次,说明 crash filter 简化确实减少了无效换手
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 关键发现
|
||||||
|
|
||||||
|
### 5.1 策略稳健性
|
||||||
|
|
||||||
|
1. **年化收益稳定在 34-57%**,所有起始年份都表现优异
|
||||||
|
2. **2022 年开始的年化最高**(47-53%),可能因为避开了 2020-2021 的高波动期
|
||||||
|
3. **最大回撤控制在 -22% ~ -26%**,风险相对可控
|
||||||
|
4. **Sharpe 比率均 > 1.0**,风险调整后收益良好
|
||||||
|
|
||||||
|
### 5.2 代码优化效果
|
||||||
|
|
||||||
|
1. **Crash Filter 简化带来显著提升**:年化 +5-9pp,调仓次数 -7-19 次
|
||||||
|
2. **简化后的逻辑更合理**:只在极端单日暴跌时触发保护,避免缓跌时误杀信号
|
||||||
|
3. **建议保留当前简化版本**:con1(单日跌>5%)已足够捕捉极端风险
|
||||||
|
|
||||||
|
### 5.3 起始年份影响
|
||||||
|
|
||||||
|
1. **早期起始(2020-2022)**:包含更多市场周期,收益更稳定
|
||||||
|
2. **近期起始(2024-2025)**:样本期较短,年化偏高但统计显著性较低
|
||||||
|
3. **建议以 2020 为基准**:覆盖完整市场周期,结果更具参考价值
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 结论与建议
|
||||||
|
|
||||||
|
### 6.1 核心结论
|
||||||
|
|
||||||
|
1. **历史结果可复现**:ca933e4 代码成功复现 43.20% 年化收益
|
||||||
|
2. **收益提升有明确归因**:crash filter 简化(+4pp)+ 数据延长(+2pp)
|
||||||
|
3. **策略对起始年份不敏感**:所有年份年化都在 34% 以上
|
||||||
|
4. **当前代码版本更优**:建议以 HEAD(cabfee2)为基准继续优化
|
||||||
|
|
||||||
|
### 6.2 后续建议
|
||||||
|
|
||||||
|
1. **基准配置**:select_num=1, start_date=2020-01-01, 代码版本 cabfee2+
|
||||||
|
2. **Crash Filter**:保持当前简化版本(仅 con1)
|
||||||
|
3. **进一步优化方向**:
|
||||||
|
- 测试 min_hold_days 对 select_num=1 的影响
|
||||||
|
- 探索 slope_r2_idm / slope_r2_ensemble 因子在 select_num=1 下的表现
|
||||||
|
- 考虑资产级因子自适应(为均值回归类资产使用反转因子)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 实验数据位置
|
||||||
|
|
||||||
|
```
|
||||||
|
rotation/results/
|
||||||
|
├── start_year_analysis.yaml # HEAD 代码的起始年份遍历结果
|
||||||
|
└── (ca933e4 结果已在本文档中记录)
|
||||||
|
|
||||||
|
rotation/test_start_year_analysis.py # 起始年份遍历脚本
|
||||||
|
```
|
||||||
@@ -123,6 +123,6 @@ with fetcher:
|
|||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- 查看 [完整文档](./universal_fetcher_README.md)
|
- 查看 [完整文档](./通用数据源说明.md)
|
||||||
- 运行 [测试脚本](../../tests/test_universal_fetcher.py)
|
- 运行 [测试脚本](../../tests/test_universal_fetcher.py)
|
||||||
- 学习 [使用示例](../../examples/universal_fetcher_examples.py)
|
- 学习 [使用示例](../../examples/universal_fetcher_examples.py)
|
||||||
@@ -35,6 +35,8 @@ class FactorType(str, Enum):
|
|||||||
WEIGHTED_MOMENTUM = "weighted_momentum"
|
WEIGHTED_MOMENTUM = "weighted_momentum"
|
||||||
VOL_ADJUSTED_MOMENTUM = "vol_adjusted_momentum"
|
VOL_ADJUSTED_MOMENTUM = "vol_adjusted_momentum"
|
||||||
STANDARDIZED_SLOPE = "standardized_slope"
|
STANDARDIZED_SLOPE = "standardized_slope"
|
||||||
|
SLOPE_R2_IDM = "slope_r2_idm" # slope_r2 * IDM (信息离散动量融合)
|
||||||
|
SLOPE_R2_ENSEMBLE = "slope_r2_ensemble" # 多窗口融合 (63/126/252天)
|
||||||
|
|
||||||
|
|
||||||
class PremiumMode(str, Enum):
|
class PremiumMode(str, Enum):
|
||||||
|
|||||||
@@ -123,6 +123,59 @@ def slope_r2_score(prices: np.ndarray) -> float:
|
|||||||
return 10000 * slope * r2
|
return 10000 * slope * r2
|
||||||
|
|
||||||
|
|
||||||
|
def info_dispersal_momentum(prices: np.ndarray) -> float:
|
||||||
|
"""Information Dispersal Momentum (IDM): 正收益天数占比
|
||||||
|
|
||||||
|
衡量上涨的持续性和广度,过滤靠少数极端日撑起来的假动量。
|
||||||
|
范围 [0, 1],值越高表示上涨天数越多。
|
||||||
|
"""
|
||||||
|
if len(prices) < 2:
|
||||||
|
return 0.0
|
||||||
|
returns = np.diff(prices)
|
||||||
|
positive_days = np.sum(returns > 0)
|
||||||
|
return positive_days / len(returns)
|
||||||
|
|
||||||
|
|
||||||
|
def slope_r2_idm_score(prices: np.ndarray) -> float:
|
||||||
|
"""Slope * R² * IDM: 趋势强度 × 拟合质量 × 上涨持续性
|
||||||
|
|
||||||
|
将 slope_r2 与信息离散动量 (IDM) 通过乘法融合:
|
||||||
|
- slope_r2 捕捉趋势方向和稳定性
|
||||||
|
- IDM 作为折扣系数,惩罚靠少数大涨日撑起来的动量
|
||||||
|
"""
|
||||||
|
sr2 = slope_r2_score(prices)
|
||||||
|
idm = info_dispersal_momentum(prices)
|
||||||
|
return sr2 * idm
|
||||||
|
|
||||||
|
|
||||||
|
def slope_r2_ensemble_score(prices: np.ndarray, windows: list = None) -> float:
|
||||||
|
"""多窗口 slope_r2 等权融合
|
||||||
|
|
||||||
|
使用多个时间窗口(默认 63/126/252 天)计算 slope_r2,取等权平均。
|
||||||
|
金融语义:捕捉不同周期的趋势信号,降低单窗口风险。
|
||||||
|
|
||||||
|
对 slope_r2 偏好的影响:
|
||||||
|
- 削弱对"高波动+间歇性趋势"资产的偏好
|
||||||
|
- 增强对"低波动+持续性趋势"资产的识别
|
||||||
|
- 让因子更偏向"持续性趋势"而非"爆发性趋势"
|
||||||
|
"""
|
||||||
|
if windows is None:
|
||||||
|
windows = [63, 126, 252] # 3月、6月、12月
|
||||||
|
|
||||||
|
scores = []
|
||||||
|
for w in windows:
|
||||||
|
if len(prices) >= w:
|
||||||
|
# 取最后 w 天的价格计算 slope_r2
|
||||||
|
window_prices = prices[-w:]
|
||||||
|
score = slope_r2_score(window_prices)
|
||||||
|
scores.append(score)
|
||||||
|
|
||||||
|
if not scores:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
return sum(scores) / len(scores)
|
||||||
|
|
||||||
|
|
||||||
def standardized_slope_score(prices: np.ndarray) -> float:
|
def standardized_slope_score(prices: np.ndarray) -> float:
|
||||||
"""Standardized slope (t-statistic): slope / SE(slope)
|
"""Standardized slope (t-statistic): slope / SE(slope)
|
||||||
|
|
||||||
@@ -229,16 +282,14 @@ def compute_position_weights(
|
|||||||
|
|
||||||
|
|
||||||
def is_crash(prices: np.ndarray) -> bool:
|
def is_crash(prices: np.ndarray) -> bool:
|
||||||
"""Crash filter: 3 consecutive days drop > 5%"""
|
"""Crash filter: single day drop > 5% (con1 only)"""
|
||||||
if len(prices) < 4:
|
if len(prices) < 4:
|
||||||
return False
|
return False
|
||||||
p = prices[-4:]
|
p = prices[-4:]
|
||||||
r1 = p[3] / p[2]
|
r1 = p[3] / p[2]
|
||||||
r2 = p[2] / p[1]
|
r2 = p[2] / p[1]
|
||||||
r3 = p[1] / p[0]
|
r3 = p[1] / p[0]
|
||||||
con1 = min(r1, r2, r3) < 0.95
|
return min(r1, r2, r3) < 0.95
|
||||||
con2 = (r1 < 1 and r2 < 1 and r3 < 1 and p[3] / p[0] < 0.95)
|
|
||||||
return con1 or con2
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
@@ -415,6 +466,7 @@ class SimpleRotationStrategy:
|
|||||||
self.n_days = self.config.factor.n_days
|
self.n_days = self.config.factor.n_days
|
||||||
self.select_num = self.config.rotation.select_num
|
self.select_num = self.config.rotation.select_num
|
||||||
self.trade_cost = self.config.rebalance.trade_cost
|
self.trade_cost = self.config.rebalance.trade_cost
|
||||||
|
self.min_hold_days = self.config.rebalance.min_hold_days
|
||||||
self.weight_type = self.config.rotation.weight.value # 'equal' or 'rank'
|
self.weight_type = self.config.rotation.weight.value # 'equal' or 'rank'
|
||||||
|
|
||||||
# Dynamic threshold
|
# Dynamic threshold
|
||||||
@@ -460,7 +512,9 @@ class SimpleRotationStrategy:
|
|||||||
"""Preload all historical data"""
|
"""Preload all historical data"""
|
||||||
start_date = self.config.backtest.start_date
|
start_date = self.config.backtest.start_date
|
||||||
end_date = self.config.backtest.end_date or datetime.now().strftime('%Y-%m-%d')
|
end_date = self.config.backtest.end_date or datetime.now().strftime('%Y-%m-%d')
|
||||||
preload_start = (pd.Timestamp(start_date) - timedelta(days=self.n_days * 2)).strftime('%Y-%m-%d')
|
# ensemble 因子需要更长的历史(252天窗口,预加载 2 倍)
|
||||||
|
preload_days = 504 if self.config.factor.type == FactorType.SLOPE_R2_ENSEMBLE else self.n_days * 2
|
||||||
|
preload_start = (pd.Timestamp(start_date) - timedelta(days=preload_days)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
print("\n[1/4] Preloading signal sources (index raw)...")
|
print("\n[1/4] Preloading signal sources (index raw)...")
|
||||||
for code in self.signal_codes:
|
for code in self.signal_codes:
|
||||||
@@ -499,22 +553,19 @@ class SimpleRotationStrategy:
|
|||||||
df = self.index_data[signal_code]
|
df = self.index_data[signal_code]
|
||||||
mask = df.index <= date
|
mask = df.index <= date
|
||||||
recent = df.loc[mask]
|
recent = df.loc[mask]
|
||||||
if len(recent) < self.n_days:
|
|
||||||
|
# ensemble 因子需要更长的窗口(252天)
|
||||||
|
required_days = 252 if self.config.factor.type == FactorType.SLOPE_R2_ENSEMBLE else self.n_days
|
||||||
|
|
||||||
|
if len(recent) < required_days:
|
||||||
return None
|
return None
|
||||||
prices = recent['close'].values[-self.n_days:]
|
prices = recent['close'].values[-required_days:]
|
||||||
if len(prices) >= 4 and is_crash(prices):
|
|
||||||
|
base_momentum = self._compute_base_momentum(prices)
|
||||||
|
|
||||||
|
if is_crash(prices[-self.n_days:]): # crash filter 仍然用 n_days 窗口
|
||||||
return 0.0
|
return 0.0
|
||||||
# Dispatch based on factor type
|
return base_momentum
|
||||||
ft = self.config.factor.type
|
|
||||||
if ft == FactorType.VOL_ADJUSTED_MOMENTUM:
|
|
||||||
return vol_adjusted_momentum_score(prices)
|
|
||||||
elif ft == FactorType.SLOPE_R2:
|
|
||||||
return slope_r2_score(prices)
|
|
||||||
elif ft == FactorType.STANDARDIZED_SLOPE:
|
|
||||||
return standardized_slope_score(prices)
|
|
||||||
elif ft == FactorType.MOMENTUM:
|
|
||||||
return momentum_score(prices)
|
|
||||||
return weighted_momentum_score(prices)
|
|
||||||
|
|
||||||
def _compute_raw_momentum(self, signal_code: str, date: pd.Timestamp) -> Optional[float]:
|
def _compute_raw_momentum(self, signal_code: str, date: pd.Timestamp) -> Optional[float]:
|
||||||
"""Compute momentum using the configured factor function.
|
"""Compute momentum using the configured factor function.
|
||||||
@@ -525,15 +576,40 @@ class SimpleRotationStrategy:
|
|||||||
df = self.index_data[signal_code]
|
df = self.index_data[signal_code]
|
||||||
mask = df.index <= date
|
mask = df.index <= date
|
||||||
recent = df.loc[mask]
|
recent = df.loc[mask]
|
||||||
if len(recent) < self.n_days:
|
|
||||||
|
# ensemble 因子需要更长的窗口(252天)
|
||||||
|
required_days = 252 if self.config.factor.type == FactorType.SLOPE_R2_ENSEMBLE else self.n_days
|
||||||
|
|
||||||
|
if len(recent) < required_days:
|
||||||
return None
|
return None
|
||||||
prices = recent['close'].values[-self.n_days:]
|
prices = recent['close'].values[-required_days:]
|
||||||
if len(prices) >= 4 and is_crash(prices):
|
|
||||||
return 0.0
|
|
||||||
if self.config.factor.type == FactorType.VOL_ADJUSTED_MOMENTUM:
|
if self.config.factor.type == FactorType.VOL_ADJUSTED_MOMENTUM:
|
||||||
|
base_momentum = weighted_momentum_score(prices[-self.n_days:])
|
||||||
|
else:
|
||||||
|
# Use the same score function as ranking
|
||||||
|
base_momentum = self._compute_base_momentum(prices)
|
||||||
|
|
||||||
|
if is_crash(prices[-self.n_days:]):
|
||||||
|
return 0.0
|
||||||
|
return base_momentum
|
||||||
|
|
||||||
|
def _compute_base_momentum(self, prices: np.ndarray) -> float:
|
||||||
|
"""Compute base momentum score without crash filter."""
|
||||||
|
ft = self.config.factor.type
|
||||||
|
if ft == FactorType.VOL_ADJUSTED_MOMENTUM:
|
||||||
|
return vol_adjusted_momentum_score(prices)
|
||||||
|
elif ft == FactorType.SLOPE_R2:
|
||||||
|
return slope_r2_score(prices)
|
||||||
|
elif ft == FactorType.SLOPE_R2_IDM:
|
||||||
|
return slope_r2_idm_score(prices)
|
||||||
|
elif ft == FactorType.SLOPE_R2_ENSEMBLE:
|
||||||
|
return slope_r2_ensemble_score(prices)
|
||||||
|
elif ft == FactorType.STANDARDIZED_SLOPE:
|
||||||
|
return standardized_slope_score(prices)
|
||||||
|
elif ft == FactorType.MOMENTUM:
|
||||||
|
return momentum_score(prices)
|
||||||
return weighted_momentum_score(prices)
|
return weighted_momentum_score(prices)
|
||||||
# All other factors: use the same score function as ranking
|
|
||||||
return self._compute_momentum(signal_code, date)
|
|
||||||
|
|
||||||
def _generate_signals(self, date: pd.Timestamp) -> Tuple[List[str], Dict[str, float], Optional[float]]:
|
def _generate_signals(self, date: pd.Timestamp) -> Tuple[List[str], Dict[str, float], Optional[float]]:
|
||||||
"""
|
"""
|
||||||
@@ -744,6 +820,29 @@ class SimpleRotationStrategy:
|
|||||||
|
|
||||||
new_holdings, factors, bond_momentum = self._generate_signals(signal_date)
|
new_holdings, factors, bond_momentum = self._generate_signals(signal_date)
|
||||||
|
|
||||||
|
# Apply min_hold_days constraint: prevent removing assets held for less than min_hold_days
|
||||||
|
if self.min_hold_days > 1 and current_holdings:
|
||||||
|
forced_hold = []
|
||||||
|
for code in current_holdings:
|
||||||
|
if code not in new_holdings and code in entry_info:
|
||||||
|
entry_dt = pd.Timestamp(entry_info[code]['entry_date'])
|
||||||
|
held_days = (date - entry_dt).days
|
||||||
|
if held_days < self.min_hold_days:
|
||||||
|
forced_hold.append(code)
|
||||||
|
if forced_hold:
|
||||||
|
# Add forced holds back to new_holdings, remove lowest-ranked assets to make room
|
||||||
|
new_set = set(new_holdings)
|
||||||
|
for code in forced_hold:
|
||||||
|
if code not in new_set:
|
||||||
|
new_holdings.append(code)
|
||||||
|
new_set.add(code)
|
||||||
|
# Trim to select_num by removing lowest momentum assets (not forced holds)
|
||||||
|
if len(new_holdings) > self.select_num:
|
||||||
|
# Sort by momentum descending, keep forced holds first
|
||||||
|
optional = [c for c in new_holdings if c not in forced_hold]
|
||||||
|
optional.sort(key=lambda c: factors.get(c, 0), reverse=True)
|
||||||
|
new_holdings = forced_hold + optional[:self.select_num - len(forced_hold)]
|
||||||
|
|
||||||
# Data integrity check: if any currently held asset is missing from
|
# Data integrity check: if any currently held asset is missing from
|
||||||
# today's factors, abort immediately to prevent false rebalancing.
|
# today's factors, abort immediately to prevent false rebalancing.
|
||||||
if current_holdings:
|
if current_holdings:
|
||||||
@@ -1407,7 +1506,7 @@ class SimpleRotationStrategy:
|
|||||||
|
|
||||||
col_labels = ["排名", "标的名称", "市场", "指数代码", "ETF代码", "仓位", "得分",
|
col_labels = ["排名", "标的名称", "市场", "指数代码", "ETF代码", "仓位", "得分",
|
||||||
"指数最新价", "ETF收盘价", "溢价率", "状态",
|
"指数最新价", "ETF收盘价", "溢价率", "状态",
|
||||||
"进场日期", "持有天数", "盈亏", "退场日期", "退场价格"]
|
"进场日期", "持有天数", "盈亏"]
|
||||||
table_data = []
|
table_data = []
|
||||||
row_actions = [] # track action for coloring
|
row_actions = [] # track action for coloring
|
||||||
|
|
||||||
@@ -1424,12 +1523,10 @@ class SimpleRotationStrategy:
|
|||||||
pnl_s = f"{p['pnl']:+.2%}" if p['pnl'] is not None else "—"
|
pnl_s = f"{p['pnl']:+.2%}" if p['pnl'] is not None else "—"
|
||||||
weight_s = f"{p['weight']:.0%}" if p['weight'] > 0 else "—"
|
weight_s = f"{p['weight']:.0%}" if p['weight'] > 0 else "—"
|
||||||
market = code_config.get(p['code'], {}).get('market', '—')
|
market = code_config.get(p['code'], {}).get('market', '—')
|
||||||
exit_date_s = p['exit_date'].strftime('%Y-%m-%d') if p.get('exit_date') else "—"
|
|
||||||
exit_price_s = f"{p['exit_price']:.3f}" if p.get('exit_price') else "—"
|
|
||||||
table_data.append([
|
table_data.append([
|
||||||
rank, p['name'], market, p['code'], p['etf'], weight_s,
|
rank, p['name'], market, p['code'], p['etf'], weight_s,
|
||||||
score_s, idx_s, etf_s, prem_s, p['action'],
|
score_s, idx_s, etf_s, prem_s, p['action'],
|
||||||
entry_date_s, days_s, pnl_s, exit_date_s, exit_price_s
|
entry_date_s, days_s, pnl_s
|
||||||
])
|
])
|
||||||
row_actions.append(p['action'])
|
row_actions.append(p['action'])
|
||||||
|
|
||||||
@@ -1597,7 +1694,7 @@ class SimpleRotationStrategy:
|
|||||||
ax5.grid(True, alpha=0.3)
|
ax5.grid(True, alpha=0.3)
|
||||||
|
|
||||||
chart_path = output_dir / 'simple_rotation_report.png'
|
chart_path = output_dir / 'simple_rotation_report.png'
|
||||||
plt.savefig(str(chart_path), dpi=150, bbox_inches="tight")
|
plt.savefig(str(chart_path), dpi=300, bbox_inches="tight")
|
||||||
plt.close()
|
plt.close()
|
||||||
print(f" + Report: {chart_path}")
|
print(f" + Report: {chart_path}")
|
||||||
|
|
||||||
|
|||||||
112
rotation/test_start_year_analysis.py
Normal file
112
rotation/test_start_year_analysis.py
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"""
|
||||||
|
Test different start years with select_num=1
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import yaml
|
||||||
|
from pathlib import Path
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
PROJECT_ROOT = Path(__file__).parent.parent
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
load_dotenv(PROJECT_ROOT / '.env')
|
||||||
|
|
||||||
|
from rotation.config_loader import load_rotation_config
|
||||||
|
from rotation.simple_rotation import SimpleRotationStrategy
|
||||||
|
|
||||||
|
|
||||||
|
def run_test(start_date: str, select_num: int) -> dict:
|
||||||
|
"""Run backtest with specified start date and select_num."""
|
||||||
|
config_path = PROJECT_ROOT / 'rotation' / 'config_simple.yaml'
|
||||||
|
|
||||||
|
with open(config_path, 'r') as f:
|
||||||
|
config = yaml.safe_load(f)
|
||||||
|
|
||||||
|
config['backtest']['start_date'] = start_date
|
||||||
|
config['rotation']['select_num'] = select_num
|
||||||
|
|
||||||
|
temp_config_path = PROJECT_ROOT / 'rotation' / 'temp_config.yaml'
|
||||||
|
with open(temp_config_path, 'w') as f:
|
||||||
|
yaml.dump(config, f)
|
||||||
|
|
||||||
|
try:
|
||||||
|
strategy = SimpleRotationStrategy(str(temp_config_path))
|
||||||
|
result = strategy.run()
|
||||||
|
return result['metrics']
|
||||||
|
finally:
|
||||||
|
if temp_config_path.exists():
|
||||||
|
temp_config_path.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
select_num = 1
|
||||||
|
years = [2020, 2021, 2022, 2023, 2024, 2025]
|
||||||
|
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"Testing select_num={select_num} with different start years")
|
||||||
|
print(f"{'='*80}")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
|
||||||
|
for year in years:
|
||||||
|
start_date = f"{year}-01-01"
|
||||||
|
print(f"\nTesting start_date={start_date}...")
|
||||||
|
|
||||||
|
try:
|
||||||
|
metrics = run_test(start_date, select_num)
|
||||||
|
results.append({
|
||||||
|
'start_year': year,
|
||||||
|
'start_date': start_date,
|
||||||
|
'select_num': select_num,
|
||||||
|
'total_return': metrics.get('total_return', 0),
|
||||||
|
'annual_return': metrics.get('annual_return', 0),
|
||||||
|
'max_drawdown': metrics.get('max_drawdown', 0),
|
||||||
|
'sharpe_ratio': metrics.get('sharpe_ratio', 0),
|
||||||
|
'rebalance_count': metrics.get('rebalance_count', 0),
|
||||||
|
'win_rate': metrics.get('win_rate', 0),
|
||||||
|
})
|
||||||
|
print(f" Total Return: {metrics.get('total_return', 0)*100:.2f}%")
|
||||||
|
print(f" Annual Return: {metrics.get('annual_return', 0)*100:.2f}%")
|
||||||
|
print(f" Max Drawdown: {metrics.get('max_drawdown', 0)*100:.2f}%")
|
||||||
|
print(f" Sharpe Ratio: {metrics.get('sharpe_ratio', 0):.3f}")
|
||||||
|
print(f" Rebalance Count: {metrics.get('rebalance_count', 0)}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e}")
|
||||||
|
results.append({
|
||||||
|
'start_year': year,
|
||||||
|
'start_date': start_date,
|
||||||
|
'select_num': select_num,
|
||||||
|
'error': str(e)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Print summary table
|
||||||
|
print(f"\n{'='*80}")
|
||||||
|
print(f"SUMMARY TABLE (select_num={select_num})")
|
||||||
|
print(f"{'='*80}")
|
||||||
|
print(f"{'Start Year':<12} {'Total Return':<15} {'Annual Return':<15} {'Max Drawdown':<15} {'Sharpe':<10} {'Rebal':<8}")
|
||||||
|
print(f"{'-'*80}")
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
if 'error' in r:
|
||||||
|
print(f"{r['start_year']:<12} {'ERROR':<15}")
|
||||||
|
else:
|
||||||
|
print(f"{r['start_year']:<12} {r['total_return']*100:>13.2f}% {r['annual_return']*100:>13.2f}% {r['max_drawdown']*100:>13.2f}% {r['sharpe_ratio']:>9.3f} {r['rebalance_count']:>7}")
|
||||||
|
|
||||||
|
# Save results to YAML
|
||||||
|
output_path = PROJECT_ROOT / 'rotation' / 'results' / 'start_year_analysis.yaml'
|
||||||
|
output_path.parent.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
with open(output_path, 'w') as f:
|
||||||
|
yaml.dump({
|
||||||
|
'select_num': select_num,
|
||||||
|
'test_date': datetime.now().isoformat(),
|
||||||
|
'results': results
|
||||||
|
}, f, default_flow_style=False)
|
||||||
|
|
||||||
|
print(f"\nResults saved to: {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
48
start_ssh_tunnel.sh
Normal file
48
start_ssh_tunnel.sh
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# SSH 隧道启动脚本
|
||||||
|
# 通过香港 ECS 建立本地 SOCKS5 代理,供 Clash 使用
|
||||||
|
|
||||||
|
KEY_PATH="/Users/aszer/code/etf/hk_ecs.pem"
|
||||||
|
HOST="root@8.218.167.69"
|
||||||
|
LOCAL_PORT=1080
|
||||||
|
REMOTE_PORT=1080
|
||||||
|
|
||||||
|
# 检查是否已有隧道在运行
|
||||||
|
if pgrep -f "ssh.*-D.*${LOCAL_PORT}" > /dev/null 2>&1; then
|
||||||
|
echo "✓ SSH 隧道已在运行 (本地端口: ${LOCAL_PORT})"
|
||||||
|
pgrep -f "ssh.*-D.*${LOCAL_PORT}" | head -1
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 确保密钥权限正确
|
||||||
|
chmod 600 "$KEY_PATH" 2>/dev/null
|
||||||
|
|
||||||
|
# 修复可能的 Windows 换行符问题
|
||||||
|
sed -i '' 's/\r$//' "$KEY_PATH" 2>/dev/null
|
||||||
|
|
||||||
|
echo "正在建立 SSH 隧道..."
|
||||||
|
echo " 服务器: ${HOST}"
|
||||||
|
echo " 本地 SOCKS5 端口: ${LOCAL_PORT}"
|
||||||
|
echo " 密钥: ${KEY_PATH}"
|
||||||
|
|
||||||
|
ssh -N -D 127.0.0.1:${LOCAL_PORT} \
|
||||||
|
-o StrictHostKeyChecking=no \
|
||||||
|
-o UserKnownHostsFile=/dev/null \
|
||||||
|
-o ServerAliveInterval=60 \
|
||||||
|
-o ServerAliveCountMax=3 \
|
||||||
|
-i "$KEY_PATH" \
|
||||||
|
-p 22 \
|
||||||
|
"$HOST" &
|
||||||
|
|
||||||
|
SSH_PID=$!
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
if kill -0 $SSH_PID 2>/dev/null; then
|
||||||
|
echo "✓ SSH 隧道已建立 (PID: ${SSH_PID})"
|
||||||
|
echo " 本地代理: socks5://127.0.0.1:${LOCAL_PORT}"
|
||||||
|
echo ""
|
||||||
|
echo "现在可以启动 Clash 使用 'SSH香港' 节点了"
|
||||||
|
else
|
||||||
|
echo "✗ SSH 隧道建立失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Reference in New Issue
Block a user