回测处理经验

一、回测结果不及预期

场景一:没有产出交易

先别急着”调到有交易”,而是先判断为什么没有交易。常见 5 类原因:

  1. alpha 本身太稀疏 — ResidualZ 在回测窗里根本没到开仓阈值
  2. 信号到交易的映射太严 — 比如 cross_only + 高 entry_z,理论有偏离但不触发下单
  3. 风控/质量门禁全挡掉 — 比如 pair_missinglow_samplemissing_rate_window、高波动过滤
  4. 白名单和回测窗错配04_test_evidence 里通过的币,在 05_backtest 那段时间刚好没有足够触发
  5. 实现或时间语义有问题 — 最危险,比如阈值方向写反、open_time/close_time 错位、cross 事件判定有 bug

解决思路:按层推进,不要一口气乱改

Step 1:先做”触发漏斗”诊断

从上到下数一遍,定位死在哪一层:

  • 有多少 bar ResidualZ 是有效值
  • 有多少 bar 满足 abs(ResidualZ) >= entry_z
  • 有多少是 cross_only 真正触发点
  • 有多少通过 low_sample / pair_missing
  • 有多少通过 regime gate
  • 最后剩多少真实 entry

Step 2:先放松执行层,不先改 alpha

机构一般先改最外层映射,而不是先推翻信号:

  • 降低 entry_z
  • cross_only 暂时改成 level 做诊断
  • 放宽 max_hold_bars
  • 暂时去掉可选的 risk overlay

如果这样就有交易,说明问题在执行映射,不在 alpha 本身。

Step 3:把”最小 baseline”和”附加风控”分开

先跑一版最小可执行策略,再逐个叠加:

  • entry / exit
  • sizing
  • portfolio cap
  • regime gate
  • invalid_sample 处理

这样才知道是哪一层把交易打没了。

Step 4:如果最小版本也没交易,再回头看信号设计

这时才考虑:

  • 这组 param_id 是否过于保守
  • 这个 bar 频率是否不适合
  • 这组白名单是不是太窄
  • best_h 和执行窗口是否不匹配

Step 5:始终一次只改一层

不要同时改 entry_zentry_mode、风控门禁、白名单。


场景二:回测结果为负

核心问题:到底是哪个层面把结果打成负的。最有用的拆法是以下 6 个角度。

1. 正确性层

先排除假亏损:

  • 时间语义有没有错位
  • 信号方向有没有写反
  • 开平仓规则有没有实现偏差
  • 双引擎是否一致
  • 费用、资金费率、持仓记账是否重复或漏记

如果这层没过,后面所有优化都不可信。

2. Alpha 层

先看扣费前有没有边。最关键的分叉:

情况判断
gross < 0 且 net < 0更像 alpha 本身没用,或信号映射方向错了
gross > 0 但 net < 0alpha 有一点边但太薄,主要死在成本和换手

这一步看:Q1/Q5、gamma、reversion_score、OOS 是否仍成立。

3. 执行映射层

很多负收益其实不是 alpha 错,而是交易规则把信号用坏了。重点看:

  • entry_z 是否合理
  • entry_mode 是否过于频繁或过于稀疏
  • exit_zmax_hold_barscooldown
  • flip 是否过多

同一个信号,交易语义不对,也会把正边打成负边。

4. 成本与微观结构层

实盘最常见的死因。重点看:

  • trade_countfee_paid_sum
  • 单笔平均毛利 vs 单笔平均成本
  • 持仓时长、换手率
  • 滑点、资金费率、冲击成本

如果发现单笔毛利很小、交易特别多、一加真实费用就翻负 — 那就不是先”调更多参数”,而是先想办法降换手、拉大单笔边际

5. 组合层

单币能赚,组合也可能亏:

  • 是否少数币在拖累
  • 并发过高导致资金分散
  • 仓位分配是否错误
  • 是否把低质量、低边际币也一起放进来了
  • 组合是否过度集中在某一类市场状态

常见动作:砍掉最差币种、改 portfolio cap、调整 sizing、把 alpha 和 risk overlay 分开记账。

6. 稳健性层

哪怕当前回测为正也可能不可用;为负也要看负得是否稳定:

  • 不同时间段
  • 不同波动 regime
  • 上涨/下跌切片
  • 不同 symbol 子集
  • 不同成本假设

如果只是某一小段负,可能是 regime 问题。如果各切片都负,通常就该考虑停线。


快速判断框架

现象优先怀疑
gross 和 net 都负alpha、方向、执行映射
gross 正、net 负换手和成本
单币多半正、组合负仓位和组合分配
Train/Test 好,Backtest 负执行层和成本层
只有极少数参数/时间段为正过拟合

稳健性诊断:不是第一优先,但不可跳过

稳健性诊断和前面几层的关系:

