框架

绩效指标交叉验证报告(我方实现 vs quantstats)

差别有两处,且方向一致(quantstats 的索提诺系统性更高):

回测 · 资产·机制 · 回测/perf_audit/crosscheck_report.md · 7908 B

绩效指标交叉验证报告(我方实现 vs quantstats)

生成:scripts/… 无需手动跑;由 crosscheck_quantstats.py 输出。

quantstats 0.0.85 | numpy 2.1.3 | pandas 2.2.3

序列 13 条 | 比对项 84 | FAIL 7 | PASS 42

一、结论(三句话)

  1. 手写核心实现通过验证:波动率 / 最大回撤 共 28 项 全部 PASS,夏普也全部 PASS。
  2. 唯一实质差异是索提诺 —— 两边定义不同(见 §三),非 bug。
  3. 年化口径:engine_v1 与 quantstats 同源(都是 bar 数 / ppy,差 0.08% = round(4) 量化误差);engine_multi 已于 2026-09-27(v5.3.2)统一为 bar/ppy,差 0.36% → 0.05%,仓库内部年化口径分歧已消除。

二、判定分层与阈值

指标判据理由
波动率 · 最大回撤绝对差 ≤ 1e-09(engine_v1 输出经 round(4) → 放宽到 5e-5)定义相同 → 真正的一致性检验
夏普相对差 < 0.5%可能 rf / ddof 差异
索提诺相对差 < 5%下行偏差定义不同,须记录来源
卡玛 · 年化不作判据口径分歧应记录而非判错(见 §四)

三、索提诺:两边定义确实不同(已定位到源码)

# quantstats/stats.py::sortino(原文)
downside = sqrt( (returns[returns < 0] ** 2).sum() / returns.count() )
res      = returns.mean() / downside * sqrt(periods)

# 我们(duckerry_backtest.py / multiasset_backtest.py)
dn  = x[x < 0].std()          # 负收益的标准差,ddof=1,仅除以负样本数
res = x.mean() / dn * sqrt(ppy)

差别有两处,且方向一致(quantstats 的索提诺系统性更高):

  • 分母口径:quantstats 除以总观测数,我们除以负样本数(后者更大 → 分母更大 → 比率更低)
  • 是否去均值:quantstats 用原始负收益 RMS,我们减去负收益均值(std 的定义)

实测差异(我方 = engine_v1):

序列我方quantstats相对差
engine_v1/BuyHold0.8620.9186.1%
engine_v1/Cash60400.8620.9186.1%
engine_v1/D-L0L2VT0.3610.60740.5%
engine_v1/M-Adp600.7920.88610.6%
engine_v1/W-Adp600.7490.8006.4%
engine_v1/W-Adp60NoEv0.7930.8456.1%
portfolio/weekly(重建)1.4311.5135.4%

处置:索提诺不改变任何结论(它不在任何表的主指标之列)。保留我方定义,但在引用索提诺时必须注明是哪种定义;如需与 quantstats 对齐,改用其 RMS 口径即可。

四、年化口径(已于 2026-09-27 / v5.3.2 统一)

quantstats.stats.cagr 源码:years = returns.count() / periods —— bar 数 / 年化期数。

序列engine_v1(bar/ppy)engine_multi(bar/ppy)quantstatsv1 vs qsmulti vs qs
g2_equal_WW_6bps0.11380.11380.11390.08%0.05%
g2_erc_WW_6bps0.10540.10540.10550.08%0.05%
g2_invvol_WW_6bps0.10540.10540.10550.08%0.05%
g3_equal_WW_6bps0.10710.10710.10720.07%0.05%
g3_erc_WW_6bps0.10110.10110.10120.09%0.05%
g3_invvol_WW_6bps0.09790.09790.09790.04%0.05%

结论:

  • engine_v1 的 bar/ppy 口径与 quantstats 同源;残留 0.08% 即 round(4) 的量化误差。
  • engine_multi 已于 2026-09-27(v5.3.2)统一为 bar/ppy:metrics() 的 years 由日历跨度 (idx[-1]-idx[0]).days/365.25 改为 len(eq)/ppy → 相对差 0.36% → 0.05%,仓库内部年化口径分歧已消除。(残留 0.05% 是 len(eq) vs returns.count() 的 1 个观测之差,属定义细节。)
  • 拍板(仍有效):年化一律以 bar 数 / ppy 为准(= engine_v1 = engine_multi = quantstats)。

