backtest-guard / 回测照妖镜
一个只做一件事的工程审查工具:把一份回测/策略代码当"嫌疑犯"过堂,逐项揪出会让回测虚高、实盘还回去的陷阱。
核心理念:让回测虚高 0.x 个夏普的坑,实盘会一次性还给你。 回测的默认假设是"乐观"——零成本、零延迟、能看到未来、永远成交;实盘的默认假设是"敌意"。本 skill 的工作就是把每一处"乐观假设"暴露出来,标注严重度,而不是替你优化收益。
何时启用
- 用户把量化策略 / 回测脚本 / 信号生成 / 因子计算 / 数据加载 / 参数寻优代码丢过来,问"能不能信""为什么实盘对不上回测"。
- 用户主动要求查未来函数、前视偏差、过拟合、数据泄漏、成交真实性。
- 在策略上实盘资金前,做一次工程尽职调查(due diligence)。
不启用:让你预测收益、荐股、给买卖点、调参求更高夏普、修实盘下单/风控逻辑、运维数据源。这些不在本 skill 职责内。
审查协议 (怎么扫一份策略仓库)
铁律:所有结论必须基于真实代码,定位到 文件:行,不臆测、不脑补。找不到证据就标"未发现/需人工确认",绝不假设"应该没问题"或"应该有问题"。
更高一层的铁律:命中可疑模式只是"线索"不是"判决"。 任何 Grep 命中后必须读上下文,区分真坑与合法用法,否则报告会被假阳性淹没、失去可信度。下文每条都标注了"合法例外",审查时务必核对。
- 定位关键代码区。用 Glob/Grep 找出四类入口:
- 数据加载与清洗(
read_csv/download/fillna/resample/merge/复权)
- 信号/特征生成(
shift/rolling/rank/指标计算/标准化)
- 执行/撮合/记账(
buy/sell/order/fill/commission/slippage/止盈止损)
- 训练/寻优/评估(
train_test_split/GridSearch/TimeSeriesSplit/argmax/sharpe/年化/复利)
- 逐类过四大陷阱清单(见下)。优先用 Grep 命中可疑模式(负数 shift、
bfill、center=True、commission=0、StandardScaler().fit_transform 在 split 前等),命中后读上下文确认,区分真坑与合法用法(例:shift(-1) 构造 label 合法,进特征矩阵就是致命未来函数;bfill 静态元数据合法,bfill 行情列致命)。
- 按严重度汇总。同一类问题合并,致命/高危优先。
- 输出体检报告(见固定模板)。完整陷阱清单见
references/backtest-pitfalls.md,本文只列每类最致命的几条。
四大类陷阱(核心)
每条格式:陷阱名 — 怎么发现(含合法例外) — 修复方向。完整 50+ 条检测点与正则/AST 提示见 references/backtest-pitfalls.md。
① 未来函数 / 数据泄漏(look-ahead / leakage)——最致命一类
- 负数 shift 引入未来值 — 搜
\.shift\(\s*-、diff(-、pct_change(-;进特征/信号即致命。合法例外:仅在显式构造 label/target/fwd_ret/y 时合法 — 修:进 signal/feature 的列一律 shift(>=1) 滞后,未来收益严格隔离在 y 列且绝不进 X。
- 同 bar 信号又同 bar 成交 — 信号用
close[t],成交价也取 close[t](同索引);backtesting.py 默认当根 close 成交、set_coc(True) 重点查 — 修:close[t] 算信号、open[t+1] 成交,信号统一 .shift(1) 再喂引擎。
- bar 时间戳语义 / resample 闭区间前视 — bar 的 timestamp 标注的是开始还是收盘?若标注开始却用了整根 bar 的 OHLC/volume 当作"该时刻可用",就是隐性前视;
resample(...).last()/agg() 把 bar 结束才知道的聚合值挂到 bar 开始;跨市场/跨时区合并(UTC 与本地时区错位)导致信号比成交早一根。合法例外:bar 标注收盘时间、且下游统一在 t 收盘后才用 t 的值,合法 — 修:统一 bar 时间戳语义(推荐标注收盘),resample 结果整体 .shift(1) 或显式注明"收盘后可用",跨市场对齐到同一时区并核对夏令时。
- 预处理在全样本上 fit —
StandardScaler/MinMaxScaler/PCA/QuantileTransformer.fit_transform 出现在 train_test_split 之前,或 fit 对象不是 X_train;手写 (df-df.mean())/df.std() 整列同理 — 修:scaler 只在训练集 fit,放进 Pipeline + TimeSeriesSplit 每折独立 fit;在线场景用 rolling/expanding point-in-time 统计。
- 后向填充 / 时间插值(仅时变行情列致命) —
bfill/backfill 把未来值灌进当前;interpolate( 对序列中间缺失用前后点 → 引入未来值(须显式 limit_direction='forward' 才安全);停牌标的最常中招。合法例外:对静态/历史不变的元数据(行业分类、合约乘数 multiplier、tick size、上市前常量属性)做 bfill 无害——必须读上下文确认该列是"时变行情"还是"静态属性",只对前者判致命 — 修:时变行情列只允许 ffill,插值 limit_direction='forward',缺失统计量取自训练集。
- rolling
center=True / 全列聚合当特征 — center=True 用到未来 w/2 根;整列 .mean()/.std()/.quantile()/.rank() 直接进信号 — 修:时序窗口 center=False 用过去,全历史统计用 expanding(),横截面 rank 按 date groupby 不跨时间。
- target leakage — 特征里混进 t+1 后才知道的列,或与 label 强相关派生列。可对每个特征与 y 算 corr 作线索(参考线
abs>0.99 重点查,非判决——强自相关特征如价格本身、慢动量、隔夜跳空与未来收益天然中高相关却合法;高相关≠leakage) — 修:命中高相关后回到"该列在 t 时点真能算出吗"做 point-in-time 可得性确认;Target encoding 用 out-of-fold,提交前跑"特征-标签相关性体检 + 可得性核对"。
- 时序用 shuffle 切分 / CV 泄漏 —
train_test_split 缺 shuffle=False,KFold/GridSearchCV 用在时序而非 TimeSeriesSplit — 修:时序 shuffle=False,CV 用 TimeSeriesSplit/Purged K-Fold(带 embargo)。
- repaint 指标 —
zigzag/fractal/pivot_high/find_peaks(对整段序列找极值=未来函数)/自实现 supertrend,事后回溯重画 — 修:换因果版本(只用历史确认),对每个指标做"截断到 t 重算,t 处值变了即 repaint"稳定性测试。
- point-in-time 缺失 — 用"当前"成分股回测历史、财报按报告期而非公告日对齐、后复权价隐含未来调整因子 — 修:成分股按历史生效区间、财报按公告日加发布滞后,价格用前复权或每时点已知调整因子;无 PIT 数据须显式标注结果不可信。
② 过拟合 / 数据窥探 / 幸存者偏差(overfitting / data snooping)
- In-sample 寻优无样本外 — 参数寻优用的数据段与最终报告指标同段;无
train/test、无 walk-forward — 修:按时间切,headline 指标只来自从未参与调参的数据;用 walk-forward / anchored 滚动重优。
- 网格搜索只报最优参(cherry-pick max) —
argmax/idxmax/study.best_params 紧跟一次大网格扫描后直接当"策略";无 deflated/PBO/Bonferroni 惩罚 — 修:算 Deflated Sharpe(计入试验次数 N、收益偏度峰度)或 Benjamini-Hochberg/Bonferroni,优先选参数高原而非尖峰,报 OOS 分布而非单个赢家。
- 同数据反复试错(p-hacking) — 大量注释掉的备选规则、奇异 magic number(
rsi<27.3、vol>0.0184)、上百次追指标的提交 — 修:CSCV 估 PBO,预注册假设与参数范围,锁一个只碰一次的 lockbox 期。
- 幸存者偏差 — 用"当前"指数成分/写死 ticker 列表回测历史,delisted/破产/退市的输家被悄悄剔除(LUNA/FTT 直接消失) — 修:用 point-in-time universe 含退市收益,按 rebalance 日重建成分,crypto 含已下架币并记最后成交价。
- look-ahead universe / 选股偏差 — 用全样本统计选标的("全期市值 top50""有完整历史的币");
df.dropna() 跨标的丢未上市/已退市名 — 修:universe 按时点用 trailing 数据选,绝不要求标的有超过当前 bar 的数据。
- 自由参数过多(curve fitting) — 同时优化 >5-7 个参数、大量 magic number、free param 与成交笔数比失衡 — 修:从经济先验固定参数、跨资产共享,要求每参数 >>10 笔交易,丢掉在 walk-forward 中变号的旋钮。
- 样本量不足 — 短期/少交易(<30-50 笔 round trip)的夏普标准误巨大;报夏普无 t-stat/置信区间 — 修:报夏普 t≈Sharpe×√years,要求 |t|>2(多重检验后更高),block-bootstrap 出夏普分布。
- 无多重检验校正(factor zoo) — 测了一堆因子/策略按裸
pvalue<0.05 留显著的,无 fdr_bh/deflated_sharpe — 修:Benjamini-Hochberg FDR 或 Deflated Sharpe haircut,提高显著阈值(新因子 t>3),披露筛了几个候选。
- 忽略市场 regime — 只在单一行情(2020-21 crypto 牛、低波股市)验证;指标只报整段混合样本 — 修:按子区间/regime(牛熊、高低波)拆分表现,至少覆盖一个完整周期+一段压力期。
③ 成交真实性 / 成本执行(execution realism)
- 零手续费 / 不建模成本 — 找不到
commission/fees/set_commission 或值为 0;换手越高误差越离谱 — 修:按交易所真实分层费率建模(Binance 现货 taker 0.1%、合约 taker 0.04%),做成本翻倍敏感性。
- 无滑点 / 理论价精确成交 — 无
slippage/set_slippage,成交价直接取 close/open 无点差;净值曲线异常平滑 — 修:至少建模半个~一个 spread + 与下单量挂钩冲击成本(平方根法则 σ·(Q/ADV)^0.5)。
- 用 bar 内极值(high/low)当成交价 / 乐观触达顺序 — 止盈用
high、止损用 low;同 bar 同时触发时,先核实该引擎的同 bar 撮合顺序假设——若引擎默认盈利方向先成交即为坑,若引擎本已止损优先则合法 — 修:确认乐观后,按最坏假设(同 bar 双触止损优先)重估;细化到 tick 还原 bar 内路径。
- 假设无限流动性 — 下单量 > 盘口/ADV 仍全额单价成交,无
volume_limit/participation 约束 — 修:加参与率上限(单 bar ≤ 该 bar volume 的 1%-10%),超量拆单,画资金规模-夏普衰减曲线定容量。
- 零延迟 + 限价单触及即成(无排队) — 信号与成交同时刻、限价"price 触及即 100% 成交";高频/做市却无
latency/queue — 修:信号到成交加延迟(高频按真实 RTT),被动限价引入排队/成交概率模型,要求价格穿过而非仅触及。
- 能在涨跌停/熔断/无量时成交 — 无
limit_up/limit_down/halt/volume==0 过滤,在一字板(open=high=low)上成交 — 修:封板不成交、停牌(volume=0)不成交,极端行情回撤才真实。
- 忽略保证金与强平/爆仓 — 用杠杆/合约却无
liquidation/maintenance_margin,权益可深度为负仍持仓回本 — 修:每 bar mark-to-market,权益 < 维持保证金触发强平清零;报告必须统计"爆仓次数",一次清零即判失败。
- 忽略做空成本 / 资金费率 / 复权 — 空头无
borrow_fee、永续无 funding_rate、跨除权日用未复权价或前复权(未来信息) — 修:空头计提借券费、永续每结算点按 名义×funding×方向 入账、用后复权或分红现金入账口径。
- 现金/保证金无约束 — 现金为 0 甚至负仍买入=隐性无限杠杆;无
buying_power 校验 — 修:下单前校验购买力。注意区分场景:单边/无对冲策略可硬约束总仓位 ≤ 权益;多空对冲/市场中性策略 gross 敞口本就 >1(net≈0)属正常,不可用 gross ≤ 权益误杀——应约束 net 敞口与维持保证金,融资买入计利息。
④ 收益统计 / 口径错误(metric integrity)——容易被忽视的"低级致命"
- 年化因子(annualization factor)用错 — 夏普/年化收益用了与数据频率不匹配的 √N:股票日频应 √252、crypto 日频常用 √365、小时频该用 √(年化小时数);搜
* np.sqrt(252)/* 252/annualize 核对频率 — 修:annualization factor 必须匹配真实 bar 频率与该资产的交易日历,crypto 7×24 不用 252。
- 算术累加代替复利 / 收益序列拼接错误 — 用
returns.sum()/cumsum 当总收益而非 (1+r).prod()-1;跨再平衡频率或跨标的直接拼接收益序列 — 修:复利用 (1+r).cumprod(),统一收益频率口径,杠杆下注意几何收益拖累(volatility drag)。
- 夏普分母 / 无风险利率口径 — 夏普分子未减无风险利率、或分母用了含偏度的总波动而非超额收益波动;高偏度策略(卖期权式)夏普虚高 — 修:分子用超额收益,披露无风险利率口径,对高偏度策略并报 Sortino/最大回撤/偏度峰度,夏普单指标不可作唯一结论。
- 权益曲线口径混用 — 已实现盈亏(realized)与浮动盈亏(mark-to-market)混报,或回撤按已实现算而忽略浮亏 — 修:权益曲线统一 mark-to-market,最大回撤含浮动亏损。
体检报告输出格式
固定模板,直接产出(可截图、可复制)。严重度 用 emoji + 中文档级,定位精确到 文件:行。
══════════════════════════════════════════
回测体检报告 · backtest-guard
══════════════════════════════════════════
受检对象: <仓库/文件名> 扫描文件: N 个 代码行: ~X
总评: 🔴 致命 a 项 · 🟠 高危 b 项 · 🟡 中 c 项 · 🔵 低 d 项
判语: 「<这是一个策略,还是一次对历史的过拟合?>」
—— 一句不留情面但克制的整体结论
────────────── 问题清单(按严重度) ──────────────
[🔴 致命] 负数 shift 引入未来值
位置: src/features.py:84
问题: df['mom'] = df['close'].shift(-5) 将 t+5 收盘移到 t,
该列进入了 X 特征矩阵(features.py:131)。
修复: 进特征的列改 shift(>=1);未来收益只留 y/label,绝不进 X。
[🔴 致命] 全样本 fit 预处理(数据泄漏)
位置: src/train.py:42
问题: StandardScaler().fit_transform(X) 在 train_test_split(:57) 之前,
测试期 mean/std 泄漏进训练。
修复: scaler 放进 Pipeline,TimeSeriesSplit 每折独立 fit。
[🟠 高危] 零手续费 + 零滑点
位置: backtest/engine.py:––(全文件未发现 commission/slippage)
问题: 成交按 close 原值记账,日内策略年换手 ~900 次,成本被完全忽略。
修复: 按 taker 费率建模 + 半个 spread 滑点,做成本翻倍敏感性。
[🟠 高危] 年化因子用错(crypto 套用 252)
位置: metrics.py:31
问题: sharpe = mean/std * np.sqrt(252),但数据为 BTC 1h bar(7×24),
年化被低估、跨频比较失真。
修复: 按真实频率年化(crypto 用 √年化小时数),并复核复利口径。
[🟡 中] 单一 regime 验证
位置: backtest/run.py:20 (回测区间 2020-10 ~ 2021-04)
问题: 仅覆盖一段 crypto 单边牛市,无熊市/震荡压力测试。
修复: 至少覆盖一个完整周期 + 一段 2022 压力期,按 regime 拆分表现。
────────────── 偏差影响(方向判断,非收益预测) ──────────────
说明:以下仅为"剔除偏差后表现往哪边走"的方向性判断,不是收益预测、
不承诺任何数字。存在 ≥1 项致命未来函数/泄漏时,当前 headline 夏普
不可作为样本外预期。修复"负数 shift + 全样本 fit + 零成本 + 年化口径"
后,样本外表现通常会**显著低于**当前回测——具体幅度需重跑确认。
建议优先级: 先堵致命未来函数 → 再修口径/补成本滑点 → 最后做
walk-forward 重验。在致命项清零前,不建议投入实盘资金。
══════════════════════════════════════════
报告原则:每条必带 位置;找不到证据的项标"未发现/需人工确认",不硬凑;命中可疑模式但读上下文后判为合法用法的,不写进清单(或写入"已排除"附注);判语克制、不羞辱、不夸张;"偏差影响"只给方向(变低/不可信),绝不承诺具体收益数字。
严重度定义
| 严重度 |
含义 |
处置 |
| 🔴 致命 |
直接让回测看到未来或系统性虚高,实盘必然复现不了(未来函数、时间戳语义前视、全样本 fit、幸存者偏差、忽略爆仓、零滑点理论价成交) |
阻断——清零前回测结果不可信,禁止上实盘资金 |
| 🟠 高危 |
显著高估收益或低估风险,方向性失真(零手续费、无限流动性、单报最优参、样本量不足、年化/复利口径错误) |
强烈建议修——修复后预期大幅缩水 |
| 🟡 中 |
口径不严谨、特定场景失真(收益统计混用、单一 regime、tick/lot 未取整、夏普分母口径) |
建议修——影响精度与稳健性 |
| 🔵 低 |
风格/边界/工程整洁问题(warm-up 未截断、对齐告警、缺断言) |
可选——提升可信度与可维护性 |
判定基准:"这个值在该时点真能算出来吗?这笔成交实盘真能成吗?这个指标口径对吗?" 三问任一为否,按影响幅度归入致命/高危。
重要免责
本 skill 是工程审查工具,只对回测/策略代码的可信度与工程缺陷做尽职调查,目的是帮你"防自欺、做 due diligence",识别会让回测虚高、实盘亏钱的技术陷阱。
- 不构成投资建议,不预测收益、不评估策略盈利能力、不荐股、不给买卖点或仓位建议。
- 报告中"偏差影响"等表述仅指方向性影响(通常使样本外表现低于回测),不承诺、不量化任何具体收益或亏损数字。
- 通过审查 ≠ 策略能赚钱;只代表未发现本清单覆盖的工程陷阱,可能仍存在未覆盖的风险、市场结构变化或模型失效。
- 审查依赖所提供代码与可见上下文;命中可疑模式经人工二次确认为合法用法的不应判坑,反之未提供的数据/外部依赖中的陷阱本工具无法发现。
- 一切实盘交易决策与资金风险由使用者自行承担。高危操作(投入实盘资金、加杠杆)前,务必小资金隔离验证。
1---2name: backtest-guard3description: 量化回测代码的"照妖镜"——逐项审查会让回测虚高、实盘亏钱的工程陷阱(未来函数/前视偏差、过拟合/数据窥探、成交真实性/成本、收益口径错误),按严重度(致命/高危/中/低)输出一张"回测体检报告"。 何时触发:用户把量化策略/回测代码、信号生成、数据加载、参数寻优脚本丢过来让你审查可信度;或问"这个回测能信吗/为什么实盘对不上回测/帮我查未来函数/有没有过拟合"。 触发词(中):回测审查、回测照妖镜、回测体检、未来函数、前视偏差、数据泄漏、过拟合、数据窥探、幸存者偏差、回测陷阱、回测可信度、实盘对不上回测。 触发词(英):backtest audit、backtest review、look-ahead bias、lookahead、data leakage、overfitting、survivorship bias、curve fitting、backtest-guard。 不适用场景:实时实盘下单/风控逻辑本身的 bug、单纯策略调参或求 alpha、行情数据源运维、非量化的通用代码审查、要你"预测收益/荐股/给买卖建议"——这些都不在本 skill 范围。4---56# backtest-guard / 回测照妖镜78一个只做一件事的工程审查工具:把一份回测/策略代码当"嫌疑犯"过堂,逐项揪出会让回测虚高、实盘还回去的陷阱。910核心理念:**让回测虚高 0.x 个夏普的坑,实盘会一次性还给你。** 回测的默认假设是"乐观"——零成本、零延迟、能看到未来、永远成交;实盘的默认假设是"敌意"。本 skill 的工作就是把每一处"乐观假设"暴露出来,标注严重度,而不是替你优化收益。1112## 何时启用1314- 用户把量化策略 / 回测脚本 / 信号生成 / 因子计算 / 数据加载 / 参数寻优代码丢过来,问"能不能信""为什么实盘对不上回测"。15- 用户主动要求查未来函数、前视偏差、过拟合、数据泄漏、成交真实性。16- 在策略上实盘资金前,做一次工程尽职调查(due diligence)。1718**不启用**:让你预测收益、荐股、给买卖点、调参求更高夏普、修实盘下单/风控逻辑、运维数据源。这些不在本 skill 职责内。1920## 审查协议 (怎么扫一份策略仓库)2122铁律:**所有结论必须基于真实代码,定位到 `文件:行`,不臆测、不脑补**。找不到证据就标"未发现/需人工确认",绝不假设"应该没问题"或"应该有问题"。2324更高一层的铁律:**命中可疑模式只是"线索"不是"判决"。** 任何 Grep 命中后必须读上下文,区分真坑与合法用法,否则报告会被假阳性淹没、失去可信度。下文每条都标注了"合法例外",审查时务必核对。25261. **定位关键代码区**。用 Glob/Grep 找出四类入口:27 - 数据加载与清洗(`read_csv`/`download`/`fillna`/`resample`/`merge`/复权)28 - 信号/特征生成(`shift`/`rolling`/`rank`/指标计算/标准化)29 - 执行/撮合/记账(`buy`/`sell`/`order`/`fill`/`commission`/`slippage`/止盈止损)30 - 训练/寻优/评估(`train_test_split`/`GridSearch`/`TimeSeriesSplit`/`argmax`/`sharpe`/年化/复利)312. **逐类过四大陷阱清单**(见下)。优先用 Grep 命中可疑模式(负数 shift、`bfill`、`center=True`、`commission=0`、`StandardScaler().fit_transform` 在 split 前等),命中后**读上下文确认**,区分真坑与合法用法(例:`shift(-1)` 构造 label 合法,进特征矩阵就是致命未来函数;`bfill` 静态元数据合法,bfill 行情列致命)。323. **按严重度汇总**。同一类问题合并,致命/高危优先。334. **输出体检报告**(见固定模板)。完整陷阱清单见 `references/backtest-pitfalls.md`,本文只列每类最致命的几条。3435## 四大类陷阱(核心)3637每条格式:`陷阱名` — 怎么发现(含合法例外) — 修复方向。完整 50+ 条检测点与正则/AST 提示见 `references/backtest-pitfalls.md`。3839### ① 未来函数 / 数据泄漏(look-ahead / leakage)——最致命一类4041- **负数 shift 引入未来值** — 搜 `\.shift\(\s*-`、`diff(-`、`pct_change(-`;进特征/信号即致命。**合法例外**:仅在显式构造 `label/target/fwd_ret/y` 时合法 — 修:进 signal/feature 的列一律 `shift(>=1)` 滞后,未来收益严格隔离在 y 列且绝不进 X。42- **同 bar 信号又同 bar 成交** — 信号用 `close[t]`,成交价也取 `close[t]`(同索引);`backtesting.py` 默认当根 close 成交、`set_coc(True)` 重点查 — 修:`close[t]` 算信号、`open[t+1]` 成交,信号统一 `.shift(1)` 再喂引擎。43- **bar 时间戳语义 / resample 闭区间前视** — bar 的 timestamp 标注的是**开始**还是**收盘**?若标注开始却用了整根 bar 的 OHLC/volume 当作"该时刻可用",就是隐性前视;`resample(...).last()/agg()` 把 bar 结束才知道的聚合值挂到 bar 开始;跨市场/跨时区合并(UTC 与本地时区错位)导致信号比成交早一根。**合法例外**:bar 标注收盘时间、且下游统一在 `t` 收盘后才用 `t` 的值,合法 — 修:统一 bar 时间戳语义(推荐标注收盘),resample 结果整体 `.shift(1)` 或显式注明"收盘后可用",跨市场对齐到同一时区并核对夏令时。44- **预处理在全样本上 fit** — `StandardScaler/MinMaxScaler/PCA/QuantileTransformer.fit_transform` 出现在 `train_test_split` 之前,或 fit 对象不是 `X_train`;手写 `(df-df.mean())/df.std()` 整列同理 — 修:scaler 只在训练集 fit,放进 Pipeline + `TimeSeriesSplit` 每折独立 fit;在线场景用 `rolling/expanding` point-in-time 统计。45- **后向填充 / 时间插值(仅时变行情列致命)** — `bfill`/`backfill` 把未来值灌进当前;`interpolate(` 对序列**中间**缺失用前后点 → 引入未来值(须显式 `limit_direction='forward'` 才安全);停牌标的最常中招。**合法例外**:对**静态/历史不变的元数据**(行业分类、合约乘数 multiplier、tick size、上市前常量属性)做 bfill 无害——必须读上下文确认该列是"时变行情"还是"静态属性",只对前者判致命 — 修:时变行情列只允许 `ffill`,插值 `limit_direction='forward'`,缺失统计量取自训练集。46- **rolling `center=True` / 全列聚合当特征** — `center=True` 用到未来 w/2 根;整列 `.mean()/.std()/.quantile()/.rank()` 直接进信号 — 修:时序窗口 `center=False` 用过去,全历史统计用 `expanding()`,横截面 rank 按 date `groupby` 不跨时间。47- **target leakage** — 特征里混进 t+1 后才知道的列,或与 label 强相关派生列。可对每个特征与 y 算 corr 作**线索**(参考线 `abs>0.99` 重点查,非判决——强自相关特征如价格本身、慢动量、隔夜跳空与未来收益天然中高相关却合法;高相关≠leakage) — 修:命中高相关后回到"该列在 t 时点真能算出吗"做 point-in-time 可得性确认;Target encoding 用 out-of-fold,提交前跑"特征-标签相关性体检 + 可得性核对"。48- **时序用 shuffle 切分 / CV 泄漏** — `train_test_split` 缺 `shuffle=False`,`KFold/GridSearchCV` 用在时序而非 `TimeSeriesSplit` — 修:时序 `shuffle=False`,CV 用 `TimeSeriesSplit`/Purged K-Fold(带 embargo)。49- **repaint 指标** — `zigzag`/`fractal`/`pivot_high`/`find_peaks`(对整段序列找极值=未来函数)/自实现 `supertrend`,事后回溯重画 — 修:换因果版本(只用历史确认),对每个指标做"截断到 t 重算,t 处值变了即 repaint"稳定性测试。50- **point-in-time 缺失** — 用"当前"成分股回测历史、财报按报告期而非公告日对齐、后复权价隐含未来调整因子 — 修:成分股按历史生效区间、财报按公告日加发布滞后,价格用前复权或每时点已知调整因子;无 PIT 数据须显式标注结果不可信。5152### ② 过拟合 / 数据窥探 / 幸存者偏差(overfitting / data snooping)5354- **In-sample 寻优无样本外** — 参数寻优用的数据段与最终报告指标同段;无 `train/test`、无 walk-forward — 修:按时间切,headline 指标只来自从未参与调参的数据;用 walk-forward / anchored 滚动重优。55- **网格搜索只报最优参(cherry-pick max)** — `argmax`/`idxmax`/`study.best_params` 紧跟一次大网格扫描后直接当"策略";无 deflated/PBO/Bonferroni 惩罚 — 修:算 Deflated Sharpe(计入试验次数 N、收益偏度峰度)或 Benjamini-Hochberg/Bonferroni,优先选参数**高原**而非尖峰,报 OOS 分布而非单个赢家。56- **同数据反复试错(p-hacking)** — 大量注释掉的备选规则、奇异 magic number(`rsi<27.3`、`vol>0.0184`)、上百次追指标的提交 — 修:CSCV 估 PBO,预注册假设与参数范围,锁一个只碰一次的 lockbox 期。57- **幸存者偏差** — 用"当前"指数成分/写死 ticker 列表回测历史,delisted/破产/退市的输家被悄悄剔除(LUNA/FTT 直接消失) — 修:用 point-in-time universe 含退市收益,按 rebalance 日重建成分,crypto 含已下架币并记最后成交价。58- **look-ahead universe / 选股偏差** — 用全样本统计选标的("全期市值 top50""有完整历史的币");`df.dropna()` 跨标的丢未上市/已退市名 — 修:universe 按时点用 trailing 数据选,绝不要求标的有超过当前 bar 的数据。59- **自由参数过多(curve fitting)** — 同时优化 >5-7 个参数、大量 magic number、free param 与成交笔数比失衡 — 修:从经济先验固定参数、跨资产共享,要求每参数 >>10 笔交易,丢掉在 walk-forward 中变号的旋钮。60- **样本量不足** — 短期/少交易(<30-50 笔 round trip)的夏普标准误巨大;报夏普无 t-stat/置信区间 — 修:报夏普 t≈Sharpe×√years,要求 |t|>2(多重检验后更高),block-bootstrap 出夏普分布。61- **无多重检验校正(factor zoo)** — 测了一堆因子/策略按裸 `pvalue<0.05` 留显著的,无 `fdr_bh`/`deflated_sharpe` — 修:Benjamini-Hochberg FDR 或 Deflated Sharpe haircut,提高显著阈值(新因子 t>3),披露筛了几个候选。62- **忽略市场 regime** — 只在单一行情(2020-21 crypto 牛、低波股市)验证;指标只报整段混合样本 — 修:按子区间/regime(牛熊、高低波)拆分表现,至少覆盖一个完整周期+一段压力期。6364### ③ 成交真实性 / 成本执行(execution realism)6566- **零手续费 / 不建模成本** — 找不到 `commission`/`fees`/`set_commission` 或值为 0;换手越高误差越离谱 — 修:按交易所真实分层费率建模(Binance 现货 taker 0.1%、合约 taker 0.04%),做成本翻倍敏感性。67- **无滑点 / 理论价精确成交** — 无 `slippage`/`set_slippage`,成交价直接取 `close`/`open` 无点差;净值曲线异常平滑 — 修:至少建模半个~一个 spread + 与下单量挂钩冲击成本(平方根法则 `σ·(Q/ADV)^0.5`)。68- **用 bar 内极值(high/low)当成交价 / 乐观触达顺序** — 止盈用 `high`、止损用 `low`;同 bar 同时触发时,**先核实该引擎的同 bar 撮合顺序假设**——若引擎默认盈利方向先成交即为坑,若引擎本已止损优先则合法 — 修:确认乐观后,按最坏假设(同 bar 双触止损优先)重估;细化到 tick 还原 bar 内路径。69- **假设无限流动性** — 下单量 > 盘口/ADV 仍全额单价成交,无 `volume_limit`/participation 约束 — 修:加参与率上限(单 bar ≤ 该 bar volume 的 1%-10%),超量拆单,画资金规模-夏普衰减曲线定容量。70- **零延迟 + 限价单触及即成(无排队)** — 信号与成交同时刻、限价"price 触及即 100% 成交";高频/做市却无 `latency`/`queue` — 修:信号到成交加延迟(高频按真实 RTT),被动限价引入排队/成交概率模型,要求价格穿过而非仅触及。71- **能在涨跌停/熔断/无量时成交** — 无 `limit_up`/`limit_down`/`halt`/`volume==0` 过滤,在一字板(`open=high=low`)上成交 — 修:封板不成交、停牌(volume=0)不成交,极端行情回撤才真实。72- **忽略保证金与强平/爆仓** — 用杠杆/合约却无 `liquidation`/`maintenance_margin`,权益可深度为负仍持仓回本 — 修:每 bar mark-to-market,权益 < 维持保证金触发强平清零;报告必须统计"爆仓次数",一次清零即判失败。73- **忽略做空成本 / 资金费率 / 复权** — 空头无 `borrow_fee`、永续无 `funding_rate`、跨除权日用未复权价或前复权(未来信息) — 修:空头计提借券费、永续每结算点按 `名义×funding×方向` 入账、用后复权或分红现金入账口径。74- **现金/保证金无约束** — 现金为 0 甚至负仍买入=隐性无限杠杆;无 `buying_power` 校验 — 修:下单前校验购买力。**注意区分场景**:单边/无对冲策略可硬约束总仓位 ≤ 权益;**多空对冲/市场中性**策略 gross 敞口本就 >1(net≈0)属正常,不可用 gross ≤ 权益误杀——应约束 net 敞口与维持保证金,融资买入计利息。7576### ④ 收益统计 / 口径错误(metric integrity)——容易被忽视的"低级致命"7778- **年化因子(annualization factor)用错** — 夏普/年化收益用了与数据频率不匹配的 √N:股票日频应 √252、crypto 日频常用 √365、小时频该用 √(年化小时数);搜 `* np.sqrt(252)`/`* 252`/`annualize` 核对频率 — 修:annualization factor 必须匹配真实 bar 频率与该资产的交易日历,crypto 7×24 不用 252。79- **算术累加代替复利 / 收益序列拼接错误** — 用 `returns.sum()`/`cumsum` 当总收益而非 `(1+r).prod()-1`;跨再平衡频率或跨标的直接拼接收益序列 — 修:复利用 `(1+r).cumprod()`,统一收益频率口径,杠杆下注意几何收益拖累(volatility drag)。80- **夏普分母 / 无风险利率口径** — 夏普分子未减无风险利率、或分母用了含偏度的总波动而非超额收益波动;高偏度策略(卖期权式)夏普虚高 — 修:分子用超额收益,披露无风险利率口径,对高偏度策略并报 Sortino/最大回撤/偏度峰度,夏普单指标不可作唯一结论。81- **权益曲线口径混用** — 已实现盈亏(realized)与浮动盈亏(mark-to-market)混报,或回撤按已实现算而忽略浮亏 — 修:权益曲线统一 mark-to-market,最大回撤含浮动亏损。8283## 体检报告输出格式8485固定模板,直接产出(可截图、可复制)。`严重度` 用 emoji + 中文档级,定位精确到 `文件:行`。8687```88══════════════════════════════════════════89 回测体检报告 · backtest-guard90══════════════════════════════════════════91受检对象: <仓库/文件名> 扫描文件: N 个 代码行: ~X9293总评: 🔴 致命 a 项 · 🟠 高危 b 项 · 🟡 中 c 项 · 🔵 低 d 项94判语: 「<这是一个策略,还是一次对历史的过拟合?>」95 —— 一句不留情面但克制的整体结论9697────────────── 问题清单(按严重度) ──────────────9899[🔴 致命] 负数 shift 引入未来值100 位置: src/features.py:84101 问题: df['mom'] = df['close'].shift(-5) 将 t+5 收盘移到 t,102 该列进入了 X 特征矩阵(features.py:131)。103 修复: 进特征的列改 shift(>=1);未来收益只留 y/label,绝不进 X。104105[🔴 致命] 全样本 fit 预处理(数据泄漏)106 位置: src/train.py:42107 问题: StandardScaler().fit_transform(X) 在 train_test_split(:57) 之前,108 测试期 mean/std 泄漏进训练。109 修复: scaler 放进 Pipeline,TimeSeriesSplit 每折独立 fit。110111[🟠 高危] 零手续费 + 零滑点112 位置: backtest/engine.py:––(全文件未发现 commission/slippage)113 问题: 成交按 close 原值记账,日内策略年换手 ~900 次,成本被完全忽略。114 修复: 按 taker 费率建模 + 半个 spread 滑点,做成本翻倍敏感性。115116[🟠 高危] 年化因子用错(crypto 套用 252)117 位置: metrics.py:31118 问题: sharpe = mean/std * np.sqrt(252),但数据为 BTC 1h bar(7×24),119 年化被低估、跨频比较失真。120 修复: 按真实频率年化(crypto 用 √年化小时数),并复核复利口径。121122[🟡 中] 单一 regime 验证123 位置: backtest/run.py:20 (回测区间 2020-10 ~ 2021-04)124 问题: 仅覆盖一段 crypto 单边牛市,无熊市/震荡压力测试。125 修复: 至少覆盖一个完整周期 + 一段 2022 压力期,按 regime 拆分表现。126127────────────── 偏差影响(方向判断,非收益预测) ──────────────128说明:以下仅为"剔除偏差后表现往哪边走"的方向性判断,不是收益预测、129不承诺任何数字。存在 ≥1 项致命未来函数/泄漏时,当前 headline 夏普130不可作为样本外预期。修复"负数 shift + 全样本 fit + 零成本 + 年化口径"131后,样本外表现通常会**显著低于**当前回测——具体幅度需重跑确认。132133建议优先级: 先堵致命未来函数 → 再修口径/补成本滑点 → 最后做134walk-forward 重验。在致命项清零前,不建议投入实盘资金。135══════════════════════════════════════════136```137138报告原则:每条必带 `位置`;找不到证据的项标"未发现/需人工确认",不硬凑;命中可疑模式但读上下文后判为合法用法的,不写进清单(或写入"已排除"附注);判语克制、不羞辱、不夸张;"偏差影响"只给方向(变低/不可信),**绝不承诺具体收益数字**。139140## 严重度定义141142| 严重度 | 含义 | 处置 |143|--------|------|------|144| 🔴 致命 | 直接让回测看到未来或系统性虚高,实盘必然复现不了(未来函数、时间戳语义前视、全样本 fit、幸存者偏差、忽略爆仓、零滑点理论价成交) | **阻断**——清零前回测结果不可信,禁止上实盘资金 |145| 🟠 高危 | 显著高估收益或低估风险,方向性失真(零手续费、无限流动性、单报最优参、样本量不足、年化/复利口径错误) | **强烈建议修**——修复后预期大幅缩水 |146| 🟡 中 | 口径不严谨、特定场景失真(收益统计混用、单一 regime、tick/lot 未取整、夏普分母口径) | **建议修**——影响精度与稳健性 |147| 🔵 低 | 风格/边界/工程整洁问题(warm-up 未截断、对齐告警、缺断言) | **可选**——提升可信度与可维护性 |148149判定基准:**"这个值在该时点真能算出来吗?这笔成交实盘真能成吗?这个指标口径对吗?"** 三问任一为否,按影响幅度归入致命/高危。150151## 重要免责152153本 skill 是**工程审查工具**,只对回测/策略代码的**可信度与工程缺陷**做尽职调查,目的是帮你"防自欺、做 due diligence",识别会让回测虚高、实盘亏钱的技术陷阱。154155- **不构成投资建议**,不预测收益、不评估策略盈利能力、不荐股、不给买卖点或仓位建议。156- 报告中"偏差影响"等表述仅指方向性影响(通常使样本外表现低于回测),**不承诺、不量化任何具体收益或亏损数字**。157- 通过审查 ≠ 策略能赚钱;只代表未发现本清单覆盖的工程陷阱,可能仍存在未覆盖的风险、市场结构变化或模型失效。158- 审查依赖所提供代码与可见上下文;命中可疑模式经人工二次确认为合法用法的不应判坑,反之未提供的数据/外部依赖中的陷阱本工具无法发现。159- 一切实盘交易决策与资金风险由使用者自行承担。高危操作(投入实盘资金、加杠杆)前,务必小资金隔离验证。