层级回答的问题
正确性 / alpha / 执行 / 成本”为什么现在亏” — 定位主因
时间段 / symbol 子集 / 成本假设 / 波动状态”这个亏法是稳定一致的,还是只发生在某些局部条件下” — 稳健性切片

它的作用不是先拿来救策略,而是帮你判断:是全局都不行,还是只有某些 regime 不行;是少数币拖累,还是大部分币都边际太薄;是只要手续费一真实化就不行,还是在某些成本水平下还能活。

四类稳健性切片

1)不同时间段切片

  • 前半段亏后半段赚?还是只有某个阶段特别差?
  • 如果只是某一小段拖累 → regime 问题;各时间段都差不多负 → 不是偶然

2)不同 symbol 子集

  • 前 5 个最强币、去掉最差 3 个币、大币 vs 小币
  • 判断是不是少数币拖累组合,是否需要收缩白名单

3)不同成本假设

  • 0 bps → 1 bps/side → 2 bps/side → 3 bps/side
  • 判断策略到底是”无边”还是”有边但太薄”
  • 这是最关键的实盘敏感性分析之一

4)不同波动状态分层

  • 高波 BTC / 低波 BTC / 下跌环境 / 非下跌环境
  • 判断策略是不是只在某种市场环境下有效,regime gate 是否值得保留

处理优先级

  1. 先确认主因 — 成本层是主因?执行层是次因?
  2. 再做稳健性切片 — 是不是所有子样本都一样差?有没有值得保留的局部结构?
  3. 最后再决定怎么处理 — 停线 / 收缩白名单 / 降换手 / 回到更慢频率 / 重开新信号族

这些切片不是”先怎么调”,而是”在决定调不调、往哪调之前,先把亏损结构拆清楚”。


实战案例:时间段切片诊断

为什么先做这个:

  • 它最先回答”这次亏损是不是全回测窗都在发生”
  • 如果只是某一小段特别差 → 优先查局部 regime、局部事件、局部数据问题
  • 如果每一段都差 → 问题更像 baseline 自身的边际和成本结构,不是某个偶发坏月份

切片结论:

  • 日历切片 3 段全部为负
  • 半窗切片 2 段也全部为负
  • 2025-12 负,2026-01 更负,2026-02 也仍然为负
  • 前两段虽然 gross_pnl_sum 为正,但都远小于 fee_paid_sum
  • 最后一小段连 gross_pnl_sum 都转负了

当前亏损不是某一个坏时间段偶然拖累,更像整个回测窗里都存在”边际太薄、费用太重”的一致性问题。

实战案例:Symbol 子集切片诊断

为什么做这个:

  • 时间段切片已经证明,不是某一个坏月份拖累
  • 下一步最该判断:是不是少数坏币拖垮了组合
  • 如果删掉最差几个币就能明显改善 → 优先考虑收缩白名单
  • 如果子集仍然都很差 → 问题更偏整体边际薄,不是坏币太多

切片结论:

  • 删掉最差 1/3/5 个币,亏损会缩小,但仍明显为负
  • 只保留 top_5、top_10 reversion_score 的高分币,费用后还是负
  • best_h=10best_h=30 分组,也都还是负

当前 baseline 的负收益不是主要由少数坏币拖累,也不是”只要留下测试层最强币组就能救回来”。

实战案例:成本假设切片诊断

为什么做这个:

前两步已经基本排除了”某一段时间”或”少数坏币”是主因,接下来最该量化的是:这条线到底是”无成本时有边、真实成本后被吃掉”,还是”连低成本假设也站不住”。


实战案例:高波动状态分层诊断

为什么做这个:

  • 前三步已经说明,不是某一小段时间、不是少数坏币、也不是”只差一点成本”
  • 最后要判断:是不是主要高波环境把策略打坏了
  • 如果亏损主要集中在高波段 → exclude_high_vol 这类 regime gate 可能还是主线
  • 如果不是 → 高波过滤就不是核心解法

切片结论:

  • 高波 bar 只占约 20.13%
  • bar 级净值增量里,高波段不是主要亏损来源,主要亏损反而发生在 low_vol bar
  • 在高波 regime 里结束的交易单笔更差,但只有 81 笔
  • 穿过高波环境的交易也更差,但只有 95 笔

当前总体亏损并不是由少量高波穿透交易主导。

二、针对 Alpha 利润太薄的解决方案

概述

  1. 先判断 gross/trade 能不能随着更严格入场明显抬升;如果不能,说明 alpha 太弱,不是执行层能救。
  2. 再看持仓时长和出场语义,确认是不是太早进、太早出,把本来能走开的 spread 提前切掉了。
  3. 最后看 symbol 和 regime 切片,确认是不是低质量币和高波阶段在稀释整体边际。

