6.8 KiB
6.8 KiB
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)官方交易日历。
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.indexrun_backtest()中用 A 股日历对齐 signals:signals.reindex(a_share_dates, method='ffill')
# Before
a_share_dates = index_close.index # 联合日历(含海外交易日)
# After
a_share_dates = data.get('a_share_dates') # SSE 官方日历
修复 2: 信号对齐到 A 股日历
run_backtest() 中增加信号 reindex 逻辑,确保回测只在 A 股交易日执行:
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
修复: 加权动量计算鲁棒性
# 新增:价格下界 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 可视化)