绩效指标交叉验证报告(我方实现 vs quantstats)
差别有两处,且方向一致(quantstats 的索提诺系统性更高):
绩效指标交叉验证报告(我方实现 vs quantstats)
生成:
scripts/…无需手动跑;由crosscheck_quantstats.py输出。quantstats 0.0.85 | numpy 2.1.3 | pandas 2.2.3
序列 13 条 | 比对项 84 | FAIL 7 | PASS 42
一、结论(三句话)
- 手写核心实现通过验证:波动率 / 最大回撤 共 28 项 全部 PASS,夏普也全部 PASS。
- 唯一实质差异是索提诺 —— 两边定义不同(见 §三),非 bug。
- 年化口径: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/BuyHold | 0.862 | 0.918 | 6.1% |
| engine_v1/Cash6040 | 0.862 | 0.918 | 6.1% |
| engine_v1/D-L0L2VT | 0.361 | 0.607 | 40.5% |
| engine_v1/M-Adp60 | 0.792 | 0.886 | 10.6% |
| engine_v1/W-Adp60 | 0.749 | 0.800 | 6.4% |
| engine_v1/W-Adp60NoEv | 0.793 | 0.845 | 6.1% |
| portfolio/weekly(重建) | 1.431 | 1.513 | 5.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) | quantstats | v1 vs qs | multi vs qs |
|---|---|---|---|---|---|
| g2_equal_WW_6bps | 0.1138 | 0.1138 | 0.1139 | 0.08% | 0.05% |
| g2_erc_WW_6bps | 0.1054 | 0.1054 | 0.1055 | 0.08% | 0.05% |
| g2_invvol_WW_6bps | 0.1054 | 0.1054 | 0.1055 | 0.08% | 0.05% |
| g3_equal_WW_6bps | 0.1071 | 0.1071 | 0.1072 | 0.07% | 0.05% |
| g3_erc_WW_6bps | 0.1011 | 0.1011 | 0.1012 | 0.09% | 0.05% |
| g3_invvol_WW_6bps | 0.0979 | 0.0979 | 0.0979 | 0.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)vsreturns.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/BuyHold | 0.2380 | 0.2382 | 0.10% |
| engine_v1/Cash6040 | 0.2330 | 0.2326 | 0.15% |
| engine_v1/D-L0L2VT | 0.1570 | 0.1571 | 0.08% |
| engine_v1/M-Adp60 | 0.1860 | 0.1863 | 0.15% |
| engine_v1/W-Adp60 | 0.1940 | 0.1937 | 0.16% |
| engine_v1/W-Adp60NoEv | 0.2130 | 0.2127 | 0.13% |
| engine_multi/g2_equal_WW_6bps | 0.2183 | 0.2184 | 0.05% |
| engine_multi/g2_erc_WW_6bps | 0.3318 | 0.3319 | 0.05% |
| engine_multi/g2_invvol_WW_6bps | 0.3318 | 0.3319 | 0.05% |
| engine_multi/g3_equal_WW_6bps | 0.2302 | 0.2303 | 0.05% |
| engine_multi/g3_erc_WW_6bps | 0.2401 | 0.2402 | 0.05% |
| engine_multi/g3_invvol_WW_6bps | 0.2214 | 0.2215 | 0.05% |
| portfolio/weekly(重建) | 0.4206 | 0.4208 | 0.05% |
| portfolio/weekly(重建) | 0.4210 | 0.4206 | 0.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。