先给你看我现在系统里两个"滚动表现"产物的真实数字(同一条策略,2026-10-08 生成):
注意 60 日回撤 比 20 日还深一倍多(−25.93% vs −11.21%)——这说明回撤是"从历史水位上刻下来的",不是本窗口自己长出来的。这篇就讲这个被大多数人忽略的细节,以及我为此栽过的跟头。
我的系统有一套"滚动表现"统计(给盯盘和复盘页用)。有天我把它的数字与一套官方回放数据对账:同一策略、同一 20 日窗口,收益和回撤都对不上,回撤差距尤其大。
第一反应是查 bug。两边各自重算、逐日核对、逐笔核对——单独看每一方都自洽。最后发现:根本不是 bug,是口径定义不同。
把两种算法摊开(伪代码级):
# 口径 A:窗口独立重跑(错误示范) window = bars[start:end] # 只取窗口内的数据 nav = run_backtest(window, initial_cash=1) # 资金重置、持仓清零,从零开始 ret_20d = nav[-1] / nav[0] - 1 # 以窗口第一天为基期 # 口径 B:全期序列切片(正确口径) nav_full = run_backtest(bars[:end], ...) # 从更早(我的实现是全历史)连续跑完整序列 segment = nav_full[start_idx: end_idx + 1] # 切片取窗口段 ret_20d = nav_full[end_idx] / nav_full[start_idx - 1] - 1 # 基期 = 窗口首日的前一交易日
差别一句话:口径 A 假设"20 天前我干干净净空仓开始";口径 B 承认"20 天前我手里就拿着仓位和浮盈浮亏"。真实账户当然不会每 20 天清零一次——所以官方用的是口径 B,我的系统最初是口径 A。
收益的差异还可能温和,回撤会直接夸张化:
到我这条数据上:60 日口径 B 的回撤 −25.93%,本质是"从更早的高点刻下来的"——这个数字对持仓者才有意义(他真的经历过那一段)。而口径 A 算出来的会小得多,给人"最近还行"的错觉。
1. 对齐生产口径:滚动统计全部改为"全期序列尾部切片",基期定义为窗口首日的前一交易日的收盘状态;
2. 让口径随身走——每个滚动产物带上锚点字段(这是我的产物真实结构):
{
"window_days": 20,
"start": "2026-09-03", "end": "2026-10-08",
"base_date": "2026-09-02",
"anchors": {"method": "full_series_slice", "full_start": "2026-01-05"}
}
以后任何人(包括半年后的我)看到数字,都能当场知道它怎么算的、基期在哪、全序列从哪开始。
3. 验收用逐值对账,不用"差不多":修完后与官方回放数据比对——20 日项命中到 −5.5028% / −11.2116%、60 日项命中到 −3.8734% / −25.9307%(小数点后四位全等),才算结案。对账断言只有一条:逐值相等;不搞"误差小于 x% 就通过"。
1. 任何"近 N 日"指标,先把基期写下来(窗口首日 还是 首日前一交易日?);
2. 回撤是高水位刻出来的——先定义"高水位从哪开始追溯";
3. 自研指标与外部对不上时,先怀疑口径、再怀疑 bug——口径差异是静默的,bug 通常会喊两声;
4. 每个统计产物带上计算口径锚点字段(方法名 + 基期日期),这是给未来的自己留的说明书;
5. 验收方式用逐值对账(能到几位就到几位),不用"看起来接近"。
本站内容为个人研究记录与方法分享,模拟,不构成投资建议;据此操作风险自负。
Joyous 的技术笔记 · 沪ICP备2026053770号
隐私最小化:不设 cookie、无表单、无第三方脚本;访问统计仅取服务端日志(≤30 天,仅聚合),不导出 IP。