实战案例:单笔毛利太薄的处理思路

这次围绕”单笔毛利太薄”的处理思路,可以概括成 5 步:

Step 1:先把问题定义清楚

不是先看组合为什么亏,而是先问:gross_pnl_per_trade 是否太低,导致手续费把边际吃掉。也就是先定位成”单笔交易经济性”问题,而不是先怀疑数据或信号方向。

Step 2:先做结构拆解,不把”毛利薄”当成全局结论

加了单笔诊断后,发现不是所有交易都薄,而是结构性变薄。最典型的是 6_to_15 bars 这档交易,毛利明显偏薄、费毛比偏高,所以拖累主要集中在某些持仓时长和某些 symbol。

Step 3:先试执行层收紧,验证是不是”交易太松”

执行层优先试了两类动作:

  • 提高 entry_z
  • 缩短 max_hold_bars

结果很明确:

  • entry_z 往上抬到 2.85/3.0,交易基本直接消失,这条路太激进
  • max_hold_bars 从 15 收到 10 有改善
  • 再压到 8/6 就开始过度收紧,反而伤到有效利润

所以执行层当前最优点是:

  • entry_z = 2.77
  • max_hold_bars = 10

Step 4:再试截面层排查,验证是不是”少数差币拖累”

在 hold10 这个当前最好执行版本上,做了一个诊断实验:剔除 TRXUSDT、BNBUSDT、HBARUSDT。结果显示改善很明显:

  • total_return: -0.00787 → -0.00533
  • gross_pnl_per_trade: 0.00257 → 0.00338
  • fee_to_gross_ratio: 0.155 → 0.118

这说明问题不只是”执行过松”,还包括”少数差币在稀释组合质量”。

Step 5:最后把研究结论和正式策略边界分开

这里要严格区分两件事:

  • 手工剔除差币:可以作为诊断实验
  • 正式策略规则:不能靠”回头看哪个币差就删哪个”

所以当前真正的研究结论是:

  • 单笔毛利薄不是全局性的,而是集中在部分持仓区间和部分 symbol
  • 执行层有效优化方向是”缩短持仓”,不是继续抬高 entry_z
  • 截面层确实存在差币拖累
  • 但最终不能停留在”手工删币”,而应该升级成事前可定义的 TopK / 打分筛选规则

总结

这次处理单笔毛利薄的思路是:

先用单笔诊断找出”薄”发生在哪,再先收执行、后查截面,最后把”人工剔除差币”的发现,上升为未来应该实现的、事前可执行的 TopK/score-based universe 规则。

如果holdout没过

一般不是“继续磨到过”,而是走标准的 FAIL -> rollback -> retry。

  - backtest 没过,先看是不是 Stage 5 问题。常见是计入手续费、滑点、冲击、资金费率后失效,或者容量根本不够。这种先回退执行层,修成交假设、仓位约束、换手和成本模型;如果还是不行,再回退 Stage 2 改因子和交易映射。
  - holdout 没过,通常不要直接在 holdout 上继续调参。这更像 Stage 2/3/4 的问题:假设不稳、数据有泄露、样本污染、只在某一段行情有效。标准做法是先冻结这次 holdout 结果,回退去修上游,然后换一块新的、没看过的 holdout
    再验。
  - 如果是“样本内很好,样本外明显塌掉”,大概率是过拟合或 regime 依赖;如果是“毛收益可以,净收益不行”,大概率是执行和成本问题;如果收益只靠少数极端日,大概率机制不稳,应该回退 Stage 1/2 重写假设。

  实务上我会这样做:

  1. 先把这次实验冻结,不再继续看着 holdout 调。
  2. 写失败归因:到底是数据、统计、执行、还是稳健性没过。
  3. 按归因回退:
      - 数据/时点问题:回退 Stage 3
      - 显著性消失:回退 Stage 2 或 Stage 3
      - 成本后失效/容量不足:回退 Stage 5,必要时再回退 Stage 2
      - 参数微调就翻符号:回退 Stage 1/2
  4. 重新只用训练集/验证集改,不碰旧 holdout。
  5. 最后再打一块新的 holdout。

  判断上也别太宽松:

  - holdout 明显失效,通常直接记 FAIL
  - 成本后失效,通常也是 FAIL
  - 只有轻微低于阈值、没触红线、补救路径又很清楚,才考虑 CONDITIONAL PASS

stage pnl 为正,账户为负

