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

6.8 KiB
Raw Blame History

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.index
  • run_backtest() 中用 A 股日历对齐 signalssignals.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=FCOMEX 铜期货) 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

修复: 加权动量计算鲁棒性

# 新增:价格下界 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 可视化)