From 43ce8056f12c9098b2b41adcad34730309e9f6ad Mon Sep 17 00:00:00 2001 From: aszerW Date: Sun, 24 May 2026 14:27:06 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E6=B7=BB=E5=8A=A0=20982fbe2=20?= =?UTF-8?q?=E5=90=8E=E4=BB=A3=E7=A0=81=E5=8F=98=E6=9B=B4=E6=80=BB=E7=BB=93?= =?UTF-8?q?=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 记录 framework_v2 配置系统重构和策略框架实现的关键变更 --- docs/experiments/982fbe2后代码变更总结.md | 197 ++++++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 docs/experiments/982fbe2后代码变更总结.md diff --git a/docs/experiments/982fbe2后代码变更总结.md b/docs/experiments/982fbe2后代码变更总结.md new file mode 100644 index 0000000..05ef425 --- /dev/null +++ b/docs/experiments/982fbe2后代码变更总结.md @@ -0,0 +1,197 @@ +# 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.12,160723 是 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 MB,1542 天 × 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 可视化) +```