为什么 stage pnl
  unit 会正,而账户级 USDT 会负,到底
  是单位缩放问题、仓位映射问题,还是手续费口径没对齐
  
  
  
  
  1. 先判定这是不是“真冲突”,还是“不同口径被拿来硬比”。
     现在 outputs/
     topic_c_1m_20240309_20260308/05_backtest_tighten_try/
     topk_k8/engine_compare.csv 和 vectorbt/audit_report.json 是
     账户级、USDT/收益率 口径;summary.txt 和
     portfolio_summary.parquet 是 Rust stage aggregate,字段文档
     里写的是 stage pnl unit。第一件事就是确认这两套值理论上应不
     应该相等。
  2. 再追同一批交易在两套系统里的数据链路。
     我会从 src/bin/strategy_ob_short_only_confirm.rs 里
     summary.txt / portfolio_summary.parquet 的生成逻辑往前追,再
     去找 engine_compare.csv 的来源,看同样的 trade_count、同样的
     symbol、同样的开平仓,为什么一个是正,一个是负。
     如果交易集合一致但符号反了,重点就不是 alpha,而是单位、仓位
     映射、手续费、资金利用率。
  3. 重点查 3 类最可能的根因。
      - 单位问题:stage pnl unit 和 USDT 是否只是没换算。
      - 费用问题:stage 聚合里的 fee_paid_sum=0.0404/0.0496,账户
        级却是 302/297 USDT,这个差距太大,优先怀疑手续费口径和
        notional 映射。
      - 仓位问题:账户级用了 starting_balance_usdt、
        capital_utilization、并发仓位约束,stage 聚合可能没有完整
        映射。
  4. 最后才决定该怎么改流程。
     如果确认只是“展示层把不同口径并排了”,那该修的是 gate 文档和
     dashboard,不该继续拿 summary.txt 判断经济性。
     如果确认本来就应该一致,那就是 Stage 5 的实现问题,先修口
     径,再看 06_holdout_tighten_try,否则继续做 holdout 没意义。

回测的结果出现 利润微薄,成本吃了大部分

  1. 先冻结结果,不让问题漂 我会先把这次失败当成一个正式样本冻结下来,保留:
  • 参数
  • universe
  • 时间切分
  • 成本假设
  • 回测引擎结果
  • 账户级关键指标

目的只有一个:后面所有判断都基于同一个失败版本,不允许边看边改。

  1. 先排除“假亏损” 我先查正确性层,不先查策略:
  • 双引擎是否一致
  • 时间语义有没有错位
  • 信号方向有没有写反
  • 费用和仓位口径有没有混
  • stage pnl 和账户 USDT 有没有被拿来硬比

这一步没过,后面全都不可信。

  1. 先判断亏损属于哪一类 我通常只分 4 类:
  • ENG_FAIL:实现、口径、时间错
  • RESEARCH_FAIL:03/04 证据本来就不稳
  • EXEC_FAIL:信号有一点边,但被执行和成本吃掉
  • THESIS_FAIL:机制本身不成立

最关键的分叉就是:

  • gross < 0, net < 0:更像 alpha 不行或方向错
  • gross > 0, net < 0:更像成本和换手问题
  1. 如果是 EXEC_FAIL,我就只拆执行经济学 我不会马上回头改信号,而是看 4 件事:
  • 触发漏斗:到底是没信号,还是映射后交易太多
  • 单笔经济性:gross/trade 对 fee/trade
  • 退出语义:是不是几乎全靠 max_hold
  • 切片诊断:时间段、symbol、持仓桶、波动状态

也就是先回答:

  • 是不是全时间都亏
  • 是不是只有少数坏币拖累
  • 是不是某类持仓特别差
  • 是不是高波环境把它打坏
  1. 只在“失败所属层”改 这是我最看重的纪律。
  • 如果是执行失败,我只改 Stage 5
  • 不回头重估 best_h
  • 不偷换 whitelist
  • 不重切 train/test
  • 不在同一份 OOS 上反复挑参数

也就是说:只允许做有唯一假设的 controlled retry。

  1. 最后再决定继续还是停 我一般只接受 4 个结论:
  • RETRY
  • RESEARCH AGAIN
  • NO_GO
  • CHILD_LINEAGE

不是每条线都值得救。 如果切片后发现:

  • 所有时间段都负
  • 所有币种费用后都负
  • gross/trade 远低于 fee/trade
  • 退出又基本全靠硬持有期

那我就不会继续磨这条线了,直接记 NO_GO 或开新谱系。

套到你现在这类问题上 我当前的判断顺序就是:

  1. 先确认不是实现错
  2. 再确认是 gross > 0, net < 0 还是 gross < 0, net < 0
  3. 再用切片拆清楚到底是:
    • 时间问题
    • 坏币问题
    • 成本问题
    • 执行语义问题
  4. 如果确认主因是“交易太多、单笔太薄”,下一步只做更窄的执行诊断
  5. 如果切片后发现怎么切都救不回来,就停线

一句话总结:

我的核心思路不是“怎么把它调成赚钱”,而是“先搞清楚它为什么亏,再判断这条线值不值得继续投 入”。