Files
etf/docs/experiments/982fbe2后代码变更总结.md
aszerW 43ce8056f1 docs: 添加 982fbe2 后代码变更总结文档
记录 framework_v2 配置系统重构和策略框架实现的关键变更
2026-05-24 14:27:06 +08:00

198 lines
6.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 982fbe2 后代码变更总结
**基准 commit**: `982fbe2` — fix: 修复跨市场收益率计算Bug
**变更时间**: 2026-05-22 ~ 2026-05-23
**状态**: 未提交working tree 修改)
---
## 变更文件总览
| 文件 | 操作 | 改动量 | 类别 |
|------|------|--------|------|
| `datasource/tushare_source.py` | 修改 | +99 行 | 数据层新增方法 |
| `strategies/rotation/strategy.py` | 修改 | +25/-15 行 | 核心Bug修复 |
| `strategies/rotation/config.yaml` | 修改 | +8/-10 行 | 标的配置修正 |
| `strategies/shared/factors/momentum.py` | 修改 | +5/-1 行 | 因子鲁棒性 |
| `scripts/export_backtest_detail.py` | 新增 | ~480 行 | 回测数据导出工具 |
| `results/backtest_viewer.html` | 新增 | ~650 行 | HTML回放器 |
---
## 一、datasource/tushare_source.py
### 新增方法 1: `fetch_etf_adj()`
获取 ETF 后复权价格数据。通过 `fund_daily` + `fund_adj` 手动计算后复权价格,消除份额折算(拆分)对收益率计算的影响。
**关键实现**
- `fund_adj` 单次限 2000 条,按 5 年分段请求再拼接
- 输出 `close_hfq = close × adj_factor`
- 包含 `open`, `close`, `adj_factor`, `close_hfq` 等列
### 新增方法 2: `fetch_trade_cal()`
获取 A 股(上交所 SSE官方交易日历。
```python
def fetch_trade_cal(self, start_date: str, end_date: str) -> pd.DatetimeIndex:
pro.trade_cal(exchange='SSE', start_date=..., end_date=..., is_open='1')
```
**修复的问题**:之前策略使用 `index_close.index`(联合日历)包含海外独有交易日和周日,每年多约 70 天,导致 CAGR 被年数摊薄、回撤虚增。详见 `联合日历Bug详解.md`
---
## 二、strategies/rotation/strategy.py
### 修复 1: 使用 SSE 官方交易日历
**问题**`compute_factors()` 使用 `index_close.index`(所有标的日期的并集),包含非 A 股交易日。
**修复**
- `_get_data_from_flask_api()``_get_data_from_local()` 中调用 `fetch_trade_cal()` 获取 SSE 日历,写入 `data['a_share_dates']`
- `compute_factors()` 优先使用 `data['a_share_dates']` 而非 `index_close.index`
- `run_backtest()` 中用 A 股日历对齐 signals`signals.reindex(a_share_dates, method='ffill')`
```python
# Before
a_share_dates = index_close.index # 联合日历(含海外交易日)
# After
a_share_dates = data.get('a_share_dates') # SSE 官方日历
```
### 修复 2: 信号对齐到 A 股日历
`run_backtest()` 中增加信号 reindex 逻辑,确保回测只在 A 股交易日执行:
```python
if a_share_dates is not None:
signals = signals.reindex(a_share_dates, method='ffill').dropna(subset=[signals.columns[0]])
```
---
## 三、strategies/rotation/config.yaml
### 修改: 标的替换
| 变更 | 原配置 | 新配置 | 原因 |
|------|--------|--------|------|
| 有色金属信号源 | `HG=F`COMEX 铜期货) | `399395.SZ`(国证有色金属指数) | HG=F 与 159980.SZ 日相关性仅 0.38属于标的错配399395.SZ 相关性 0.56 |
| 原油 | `CL=F` + `160723.SZ` | 删除 | CL=F 与 160723.SZ 相关性仅 0.12160723 是 LOF 有 QDII 额度限制 |
**影响**:标的从 12 只缩减为 10 只,消除了两对信号-执行严重错配的标的。
---
## 四、strategies/shared/factors/momentum.py
### 修复: 加权动量计算鲁棒性
```python
# 新增:价格下界 clip防止 log(0) 或 log(负数)
prices = np.clip(prices, 0.01, None)
y = np.log(prices)
# 新增:异常值检测
if np.any(np.isnan(y)) or np.any(np.isinf(y)):
return 0.0
```
**修复的问题**:某些历史数据中出现 0 值或极端异常值,`np.log(0)` 产生 `-inf` 导致后续回归计算崩溃。
---
## 五、scripts/export_backtest_detail.py新增
模式 B指数信号 + ETF 收益)的逐日回测明细导出脚本。
**输出**`results/backtest_detail.json`~5.2 MB1542 天 × 10 标的)
**每日每标的记录字段**
| 字段 | 说明 |
|------|------|
| `index_close` | 指数收盘价 |
| `momentum` | 加权动量得分 |
| `rank` | 动量排名BOND 不参与) |
| `threshold` | V3 动态阈值 |
| `above_threshold` | 是否过阈值 |
| `etf_close` / `etf_open` | ETF 原始价格 |
| `etf_close_hfq` | ETF 后复权收盘价 |
| `premium` | ETF 溢价率 |
| `etf_return_ctc` | ETF close-to-close 日收益率 |
| `index_return` | 指数日收益率 |
| `is_held` | 是否在持仓 |
| `entry_date` / `entry_price_etf` / `entry_price_idx` | 入场信息 |
| `holding_days` | 持有天数 |
| `cum_return_etf` / `cum_return_idx` | 累计收益ETF/指数) |
**关键实现**
- 因子计算:先在原始日历计算,再 ffill 对齐到 A 股日历(与 strategy.py 一致)
- 溢价率:通过 `fetch_etf_nav()` 获取基金净值,`(ETF收盘价 - NAV) / NAV`
- NAV 去重:`nav_raw[~nav_raw.index.duplicated(keep='last')]`
- 净值扩展:信号前的日期填充 nav=1.0
---
## 六、results/backtest_viewer.html新增
单文件 HTML 回放器,用于人工逐日验证回测数据准确性。
**功能**
- 文件选择器加载 JSON
- Canvas 净值曲线(点击/拖拽定位日期)
- 策略统计面板CAGR、最大回撤、夏普、Calmar、胜率、换仓次数、平均持仓天数
- 全标的排名表(按动量排序,含市场分组、溢价率、动态阈值分隔行)
- 持仓卡片ETF + 指数双累计收益)
- 调仓明细栏
- 键盘快捷键:← → 翻页Space 播放/暂停
- 跳转前/后调仓日按钮
---
## 修复前后指标对比
**回测区间**2020-01-02 ~ 2026-05-19使用 `own_strategy_etf_vs_index.py` 四模式对比
| 指标 | 修复前 Mode A | 修复后 Mode A | 变化 |
|------|-------------|-------------|------|
| CAGR | 15.63% | 11.80% | -3.83% |
| 最大回撤 | — | -29.49% | — |
| 夏普 | — | 0.818 | — |
| 指标 | 修复前 Mode B | 修复后 Mode B | 变化 |
|------|-------------|-------------|------|
| CAGR | 26.13% | 28.07% | +1.94% |
| 最大回撤 | — | -13.34% | — |
| 夏普 | — | 1.685 | — |
| 模式 | CAGR | 最大回撤 | 夏普 | Calmar |
|------|------|----------|------|--------|
| A (指数→指数) | 11.80% | -29.49% | 0.818 | 0.400 |
| B (指数→ETF) | 28.07% | -13.34% | 1.685 | 2.104 |
| C (ETF→ETF) | 21.27% | -13.27% | 1.304 | 1.603 |
| D (ETF修正) | 17.93% | -13.36% | 1.130 | 1.342 |
**差异分解**
- 收益差异 B-A: +16.27%/年ETF 收益 vs 指数收益)
- 信号差异 C-B: -6.80%/年ETF 信号 vs 指数信号)
- 隔夜偏差 C-D: +3.34%/年close 买入虚高 vs open 买入现实)
---
## 依赖关系
```
config.yaml (标的配置)
tushare_source.py (fetch_trade_cal / fetch_etf_adj)
strategy.py (compute_factors → generate_signals → run_backtest)
export_backtest_detail.py (导出 JSON)
backtest_viewer.html (加载 JSON 可视化)
```