五、portfolio 周净值重建的向量化对拍

  • 重建序列:2066 个周 bar,推断 ppy = 52
  • 向量化版本 vs portfolio.target_weights() 抽样 8 个 bar 对拍,最大绝对差 9.992007221626409e-16(机器精度)→ 重建可信。

(若不向量化,target_weights 单次 ~0.59s × 2000+ bar = O(n²),无法在 iSH 上跑完。)

六、明细

完整逐项结果见同目录 crosscheck_results.csv / crosscheck_results.json。 复现:PYTHONPATH=/var/minis/mounts/量化策略/quantstats-main python3 crosscheck_quantstats.py (约 90s;iSH 上接近 2 分钟静默 kill 阈值,必要时用 --group 分批。)

七、修订记录

v1.2(2026-09-27,P3-③ 口径同步)

  • 背景:v1.1 已定位「engine_multi 用日历跨度、与 quantstats/engine_v1 差 0.36%」是仓库内部年化口径分歧,并拍板「年化一律 bar 数 / ppy」。v1.2 即把该拍板落地到 engine_multi。
  • 改动:回测/engine_multi/multiasset_backtest.py::metrics() —— years = max((eq.index[-1]-eq.index[0]).days/365.25, 1e-6) → years = max(len(eq)/ppy, 1e-6)(复用同函数已算出的 infer_ppy)。仅年化公式,一处。
  • 证据:本脚本 §四 表 —— multi vs qs 由 0.36% → 0.05%(见上表);engine_multi 的 cagr 列由 0.1143/0.1059/… 变为 0.1138/0.1054/…,与 engine_v1 逐位一致。
  • 残留 0.05% 的解释:engine_multi 用 len(eq)(bar 数),quantstats 用 returns.count()(收益观测数 = bar 数 − 1)→ 属定义细节,非口径分歧。
  • 影响:engine_multi 五张表的「年化」「卡玛」两列已全量复算并同步 README;所有排序与结论均未改变。

v1.1(2026-09-27,监工复核修复)

  • 修复:qs.stats.calmar(r) → qs.stats.calmar(r, periods=ppy)。calmar 内部 = cagr(periods) / |max_drawdown|,漏传 periods 时用默认 252 → 所有非日频序列的 quantstats 卡玛被系统性高估(周频约 +82%、月频约 +98%);日频序列不受影响(D-L0L2VT 0.157 vs 0.1571 吻合)。
  • 影响面:零 —— 卡玛本就「不作判据」,FAIL 项仍为 7(全部为索提诺)。修复后卡玛列变为可用,且反向印证年化口径结论(见下表)。
  • ⚠ v1.0 的卡玛列(旧 CSV)不得引用。

修复后卡玛对照:

序列我方quantstats相对差
engine_v1/BuyHold0.23800.23820.10%
engine_v1/Cash60400.23300.23260.15%
engine_v1/D-L0L2VT0.15700.15710.08%
engine_v1/M-Adp600.18600.18630.15%
engine_v1/W-Adp600.19400.19370.16%
engine_v1/W-Adp60NoEv0.21300.21270.13%
engine_multi/g2_equal_WW_6bps0.21830.21840.05%
engine_multi/g2_erc_WW_6bps0.33180.33190.05%
engine_multi/g2_invvol_WW_6bps0.33180.33190.05%
engine_multi/g3_equal_WW_6bps0.23020.23030.05%
engine_multi/g3_erc_WW_6bps0.24010.24020.05%
engine_multi/g3_invvol_WW_6bps0.22140.22150.05%
portfolio/weekly(重建)0.42060.42080.05%
portfolio/weekly(重建)0.42100.42060.09%

读法(v1.1 原貌):engine_v1 相对差 0.08–0.16%(与 quantstats 同源 bar/ppy,残留为 round(4) 量化);engine_multi / portfolio 周重建 当时稳定为 0.36%(日历跨度口径)——与 §四 的年化口径分歧同源同量级,互为印证。该分歧已于 v5.3.2 消除(0.36% → 0.05%),见 §七 v1.2。