Bigopen V9 · 美股期权流股票策略

我们怎么筛因子

一套已经成型的方法论:四阶段漏斗、9 关判据电池、10 级判据阶梯、9 维健康度、 对抗式证伪,以及上线之后的对账与体检。每一条判据背后都有一个被它毙掉或救下的真实因子。

生产基线 10f(实盘已去 nbp3d → 9f) 候选 11f +atr_low_pen_roll 主流程 /explore-factor 整理 2026-09-14

§00怎么用这份文档

这份手册是操作手册,回答"怎么做"。团队的 onboarding 报告 (onboarding_report_zlj/)是叙事报告,回答"为什么"。两份配套看,不重复。

我有一个因子想法

先读 §01 数据卫生 确认环境干净,然后从 §04 四阶段漏斗 开始,逐关对照 §05 判据电池 走。任一关不过即停。

我在复查别人的 PR

直接翻 §05b 判据阶梯§06 九维健康度§08 对抗式证伪。重点查阈值来源、残差网格同向率、增量三分账。

我想知道某个想法是不是试过了

§09 因子坟场。里面按"哪一关塌了"归类,并标明换基线之后 还值不值得重验——A 类死法不值得,B/C 类值得。

我做月度体检 / 怀疑某条腿失效

§10 因子角色三分法 决定该用什么证据读它,再跑 §11factor_health_dashboard.py

一个因子的完整生命周期

候选想法关 1 经济直觉 数据卫生§01 六条必检 信号层Phase 1–2 组合回测Phase 3 健康度 + 证伪Phase 4 · 9 维 PR → shadow分阶段 era 实盘 + 体检逐笔对账 因子坟场 · 按"哪一关塌了"归类留档 A 特征层无信号(不必重验) · B 共线残差层 · C 回测容量层(最该重验) · D 证据不足搁置 被否因子换基线后只重验 B / C 两类;月度体检发现漂移的现役腿,回流到证伪环节
阅读约定

[事实] = 有文件、回测产物或代码支撑,可以倒推验证。 [经验] = 团队判断,可以挑战。所有数字都标了 asof—— 缓存 vintage 会漂,不标 asof 的数字在这份文档里一律不写。

§01前置:不干净的数据不配谈因子

这一节不是背景知识,是开工前的六条必检。历史上最贵的几次翻车 都不是因子设计错了,是环境不干净——整轮 V8 探索(83 组矩阵)的"+pp"几乎全是数据 bug 造出来的人工产物。

1. 时间窗:永远显式传 start / end

引擎默认 --start-date 2023-03-10--end-date 2025-12-31, 两个默认值都会坑人。

参数引擎默认官方口径不改会怎样
--start-date2023-03-102023-04-20 0310→0420 属于 rolling 因子 warmup 窗(ret_5d 90d / nbp3d 90d)。同样的终值用更长窗算 CAGR, 凭空少 4–7pp,还多出 9 笔早期低质量交易。0420 也是 V9 实盘启动日。
--end-date2025-12-31当前 asof 2026 数据被整段丢弃。regime / 波动类因子如果不含 2026 会假通过

2. 环境变量:guard 会静默 fail-open

回测脚本不读 .env,必须自己导出:

set -a; source .env; set +a   # 导出 MASSIVE_API_KEY

build 日志里必须出现 MASSIVE_API_KEY present: yes。拿不到 key 时 build_spy_guard_map 返回空 map,整个 SPY MA20 risk-off 层静默消失, 日志只留一行 guard disabled: Massive API key not found

实测代价

同一张钉死的 60,091 行 9f signal_table,一次回测出 121% / MaxDD 12.03%, 几小时后同样的代码出 138% / MaxDD 19.64%。差异全部来自 guard 开没开。 在 worktree 里跑回测默认拿不到 key——这是最容易中招的场景。

3. daily_bars:目标状态是"一票一文件"

daily_bars/{ticker}_{start}_{end}.parquet 存,增量拉取会堆多版本 (某只票堆过 49 个,SPY 约 40 个)。_pick_best_daily_file 按"实际数据结尾最新"挑, 于是窄窗口、end=今天的文件会遮蔽宽文件。

  • 症状:当天跑不受害(end 打平、宽文件胜出),隔几天复现必爆。 2026-07-05 复现时 ret_5d 覆盖率从正常的 99.5%+ 掉到 90.3%,7,295 行被 warmup-drop,10f 少 256 笔。
  • 修法python3 claude_explore/daily_bars_prewarm_zlj.py --base <base.parquet> (预热 2022-12 至今的宽文件,让 picker 的 tiebreak 必胜,不删文件)。
  • 验证:build 日志 history_short<300Dropped ~1200。 修后 10f asof0623 对齐同事到 0.2%(1787 / 1790 笔,MaxDD 10.93 精确一致)。

4. massive_cache 的三类陷阱

核心事实

fails=0 / All tickers fetched cleanly 不代表 cache 完整。 根因一句话:历史月分片一旦落盘就再也不会被增量 fetch 碰到——所以坏分片永不自愈,旧 vintage 也永不更新。

陷阱判据修法
#1 截断分片
某次 fetch 中断只写进半个月
分片 max bar date 距月末 ≥5 天。判据必须包含该 ticker 的最后一个月—— 第一版扫描跳过最后一个月,只抓到 49 个中间月,漏掉的 6 个正是残余 gap(ABT / GO / SEPN)。 cache_repair_zlj.py scan-truncated;最后一个月截断直接 rm -rf <ticker>/
#2 _meta.json greedy fallback coverage()二选一不是并集explored_ranges_adj 非空就只信它, 为空才回落到"月文件日历月边界",并 greedy 认领整月——有 2026-05.parquet 就认领 05-01..05-31,哪怕 bar 只到 05-15。 删分片必须连带删 _meta.json
#3 vintage 漂移
(数字对不上的主因)
行数全对齐但收益差几 pp,且差异集中在最近一年。adjusted price 会随除权拆股回溯调整 (实测两天之隔 8 只票 1,491 个点位被改,AZN 2×、TZA 10×、FFAI 150×)。 cache_repair_zlj.py purge-year 2026 --apply(约 16 分钟 / 6,900 万 bar)

排查顺序:先比 build_base_xy_meta.jsonprice_status_breakdown 七个字段定位 → 行数不对用 #1 / #2 → 行数对但收益差用 purge-year → 都做完才考虑找同事要 tarball。

教训

不要因为"历史值被回溯改过"就断言不可复现。那 8 只票只贡献 1 笔非 2026 成交, 用它解释 2026 的 3pp MaxDD 是过度外推;真凶是 1,717 个 ticker 的 2026 分片整体是旧 vintage。 先把能重抓的都重抓干净,再谈不可复现。

5. delta build 的特征 NaN 陷阱

build_bigopen_v9_opportunities.py --start-date <窗口起点> 的增量模式下, stage1 按"该 ticker 在本次构建里的最早信号日"开始加载分钟线,不带 rolling 特征所需的 lookback

特征需要的前置delta 窗口内 NaN 率后果
dvol_10d_ratio30 session100%dvol 门型因子不罚分
realized_vol_3d4 session~50%rv3d 不加分

判据:merge 后查窗口行 dvol_10d_ratio 的 NaN 率,>10% 即中招 (本底 NaN 率 6.9% / 4.5% 是设计内的"首 30 session"情况)。 修法:跑 fix_base_features_zlj.py(stage1 同款公式从本地分钟缓存补算,已验证与 live 在线函数逐位一致), 或把 delta 起点提前 ≥40 个交易日再截取。8f 时代没有 dvol / rv3d,所以旧流程从没暴露过这个坑。

6. pandas 3 的 µs / ns 精度陷阱

pandas ≥3 默认 datetime64[us]Series.astype("int64") 返回微秒, 但 Timestamp.value 恒为纳秒,两者进 np.searchsorted 差 1000 倍。

位置症状状态
utils/bigopen_stage1_build.py所有信号被标 no_price_data, 最终 KeyError: ['entry_price', ...]主干已修 自带 _NS_SCALE
factors/vwap_dev_feature.py:97searchsorted 永远选当日最后一根 bar → vwap_dev 退化成"EOD close vs 全天 VWAP"(带前瞻),选出完全不同且更差的信号 (11f vwap15 CAGR 79.71 而非报告里的 125.81)曾漏修 dev_zlj 已补 .dt.as_unit("ns")

排查法:grep 所有作用在 datetime Series 上的 astype("int64")。 同事的 pandas 2.x 环境不触发,所以他们的报告数字没错,是复现环境炸

还有一条:只信同会话 delta

fast-path 对照法

比较因子时不要跨会话比绝对数字。做法:钉死同一张 scored table(例如 60,091 行的 9f 表), 只改新因子那一列分数,所有变体在同一次会话里回测。 这样 guard 状态对所有变体一致,变体之间的 delta 是干净的、对 guard 与 daily_bars 两个 bug 免疫。

想要可复现的绝对数字:先把 daily_bars 去重到每票一份 canonical 多年文件, 确保 MASSIVE_API_KEY 在 env,再跑。2026-07-14 复验结论: guard 在位 + prewarm 过的 base 上,同 base 完全可复现(MaxDD 7 档逐位一致、 2023–2025 逐年匹配到 0.02pp)。

§02基线、口径与生产数字

三阶段流水线

Stage 1-1  stage1_1_fetch_minute_bars.py   →  massive_cache/*.parquet   (唯一的网络 I/O)
Stage 1-2  build_bigopen_v9_opportunities  →  base_xy.parquet           (离线确定性, 含标签 y_2d)
Stage 2    build_factors.py                →  signal_table.parquet      (因子打分, 秒级)
Stage 3    run_backtest_v9.py              →  portfolio_report.json     (SPY MA20 guard)

标签 y_2dentry_price / exit_price / exit_reason 在 Stage 1-2 就定死, 因子永远不重算标签。score_raw 只在 Stage 2 产生。

事实

唯一的回测引擎是根目录 run_backtest_v9.py(docstring 注明"= 原 V9.1")。 run_backtest_v9_1.py / bigopen_v9_1.py 已于 2026-06-03 删除(commit 02f9920)。 旧文档和 skill 里的 run_backtest_v9_1_zlj.py wrapper 会 ModuleNotFoundError

探索基线:8 核

iv  stock_price  otm  asset_type  rule_hit  time_slot  nbp3d_low  ret_5d_roll_deep
基线(asof0427, 1x, $5M)CAGRMaxDDSharpeCalmarTrades终值
8 核105.35%13.65%6.047.721,432$43.9M

按年:2023 +25.16% / 2024 +104.29% / 2025 +197.40% / 2026(1–4 月)+15.51%。 窗口必须 --start-date 2023-04-20 --end-date 2026-04-27

base8 contamination

build 时加任何 rolling 因子,它的 warmup 期 NaN 会让整行被 drop,污染 signal_table: base8 行数从 1,432 缩到 1,238、CAGR 从 105.35 掉到 94.61。 测静态因子 → 一张 signal_table,base8 复现 105.35;含 rolling → 分两张 build,不混。 /explore-factor 的 Step 3.5 就是专门问这一句的;不回答默认按全静态走。

当前生产口径(asof0810, 全部同一底表 + 同一张 scored 表,可跨档直接比)

版本CAGRMaxDDCalmarSharpeTradesWin%PF终值
10f 1x 生产基线114.99%10.93%10.526.721,90055.5%1.57$62.86M
11f +atr 1x 候选119.17%10.17%11.726.861,87855.5%1.58$67.00M
10f 1.5x190.56%18.06%10.556.721,87955.5%1.51$170.22M
11f +atr 1.5x199.43%16.76%11.906.781,85255.6%1.52$188.03M
10f 2x280.79%24.24%11.586.661,86055.6%1.47$416.36M
11f +atr 2x292.97%23.00%12.746.691,83355.5%1.46$462.07M

score_raw 实测区间:10f [−9, +11] / 11f [−10, +11](68,923 行)。准入门槛 min_score = 6。

数字为什么会漂:一定要标 asof

asof10f CAGRMaxDD状态与原因
0623 / r0714122.15%10.93%prewarm 后的对齐态
0722105.31%11.46%非对齐 daily_bars 混合 vintage:V10 backfill + prewarm 改了 277 只票的 ret_5d 历史值 → ret_5d_score_low 在 495 个信号(0.79%)上翻转 → min-score=6 边界准入变化 → 组合路径混沌发散
0810114.93%10.93%已对齐 purge-year 2026 + daily_bars 整删重拉 = 单一 fresh vintage
0824114.47%10.93%保持对齐 与 0810 MaxDD 逐位相同、CAGR 差 <0.8pp、笔数 +37(正好两周量)
硬规则

V9 的 headline CAGR 对 daily_bars 缓存快照不平稳(±15pp 级)。 跨期对比只能固定缓存快照;新月份的增量才是真实 delta。任何"因子 A 比因子 B 好 X pp"的结论, 只有同会话、同 base、同 scored 表才算数

§03五层抽象与"一次只改一层"

动手之前先把策略拆成五层,想清楚这次改的是哪一层。 这是通用研究工作流(skills/youthquant-strategy-research/SKILL.md)的开篇, 也是最常被跳过、代价最大的一步。

定义V9 的取值
signal_universe哪些事件算候选信号UW 期权异常流中的 call + ASK 大单(premium ≥ $100K 档)
execution_object真正交易的是什么正股(不是期权)
single_trade_rule单笔怎么进、怎么出、成本怎么算信号后收盘价入场;止损 −15%;max hold 2 天(周四五顺延到周一开盘);滑点 5bp + 佣金 $0.005/股
portfolio_rule仓位分配、限仓、风控score ≥ 6 准入;动态 base 仓位 ×score 权重;单笔 15% / 单票 20% / 行业 50% cap;SPY MA20 guard
promotion_rule什么条件下从研究升到 paper / livePhase 4 九维 ≥7 绿 + 关键项全绿 → PR → shadow → 分阶段 era 上线
三条硬规则
  • 先固定前三层,再评估第四层。组合收益在前三层没定死之前没有意义。
  • signal_universesingle_trade_rule 大改 = 新的策略家族,不是"因子改进"。
  • 不要把"新的 y"和"新的因子"混在一起归因。同时改两层却只汇报"因子有效",默认视为不合格。

版本四分类

每轮实验结束后,把结果明确归到一档,不要含糊:

档位含义
baseline当前正式口径(现在是 10f)
candidate回测有效、值得继续验证(现在是 11f 的 atr_low_pen_roll
research-only有趣但暂不并入主链路
live-ready已满足上线前全部检查

只在样本内提升收益、却带来大量实现复杂度或一致性风险的实验,默认停在 candidate 或 research-only

§04四阶段漏斗

固化在 /explore-factor 里。判官是 8 核生产基线。 任一关不过即停——不要"再调调看"。

Phase 1 — 单因子分层检验

用 base_xy + builder 输出的 feature cache,做四件事:

  1. 过滤:可交易资产 + OTM 5%–50%
  2. 分桶(quintile):每桶 mean(y_2d)、N、t-stat
  3. ANOVA F 检验
  4. Spearman 相关性矩阵:对 score_rawnbp3d_score_lowret_5d_score_lowstock_price_score
指标过关线为什么是这条线
ANOVA p< 0.05特征至少要能分层
最优桶 mean(y_2d) 超额> +0.2%相对全体均值。达不到就是噪声级
与已有因子相关性|r| < 0.4要的是新维度,不是已有因子的代理
候选打分后 +1 占比8% – 18%太稠密没区分度(count_low 打了 49.8% 的信号 +1),太稀疏没样本

Phase 2 — 信号层对比

baseline = 8 核 score_raw。新因子加进去之后,看它能不能把 score ≥ 6 / 7 的 边缘信号"提拔"成真正值得交易的票。这一步是为 Phase 4 的"真 promote ratio"做铺垫。

Phase 3 — 8 核组合回测(关键判官)

# 1. build 含新因子的 signal_table
python3 claude_explore/build_factors_zlj.py \
  --base exp_result/bigopen_v9_base_asof0427/bigopen_v9_stage1_base_xy.parquet \
  --factors iv stock_price otm asset_type rule_hit time_slot nbp3d_low ret_5d_roll_deep <new_factor> \
  --out-dir claude_explore/scored_v9_8core_with_<new>_zlj

# 2. 回测 + baseline 对照(注意时间窗)
python3 run_backtest_v9.py \
  --signals claude_explore/scored_v9_8core_with_<new>_zlj/signal_table.parquet \
  --factors iv stock_price otm asset_type rule_hit time_slot nbp3d_low ret_5d_roll_deep <new_factor> \
  --min-score 6 --capital 5000000 --leverage-ratio 1.0 \
  --start-date 2023-04-20 --end-date 2026-04-27 \
  --out-dir results/bt_v9_8core_<new>_zlj
指标8 核基线新因子达标线
CAGR105.35%≥ +3pp(≥ 108.35%)
MaxDD13.65%≤ 14.65%(+1pp 上限)
Sharpe6.04≥ 5.5
Calmar7.72≥ 6.5
2026(1–4 月)+15.51%不退化(≥ 12%)
Phase 3 通过 ≠ 因子健康

回测赢了照样可能被毙。rvol_tod gate 把 Calmar 从 11.10 抬到 12.72、 终值 $66.7M,仍然被否——因为无 plateau、残差网格 59%、2023 全面反向、96% 的改善集中在 2026 的 34 笔。 这是全语料里最干净的"赢了回测但不合格"案例。

Phase 3.5 — build 前必答的一句

开跑前自问

这次要测的因子是静态FactorBase,无 warmup)还是 rolling(会触发 warmup drop)?

  • 全静态 → 一张 signal_table 搞定,base8 复现 105.35
  • 含 rolling → 分 2 张 signal_table(Build A 干净基线 + Build B 含 rolling),不混

Phase 4 — 九维严格审视

§06。工具 claude_explore/critical_analysis_v2_zlj.py

完整落地步骤

  1. 数据探索analyze_*_zljcc.py 做 Phase 1 检验(做完即弃的小脚本)
  2. Builderclaude_explore/utils_zlj/必须用 codex global dedup 模式(见 §05 关 3)
  3. Factor 类claude_explore/factors_zlj/,继承同款 FactorBase / RollingIcFactor,签名照抄主干 factors/
  4. 注册claude_explore/factor_registry_zlj.py 注入主 registry,主干 factor_registry.py 不动
  5. Build + 回测用 wrapper(自动注入 zlj 因子)
  6. Phase 4 审视critical_analysis_v2_zlj.py
  7. 报告写成 reports/<topic>_exploration.md 风格的活模板(见 §09)
  8. 若全绿写 PR 描述给用户确认,由用户提 PR 迁入主干
与 main 100% 同构(硬要求)

探索代码的结构必须和主干一致——因为实盘 future_v_0_1/tradingsystem/ib_bigopen_v9_*.py 用的是 main 上的 builder 与 registry。 结构一偏离,PR 时改不动,上线后行为漂移。目标是"通过即 PR copy 过去就能跑,不用重写"。 同时永远不直接动主目录 factors/ / utils/ / factor_registry.py

§05判据电池 · 9 关

§04 是流程,这 9 关是判据。核心只有一个问题: 这个 +pp 是真 alpha,还是过拟合、伪装、或者单年异象? 任一关塌了就毙。每一关下面都挂着一个在这关上摔死的真实因子。

关 1经济直觉先行

判据:能不能用一句话说清"它为什么能预测收益"?说不清 = data mining,停。

正面例子:nbp3d_low = "前几天冷门、没人抢的合约,出大单更纯净"; ret_5d_roll_deep = "急跌抄底";rv3d_band_15_30 = "火苗大小"—— 没有火苗(rv < 0.5)扔汽油也烧不起来,正常燃烧(1.5–3.0)扔汽油助燃,已经是大火(> 3.0)再扔就爆炸(反指); dvol_10d_low_pen = "没有标的端资金共振配合的孤立大单,大概率褪色"。

反例:任何"先跑网格、再编故事"的因子。industry 哑变量 F=3.97 / p=2.4e-15 看起来极显著,但它是 108 选 1 的多重比较产物,且增益纯粹是仓位 reshuffle。

关 2方向与打分模式都要试

判据:low / high 两个方向、binary / ternary 两种打分都跑一遍, 按"四年全改善 + 不误杀"选,不是按单一指标最高选。

nbp3d 方向总 PnLMaxDDCalmar结论
baseline$19.58M10.76%6.37
high$18.23M(−6.9%)14.04%4.66 只有 2026 改善
low$20.52M(+4.8%)9.83%7.19选它 四年 Calmar 全升

binary vs ternary:Calmar 几乎打平(7.38 vs 7.35),但归因拆开看, ternary 在 2024 主动踢掉了 12 笔胜率 58%、平均赚 2.56% 的好信号—— 把 score=6 的票扣到 5 就漏单了。所以选 binary(2024 收益 44.74% vs 35.11%)。 新因子默认优先 binary {0, +1}

同源教训:expiry tolerance 也走了同样的流程。tol=1 初看赢 (Calmar 7.38 vs 7.19),后来发现股票期权绝大多数周五到期,tol=1/3 实际捞不到任何新合约; tol=7 把覆盖率从 25.7% 抬到 31.9%,代价是 2024 MaxDD 炸到 12.27%、Calmar 5.80。 最终选定 tol=0。

关 3先确认数据没 bug——算错的因子会凭空造 alpha

判据:聚合粒度对不对?去重做了没有?时间窗对齐没有? 这一关塌了,后面所有关都是在给幻觉做体检。

三个真实的数据 bug

bug后果正确做法
聚合粒度错:nbp3d 早期按 (ticker, et_day) 聚合,没分 expiry ratio 失真改成合约级 (ticker, expiry, et_day)
(ft, fd) 分组 dump 窗口 bug:每个 signal 只看自己那一份 UW dump 里的 flow, 而 dump 窗口长度 4–14 天不齐 flow 漏看 + 跨 dump 重复不一致。整轮 V8 探索(83 组矩阵)的 +pp 几乎全是人工产物。 F2_pos20 从"+8.63pp / DD 改善"缩水成"+2.18pp / DD 加 1pp" codex global dedup + per-ticker 分组,见下
时区陷阱flow_dt_et.values 会自动转成 UTC naive 与 ET naive 的 entry_time 比较系统性错位 4–5 小时 → count 系统性偏低 → 污染下游所有微观结构因子 永远用 tz-aware Series 直接比较,不转 numpy

UW flow builder 的 codex 规格(所有新 builder 必须照抄)

# Step 1+2: streaming hash dedup(7.6GB 内存机器友好)
seen = set()
for chunk in chunks:
    df = pd.read_csv(chunk, dtype=str, usecols=DEDUP_KEY)
    hashes = pd.util.hash_pandas_object(df[DEDUP_KEY].fillna(""), index=False).values
    new_mask = np.fromiter((h not in seen for h in hashes), dtype=bool, count=len(hashes))
    ...

# Step 4: 按 ticker 分组(不是按 (ft, fd)!),双时间条件 AND
mask = ((flow_time_utc >= sig.signal_time_utc - 14d) &
        (flow_time_utc <  sig.signal_time_utc) &
        (flow_time_et_day < sig.signal_time_et_day))

29 列 dedup key(统一规范):date, time, ticker, strike, option_type, expiry, side, premium, size, spot, dte, price, otm_pct, bid_ask_range, volume, oi, vol_oi, chain_bid_ask, share_eqv, iv_pct, delta, theta, gamma, vega, rho, theo, code, flags, tags

时区 sanity check(做完 builder 必跑)
pd.Series([pd.Timestamp('2023-03-10 09:30:00').tz_localize('US/Eastern')]).values[0]
# 输出 14:30(UTC naive),不是 09:30(ET)→ 立刻意识到陷阱

另外两条数据纪律:CSV 里的 dte 列是按下载日算的, 用的时候必须用 expiry − signal_entry_date 自己重算。 截断后 flow_count = 0有效信息(过去 14 天只有触发信号那一条流), 应该填 0 不是 NaN——早期一次"过于激进"的 bugfix 把 0 改成 NaN,让 2,285 个信号丢了有效信息。

关 4训练池 = 可交易池

判据:滚动阈值只能用能交易的资产训练。 混进 ETF/ETN 会让"低分位"边界被往 0 压,污染阈值。

三条理由:① 分析池与训练池不对齐 = 用的不是同一分布的分位数; ② ETF 有日内到期、流量结构完全不同;③ 样本独立性被破坏。

修法:training_asset_types=("STOCK", "ADR")。nbp3d 修正后训练样本 9,836 / 13,463 = 73%, 新生产口径 Calmar 6.90 / MaxDD 10.58%。

代价要认:报告原话——"新版虽然绝对收益略低(−$1.68M), 但训练/打分同分布,样本外稳定性更可预期"。这是一次主动用收益换稳健性的决定, 不是无损优化。nbp3d_lowret_5d_roll_deep 都加了这个限制。

关 5搜索空间要窄;看 percentile,不看 ratio 最干净的过拟合物证

判据:自适应阈值的候选越多越容易过拟合。 诊断的正确对象不是阈值数值(ratio),而是算法每天选了哪个候选(percentile)

经典案例:ret_5d 的 Deep vs Wide

口径候选数pctile 中位pctile std阈值中位+1 覆盖率CAGRCalmar
Deep p5–255107.18−8.18%12.6%101.55%7.44
Wide p10–90174536.00−1.01%42.5%107.23%7.26

Wide 的 CAGR 更高,但四项诊断全部指向过拟合:

  • percentile std 是 Deep 的 5 倍,而训练 IC 中位数只高 10%——代价与收益完全不成比例。
  • 命中分布呈 U 型双峰:Wide 有 70% 的天数挤在 p10 和 p90 两个极端 (p10 29.4% + p90 40.6%)。这是 argmax over noisy IC 的典型症状。Deep 则 68.7% 的天数停在 p5/p10。
  • 相邻日大跳(|Δpctile| ≥ 20)频率 3.5 倍(Wide 6.6% vs Deep 1.9%)。 每次大跳都是对前一天"最优"的否定,真实信号不该这么频繁颠倒。
  • 最致命的一项——逐年中位 percentile:Deep 四年是 p10 / p10 / p10 / p5; Wide 是 p90 / p90(2023、2024)然后 p20 / p20。
为什么 p90 是致命的

direction=low 下选 p90,意味着阈值 ≈ 训练集 90 分位 ≈ +10%, 也就是 ret_5d <= +10%几乎所有信号都打 +1。 这是退化成恒 +1 的伪信号,IC 表面高只是因为牛市 base rate 把它撑起来了。 报告原话:"这不是自适应,是因子身份在不同年份切换……那一年 Wide 选的根本不是同一个因子。" 换一个 regime(横盘 / 熊市),Wide 的 p90 选择会立刻爆掉。

跨因子可比性:同一套诊断在 atr_pct 上也识别出 Wide 的过拟合特征 (Deep std 7.89 vs Wide 27.22,大跳率 2.8% vs 12.0%),但严厉程度不同—— percentile std 和双峰程度可以当作跨因子可比的过拟合指标

沉淀的监控线:Deep 的 percentile 中位应稳定在 p5–p15、std < 10、 ≥87% 的相邻日 |Δpctile| = 0。如果中位漂到 p20+ 或 std > 12,说明 IC 曲线在变,需要复盘信号语义是否退化。 这是最早期、最干净的过拟合信号检测。

关 6增量归因:捞回新好信号,还是放大老仓位?

判据:把 +PnL 拆成三份——新增 / 被挤掉 / 仓位放大。 如果增益主要来自"仓位放大"或集中在少数几笔,那是 sizing 杠杆假 alpha,不是因子。

案例三分账判定
nbp3d binary
(+$3.03M)
新增 82 笔 +$2.22M · 挤掉 21 笔 −$0.71M · 仓位复利 +$1.52M 合格 增量不是"替换差信号",是捞回原来差 1 分未能入场的信号
rv3d_band_15_30 common 1,966 笔 · removed 0 · added 602 笔(mean_y 0.562, +338.3) 教科书 100% 真新增,0% reshuffle
uw_sparse_ask 22.3% 是 alpha,77.7% 是仓位复利 假 alpha
uw_flow_count_le1 55% 是新增,但其中约 64% 又集中在单笔 FSLY 上 拿掉就崩
rvol0 早退(干净基线) common 仅 27%(new_9f 上只有 9%),73–91% 是资金 reshuffle 真实时点效应 2025 为负,靠 2026 单年撑

关 7稳健性电池:drop-2025 / trim-5 / 分年 / t-stat

判据:alpha 不能靠单年或单票。2025 是样本量最大的一年(n ≈ 21K) 且 V9.1 当年 CAGR 197%——任何因子都要问一句"是不是被 2025 主导了"。

因子全样本 difftdrop-2025 diffdrop-2025 ttrim-5 t判定
bear_C_tk+0.2892.800.0581.012.12 塌 5 倍、失去显著性 = 2025 单年异象
bull_B_tk+0.131−0.019 剔 2025 直接转负
bear_A_tk+0.1174.35+0.1523.583.91 全过 剔 2025 反而更高,3 正 1 微负

口径定义:diff = active 桶 avg_ret − rest 桶 avg_ret(%); drop25 = 剔除 2025 后重算;trim5 = 剔除 active 桶 top-5 winners 后重算。

但是——这一关过了也可能死在下一关

bear_A_tk 是全语料里唯一一个"所有稳健性测试都通过"的候选, 进组合回测却是 −$1.28M(320 笔 marginal admission 在 2025 亏 $1.32M)。 单因子分层 alpha ≠ 组合 alpha。详见 §09

关 8简单优于复杂;绝不按 score 桶加分

判据:每加一个约束、一个分桶,都在扩大搜索空间、让噪声更像信号。 因子必须是 pure standalone 函数:输入原始特征,输出分数, 不可以读 score_raw 或其他因子的 *_score

约束本身就可能是过拟合产物

bear 系列加上 sparse [1,2] 和 max_share ≥ 0.70 两个约束, "没有带来真正的 alpha 提升,反而让因子对 2025 单年的依赖增加"。最后活下来的 bear_A_tk最简单的那个(只有一个 share ≥ 0.70 条件)。

"按 score 桶加分" = admission rule,不是因子

2026-05-09 的统一评估开篇就撤回了十天前自己的建议: "按 score_raw==5 / >=6 拆分评估 +1 booster 效果——这种拆分本身有过拟合嫌疑, 分桶把搜索空间扩大、容易让噪声看起来像真信号",因为那等于"拟合了 score=5 噪声子集和 score≥6 噪声子集"。

决定性判例:current_call_low_oi

score_raw==5 条件时 CAGR +7.34pp,看起来很强。 去掉条件改成纯 +1 因子后,CAGR 跌到 −10.49pp,比 baseline 还差。 说明 alpha 完全集中在 boundary,filter 在全样本上没有独立 alpha—— 这不是 +1 因子,是 admission rule。2026-05-19 完整审计后放弃,不上线、不提 PR。

怎么应用:写因子时检查 score() 有没有读 score_raw 或别的 *_score 列 → 有 → 不是 pure 因子,重新设计。想做"score==K 边界准入"的需求, 应该在组合构建层加 admission layer,不要包装成 factor。 评估新 +1 因子时先看全样本 t-stat,不只是 score==5 子集。

关 9主动证伪自己("打脸"文化)

判据:做出好结果后的第一件事,是攻击它。 列出至少三条 threat,逐条去找反证。

判例:nbp3d 的 lag_1week,"+6pp 年化"被作者自己推翻

阈值滞后一周(年化 79.50% / Calmar 7.78 vs no_lag 77.12% / 7.18),解释是"降噪"。 作者随后提出三条威胁并逐条验证:

  1. Threat 1 — 排序反了。阈值周变化 std 的排序与收益不符: 2024 的 std 0.507 是最稳定的一年,却正是 lag 赢得最多的一年; 2023(std 0.862,按"降噪"假说应当最受益)反而亏 $31k
  2. Threat 2 — 57% 来自一笔。2024 的 +$347k 里, SMCI 2024-02-20 单笔独占 +$179k——它的 ratio 恰好落在 threshold[wk]threshold[wk−1] 之间。 这是边界运气,不是机制性优势。
  3. Threat 3 — 2026 三个版本完全一致(Calmar / Sharpe 2.20 / 0.77 逐位相同), 因为当年信号密度上升到 93 只票/天。如果机制为真,不该在高密度下消失。

结论:+6pp 高度依赖单一事件,不具备统计稳健性 → 恢复 no_lag

沉淀的三条方法:① 分年均值的改善可能来自单笔暴击; ② 评估 A/B 必须拆解仓位复利 × 独有交易 × 对手独有交易; ③ 阈值时间序列的抖动统计,是检验"降噪"类假说最直接的手段。

§05b判据阶梯 · 10 级

9 关是原则,这 10 级是硬线——每一条都是被某个具体因子逼出来的, 按它们出现的时间排。复查别人的 PR 时对着这张表走最快。

#硬线数值判据是谁逼出来的
1Step 1 原始分桶 ANOVA p < 0.05;最优桶 t;四年同号 入门线。注意:raw 四年同号只是必要条件
2Step 1.5 A 档残差二次筛
最常用的杀手锏
score_raw ≥ 6 的已准入池里重新分桶:spread t ≥ 2(score_bin × 年) 网格 ≥ 80% 同向 生于 bear_pen 的 3/19 = 16%(raw 四年同号,残差里全是噪声)。 杀死 vwap_deep(47–69%)、rvol_tod(59%)、opex_dist_days(raw p=1.23e-30 但残差 t=1.13)
3共线硬线 stock_price_score ρ > 0.5 或与 ret_5d_score ρ > 0.4 → 直接毙 gamma_abs(ρ=+0.502,t=3.14 全骗人,本质是 low-price 代理,回测 −23%); price_vs_ma20(ρ=−0.401,全样本 p=1.8e-9 但 A 档单边 spread 反向)
4rolling 对照反过拟合 固定阈值因子上线前必须出 rolling 对照;两者的回测差额就是过拟合 alpha 通过:dvol(固定 9.32 vs rolling 9.19,噪声级)、atr_low_pen(rolling 反而更好)。 塌房:bull_boost(−$2.69M,rolling 下 2026 IC 翻负,证伪了全样本 +0.151 是 in-sample fluke)、 rv3d rolling band(Calmar 6.96 vs fixed 11.64)
5percentile 分布诊断 看算法选了哪个候选,不看阈值数值。std、双峰、大跳率、逐年中位 ret_5d Wide(见 关 5
6阈值 plateau + 整数纪律 两位小数的阈值一律怀疑数据窥探;取 plateau 中段的整数,刻意避开 in-sample 峰 dvol 0.76 → 0.80;atr_low_pen 2.31 → 2.5。见 §07
7added / removed / common 三分账 common(真时点效应)占比要高;reshuffle 与仓位复利要拆出来 rv3d 100% added(正例);uw_sparse_ask 77.7% 复利、 rvol0 73–91% reshuffle(反例)
8集中度与 regime drop-2025 / trim-5 / 逐年美元 ΔPnL / 扩窗切点。 回测窗口必须含 2026 sec_rvol10:23–25 窗口看着还行,含 2026 后每年都亏。 regime / vol 类因子不含 2026 会假通过
9方向是自由变量 Step 1 与残差都过、但 +1 booster 反噬时,先试 gating 方向(坏的一侧 −1)再判死 dvol:同一特征 band(+1) Calmar −29%,gating(−1) +14.8%。见 §07
10预注册 OOS 只评增量段(累积 ≥30–100 笔边际准入后判定);写明不过即关闭、 不得为通过而调阈值 / 口径 / 判定标准、不得以"再等等"续命超过一次 vwap_deep 的原版 OOS 标准检验力 ≈ 0——"推进一周后重跑全窗口", 而全窗口指标 99% 由 in-sample 数据构成,几乎必然通过。走完形式上的"通过"不构成任何证据

§06九维健康度与判决

Phase 3 只证明"组合数字变好"。Phase 4 回答"变好的方式健不健康"。 工具:claude_explore/critical_analysis_v2_zlj.py(解析 base 与 +factor 两份 executed_trades.csv,输出九项指标)。

#维度怎么算健康阈值
1PnL 集中度 关键top-1 / top-5 trade 占 added 总 PnL 的比例top-5 < 80%
2Median 回报added trades 的 median(realized_return_pct)≥ baseline median 的 80%
3真 promote vs existing 关键真 promote(加因子后 6 分、原 8 核 5 分)的 mean,对比 base8 里原本就 6 分的 existingratio ≥ 0.9
4季度稳定性 关键季度 added PnL 为净正的比例> 70%
52026 trade-level 关键2026 added 的 median 与胜率median ≥ baseline 且 win ≥ baseline
6阈值平台上下相邻 2 个阈值是否都同向改善平台稳定
7Trigger rate+1 占比8% – 18%
8Sizing 杠杆贡献 关键added PnL 占 final_equity 总 Δ 的比例added > 60%
9交易量扩张分布各年扩量比例最大/最小差 < 1.5×
判决规则

9 项里 ≥ 7 项绿,且关键项(1、3、4、5、8)全绿,才算"健康因子"。 少一项关键项都不算。

单独说一句:真 promote ratio(最难的一项)

这一项问的是:因子新提拔上来的信号(5→6 分),和原本就 6 分的信号一样好吗? 如果新提拔的明显更差,说明因子在往门槛里塞垃圾,即使总 PnL 上去了也是靠数量堆的。

经验

历史上几乎所有"+pp"的候选都不达标,直到 uw_flow_codex_combo_070 第一个拿到 1.19× (新提拔的比原本 6 分的还好)。看到一个因子 CAGR 涨了很多,先问这一项。

十条最常踩的陷阱(/explore-factor 原表)

陷阱表现防范
(ft, fd) 分组 bugflow 漏看 → 因子值失真。V8 全部探索结果不可信的根本原因新 builder 必须 codex global dedup + per-ticker
打分稠密count_low 给 49.8% 的信号 +1 → 没区分度+1 占比 8–18% 才放行
ternary 错杀F2 的 −1 把 score=6 信号打到 5 → 漏单默认 binary 0/+1
6 核错觉旧基线上 +12pp,到 8 核只剩 +4pp(被 nbp3d / ret_5d 吸收)直接用 8 核基线 105.35
base8 contaminationbuild 时混入 rolling 因子导致 warmup drop复现 105.35 时只 build 静态因子
start-date 错默认 0310 多吃 warmup → baseline 少 7pp必须 --start-date 2023-04-20
未传 end-date默认 2025-12-31,2026 数据被丢始终显式传 --end-date
timezone 陷阱.values 自动转 UTC naive永远用 tz-aware Series 比较
PnL 看似 +pp 实则集中top-1 trade 占 64% added PnL,拿掉就崩Phase 4 #1
"sizing 杠杆"假 alphascore+1 让其他高分票仓位放大Phase 4 #8:拆 added / boosted / same
2026 退化整体 +pp 但 2026 反降Phase 4 #5 必查 2026 trade-level

§07阈值与 rolling 校准(反数据窥探)

整数纪律:两位小数的阈值一律怀疑

判例:dvol 的 0.76 → 0.80

初版 cutoff 用 0.76。用户一句"小数点后两位看起来有过拟合嫌疑"点破之后, 追溯发现 0.76 来自 Step 1 的 in-sample qcut(6) 下边界(0.762 四舍五入)—— 本质上是用全样本 y_2d 算出来的最优切点,就是数据窥探

响应:对 [0.70, 0.85] 跑 robustness sweep,看是 plateau(信号 robust)还是 sharp peak(过拟合)。

阈值0.700.720.740.760.780.800.820.85
Calmar(asof0601)11.3111.5011.4711.5611.6011.5311.7411.77
MaxDD整段 10.85% – 10.93%,几乎不动 → [0.70, 0.85] 全段是 plateau

最终选 0.80,四条理由:(a) 整数物理含义清晰——"低于历史基线的 80%"; (b) 跨 asof 一致,不踩任何一个 in-sample 峰(asof0511 的峰是 0.78,特意不取; 0.85 在 asof0511 塌过但在 asof0601 最好);(c) MaxDD 改善幅度与 plateau 其他点一致; (d) 跨四年 mean_pnl 三正一负且总体改善。

沉淀的规则

任何带 2 位小数的阈值都该被怀疑数据窥探,必须做 robustness sweep, 然后取 plateau 中段的整数(而不是 in-sample peak)。

atr_low_pen 照这个方法选了 2.5(候选集冻结在 {2.0, 2.5, 3.0}, 扫描峰其实是 2.31):plateau 中段的圆整点,且"ATR 不足价格 2.5%"物理含义干净。

rolling 因子的两段式构造

  1. Stage 1 — 每日 IC 搜阈值训练窗 [D−97, D−7)那个 7 天 gap 是关键:2 日标签需要时间 settle,不留 gap 就是前瞻。 训练池只用 STOCK/ADRmin_samples=50候选分位必须窄
  2. Stage 1.5 — 桥接成 ratiorv_ratio = rv3d / date_threshold[D]横截面而非逐票(单票 90 天样本太稀疏;逐票会让 KO 的"自己的高"和 TSLA 的"自己的高" 拿到同一个 +1)。ratio 化不是可选项:rv3d 中位数四年从 18.8 漂到 33.0(+76%), 不归一化的固定阈值必然失效。

dvol_10d_ratio 反过来是自归一化的 (mean(dollar_vol[T-10..T-1]) / mean(dollar_vol[T-30..T-11]),四年中位稳定在 1.04), 所以它可以直接用固定阈值,不需要 rolling。这也是为什么它的 rolling 对照只是 sanity check。

方向选错就塌房:band(+1) vs gating(−1)

同一个 dvol_10d 特征,两个方向的结果完全相反:

路线 A:band +1(奖励中段)路线 B:gating −1(拦下低尾)
设计dvol ∈ [0.91, 2.20) → +1dvol < 0.80 → −1
受影响比例~60%~20%
Trades+674(涌入)−179(拦下)
Calmar(asof0601)7.09(−29%11.53(+14.8%)
MaxDD15.64%(+3.61pp)10.93%(−1.10pp 改善)

为什么 +1 败:booster 把 score=5 的边缘信号推上 score=6 的准入线, 其中相当一部分是"其他因子分数不够、只能靠 dvol band 凑过门槛"的低质信号,稀释组合质量、抬高回撤。 同样的失败模式在 rv10d(−32%)、gamma_abs(−23%)、chain_ba+1(−30%~−56%)上重复出现。

为什么 −1 成:(a) 受影响一边 ≤ 20%;(b) 离散硬定义; (c) trades 减少而非增加——gating 只改 admission 门槛,不引入新的边缘信号。

硬规则(原文)

"看到 Step 1 + admitted 残差都过、但回测 +1 booster 反噬时, 直接试 gating 方向(−1 on bad side)作为对照。同样的特征, 方向选错塌房(−29%),方向选对增益(+14.8% Calmar 在 asof0601,+36% 在 asof0511)。"

dvol 那一天一共探索了 8 个新因子,7 个否决、1 个通过—— 而唯一通过的那个方向是 gating,不是任何 +1 booster。

§08对抗式多智能体证伪

重要因子上线前的标配。方法:每个 agent 负责试图推翻一个论点, 不是验证它。产出不是"能涨就上",而是 ship-with-caveats + 信心分数

八维裁决表(dvol_10d_low_pen 第 10 因子,2026-06-03)

维度问的是什么裁决信心
Real alpha删掉的是真烂单,还是只是机械降仓?成立0.82
Threshold plateau0.80 是 plateau 还是过拟合尖峰?成立0.85
Gating 不对称−1 成、+1 败,是结构性的还是 direction-mining?成立0.78
No look-ahead特征有没有偷看未来?成立0.90
Orthogonality与 9f 真的正交吗?部分 结论对、数字夸大0.86
可复现性baseline 跨 run 稳定吗?部分 不可复现0.80
Yearly 广泛性"3/4 年改善"是真广泛吗?部分 符号广,增益集中 20250.78
Methodology方法论足以上线吗?部分 ship with caveats0.70

综合裁决:SHIP WITH CAVEATS,信心 ~0.80(方向高、绝对幅度低)。

真证据 vs 夸大之处(两边都要写进报告)

真证据

删掉的 236 笔是真负 EV(return% +0.071% / 胜率 49.6%,对比保留组 +1.268% / 56.6%,t 检验 p=0.0058); 暴露几乎没降(利用率 61%→58%)但收益反升 → 是真 alpha 不是降仓; 阈值 0.70–0.85 是 plateau(Calmar CV 3.9%);band(+1) 同时变差证明 gating 不对称是结构性的; 前瞻检查 0/3361 mismatch。

夸大 / 存疑

PR 说 orthogonality < 0.03,实测 6/9 超标(rv3d +0.066)——不过 penalty 分数 R²=0.006,仍 99.4% 独立; "3/4 年改善"无一显著,2025 一年就贡献 104.7% 的增益,2026 的 MaxDD 反而恶化; 某个 −$964 的数字很脆(阈值挪到 0.85 就翻成 +$503)。

最深的 confound(值得单独记住)

dvol 的 gating 删掉的正是 SPY guard 本该拦住的低量能信号, 所以它的收益与 guard 坏得多严重成正比。 PR 报告的 +14.8% 是 guard-ON 下界,在 broken-guard 环境下能虚高到 +64.6%。 生产里的真实边际接近下界。这就是为什么 §01 的第 2 条(guard fail-open)是数据卫生的第一梯队。

对抗验证真正的价值:抓出差点漏掉的前瞻

exit 方式 sweep(35+ 变体,7 个 agent 对抗验证)里,初版的 walk-forward gate 按 prod-exit 过滤 trailing 交易,但它需要的 day2_marginal 依赖 2td_close 的结果—— 这个结果晚于 prod 的周一开盘出场(周五入场的情况晚 +30 小时)。 结果是 约 6% 的决策偷看了未实现价格

修法与结论

gate 必须 on ex_2td < entry(只用已平仓的 trade 喂 buffer)。 修正后边际只小幅缩水、结论稳:K=80 → Calmar 10.39、2026 Calmar 从 0.98 抬到 3.12; K=40–80 是 plateau 不是单点。同一轮还发现 SPY-regime 版用当日 SPY close 也有轻微 look-ahead。

推荐口径:ship,但信心只有 ~0.65,附四条 guardrail——用 ex_2td<entry 重建确认、 先 shadow-deploy ≥1 季度对账、生产需维护最近 K 笔已平仓 trade 的 day1+day2 收盘 buffer、 监控 rolling day2-marginal 与 gate 切换率。

仓库里并存 codex_explore/ · minimax_explore/ · claude_explore/ 三个 AI 探索工作区——多智能体是研究方法论,不是噱头。

§09因子坟场 · 判例库

一个团队的成熟度,看它敢不敢枪毙自己做出来的"好结果"。 这些否定性结论全部归档在 reports/claude_explore/看"哪一关塌了"比看结论更有用——它决定了换基线之后还值不值得重验。

死法四分类(决定重验价值)

类别含义换基线后值得重验吗
A · 特征层无信号Step 1 就死,特征本身没有预测力不值得 与基线无关
B · 共线 / 残差层死与已上线因子重叠,被吃掉值得 基线换了共线对手就换了
C · 回测 / 容量层死信号面绿、组合层塌最该重验 容量对手会变
D · 搁置证据不足以判死,也不足以上线看新证据
2026-08-31 全量重验的结论

在"去 nbp3d + 含 atr 惩罚"的新基线上重验全部历史被否因子: 没有任何一个复活(17 个特征 ANOVA 全部 p > 0.05)。 但重验过程反向查出了两条现役腿的问题——见 §12

判例总表

被毙的因子 / 方向当初多诱人哪一关塌了
ret_5d Wide rolling(p10–90)CAGR 107% > Deep 的 101% 关 5 percentile U 型双峰,2023–24 退化成"恒 +1"蹭牛市 beta——那一年它根本不是同一个因子A
current_call_low_oiscore_raw==5 条件下 +7.34pp 关 8 去条件改纯 +1 后 −10.49pp 比基线还差——是 admission rule 不是因子,alpha 全在边界A
整轮 V8 探索(83 组矩阵)一堆 +pp 关 3 (ft, fd) 分组 dump 窗口数据 bug,+pp 是人工产物。F2 复测从 +8.63pp 缩到 +2.18ppA
sparse 系列(sparse_ask / B、C 变体)+0.289 / t=2.80(全表最高) 关 7 drop-2025 塌到 +0.058 / t=1.01,是 2025 单年异象;"加 sparse 约束"本身就是过拟合产物A
expiry-match bear 全系列 关 1 / 7 全样本已是负贡献——限定到同一合约链后 bear 不再代表"反转",沦为双向消息混杂的噪声A
nbp3d lag_1week+6pp 年化 关 9 57% 来自单笔 SMCI 边界运气,2026 三版完全一致 → 恢复 no_lagA
count_14d_zero / le1回测 +17.53pp / DD −1.13pp / 四年全正 关 3 + 关 6 数据 bug 之外,55% 新增里约 64% 又集中在单笔 FSLY——拿掉就崩A
F2(opening ask spread)全阈值+1~3pp Phase 3 未达标:边际,且 DD 全部变差约 1ppC
gamma_abs / high_gammat=3.14 阶梯 3 全骗人——是 low-price 代理(ρ=+0.502 与 stock_price_score),回测 Calmar −23%B
price_vs_ma20全样本 p=1.8e-9 阶梯 3 与 ret_5d ρ=−0.401,A 档单边 spread −0.536 反向B
dte_staticRaw p=0.03,四年同号,重验时是唯一接近显著者(p=0.088) 阶梯 3 + 回测 与 otm_score ρ=+0.334,A 档残差 t=−0.08、网格 65%;上卡回测 Calmar 10.96→7.81B
opex_dist_daysraw ANOVA F=36.6,p=1.23e-30,形状干净单调 阶梯 2 A 档残差 t=1.13,逐年符号乱翻。raw ANOVA 显著 ≠ A 档 booster 有效B
dist_52w_high 阶梯 3rule_hit 吸收(BigOpen 的 rule 里本来就含 breakout / new high 类)B
rvol_tod gateCalmar 11.10 → 12.72,终值 $66.7M 四条一起:只有 thr=3.0 有效无 plateau;残差网格 59%;2023 全面反向(高 rvol 当年是好信号); 96% 的改善来自 2026 的 34 笔回测赢了照样毙C
vwap_deep(alpha 档 / 容量档)全宇宙分层强:F=19.6 / p=1.4e-19 / t=5.17,与 10f 最大 |ρ| 仅 0.038 阶梯 2 + 6 阈值景观崎岖——动 0.1pp 终值差 $13M,中间档(−1.0%)跌破基线,不存在 plateau; 残差网格 47–69% 全阈值 fail;扩窗到 2024 年底全为负(站在 2024 年底会被直接否决C
atr_pct 高尾 booster信号面全绿:score=5 边际四年正 t=+3.35,残差网格 79% 容量挤占:新增 492 笔(+1.08%/笔)挤掉 114 笔更好的(+2.07%/笔)。Calmar 11.10→8.98C
sec_rvol10(行业量能)残差网格 100%(12/12),历轮首见 回测每年都亏,合计 −$10.66M;2026 Calmar 6.15→2.89 腰斩C
dvol_10d_band(+1 版) 关 9 方向 回测 −18%(低尾惩罚版才是上线的那个)C
RF 整体替换评分卡ML 看起来该赢 决定性否决:Calmar 7.97 → 0.74。HYBRID sizing 版 2026 +16~24% 但单年集中,未达 admission 级C
industry 哑变量F=3.97 / p=2.4e-15 关 1 纯 reshuffle + 108 选 1 的多重比较A
中概股多空"中概是庄股、多空都做"的直觉 空头负 EV(CAGR −3~−7%、MaxDD 29%、PF<1);多头剔除 JD+GDS 后从 6.78% 塌到 1.49%(edge=两只票)。组合不优于只做多A
闲置资金指数 overlay"平均闲置 55%,配低相关底仓增厚" "平均闲置 55%"是均值骗局(p95 利用率=100%,可靠闲置≈0);黄金低相关是 regime 产物且已衰减(GLD 周相关 2023 −0.34 → 2026 +0.18); overlay 的低相关价值结构上不可跨 regime 验证(核心策略依赖 UW flow,2023-04 前不存在)D
7 个动量 / 波动 / catalyst 候选
rs_5d_vs_spy · vspike_20d · dist_52w_high · intra_30m_ret · days_to_earnings · opex_dist_days · sector_5d_excess
两批各有亮点全部死在阶梯 2:A 档残差 t ≥ 2 的门槛,最高只做到 1.19。结论:rv3d 是个例外,不是规律A/B
Step 1 就死的一批
delta_abs · theta_abs · chain_ba · vol_oi · vol_breakout · size
特征层无信号:p = 0.472 / 0.232 / 0.118 / 0.79 / 0.858 / U 形。 vol_oi 还有 pre-selection bias(前端 premium≥100K 已隐式条件高 vol_oi); vol_breakout 去聚类后 F 从 3.27 掉到 0.032——纯聚类假象A

三个最贵的教训(各自生出一条项目硬规则)

教训一 · bear_A_tk:单因子分层 alpha ≠ 组合 alpha

bear_A_tk 是全语料里唯一一个所有稳健性测试都通过的候选: n=8,345(占 15.3%)、全量 diff +0.117pp、t=4.35 全表最高、三正一微负、 drop-2025 后 diff 反到 +0.152 / t=3.58、trim-5 后 t=3.91 只衰减 10%。 报告当时的结论是"它的 alpha 是真的,不是 2025 单年异象"。

进组合回测:−$1.28M。320 笔 marginal admission 在 2025 亏掉 $1.32M。

规则:分层检验再漂亮也只是必要条件。资金受限的组合引擎里, 新增信号要和被它挤掉的信号比,不是和零比。这条催生了 factor_admission_alpha_pitfall

教训二 · sec_rvol10:诊断层的 regime 中性 ≠ 生产层能中性

行业指数动力学四条腿里,sec_rvol10 是唯一残差网格拿到 100%(12/12) 的, 项目历史上首次。回测结果:每一年都亏,−$0.32M / −$1.83M / −$1.46M / −$7.05M, 合计 −$10.66M;2026 单年 Calmar 6.15→2.89 腰斩。

根因:残差网格是用每格内自适应三分位算的(regime 中性),所以才 100% 干净; 但任何可部署的阈值(固定的或滚动分位的)都把 regime 偏斜重新灌了回来—— 固定 0.015 在 2026 的触发率是 2023 的 8.6 倍

规则:诊断用的中性化手段不能默认迁移到生产。同一轮还确认: 23–25 窗口那点 Calmar 改善是靠 return-drag 压 MaxDD(降杠杆就能廉价复制), regime / vol 类因子终审必须带 2026

教训三 · atr 高尾 booster:别拿"资金约束"当免死金牌

booster 当年死于容量挤占——新增 492 笔(+1.08%/笔)挤掉 114 笔更好的(+2.07%/笔)。 两年后的假设是:既然死因是容量,那么换干净基线 + 叠加 rvol0 早退(提前一天释放资金)应当复活。

基线base+atr_pct 静态高尾+atr_pct_roll_high+dte_static
new_10f10.968.406.117.81
new_10f + rvol0@0.711.708.926.378.29
new_9f12.199.026.458.46
new_9f + rvol0@0.711.999.296.608.86

数字为 Calmar。四个基线全否;rvol0 释放资金只把 8.40 抬到 8.92,离基线 11.70 还差 2.8。

"容量挤占"这个当年的死因诊断被证伪了:booster 不是被挤掉的,是它自己有害 (高 ATR 单止损率高、集中度抬 MaxDD——加 booster 后 trades +24% 而年化几乎不动,MaxDD 从 11.02 抬到 14.30)。

规则:资金受限引擎里 +1 booster 的真实门槛 = 新增要好过被挤掉的 (热年的机会成本约 +2%/笔),不是"边际均值为正"。别再拿资金约束当免死金牌。

§10现役因子、打分规则与角色

注册表里共 62 个因子CORE_FACTORS 6 个。 每个因子对一条信号产出一个整数分项,求和 = score_raw,门槛 min_score = 6。 生产线是 6 核 → 8f → 9f → 10f 逐步长出来的。查命令:python3 factor_registry.py list / show <key>

核心 6 因子 — 刻画"信号本身的质量"(静态分箱,−2 ~ +2)

因子打分表衡量什么 / 为什么这么打
iv
隐含波动率%
≤25 → −2
(25,50] → +1
(50,100] → +2
>100 → −1
中高 IV(50–100)最好——说明确实有大事在酝酿。IV 太低(<25)"师出无名",太高(>100,彩票/暴雷区)都扣分
stock_price
股价 $
≤20 → +2
(20,50] → +1
(50,150] → −1
(150,500] → −2
>500 → 0
越便宜 % 涨幅空间越大。低价股给 +2——右尾大赢家最可能从这里出
otm
虚值度%
≤2 → −2
(2,5] → −1
(5,10] → 0
(10,20] → +2
(20,50] → +1
>50 → 0
甜区 10–20% OTM:真方向性押注,而非近月对冲或做市。otm 还兼任硬准入: 只有 otm_pct ∈ [5, 50]闭区间)可交易。2026-04-27 由 (5,50] 改为闭区间,CAGR 105.69 → 106.25
asset_typeSTOCK / ADR → +1
ETF_ETN → −1
个股期权流带 idiosyncratic 信息;ETF 大单多是对冲或宏观仓位
rule_hitrule 非空 → +1
rule 为空 → −1
被 UW 标了 rule = 落入已知形态,比"裸流"更可能是真信号
time_slot
美东时间
open 09:30–10:30 → +1
mid_morn / midday / pre_close → 0
close 15:00–16:00 → +1
开盘 = 对隔夜消息的即时反应;收盘 = 机构为次日布局;盘中多是算法拆单噪音

核心 6 分理论上限 +9、下限 −8。五个分箱因子都走 score_numeric(),NaN → "MISSING" → 记 0 分。

后加的 4 个因子 — 补上"时机与环境"的边际

这 4 个不是静态分箱,而是按交易日滚动、用 IC 自动找最优阈值、严格无前瞻 (训练窗 [D−97, D−7),留 7 天 gap 让 2 日收益完全 settle)。

因子(属于)打分逻辑衡量什么 / 为什么
nbp3d_low
第 7 · 8f
ratio = (看多权利金 − 看空权利金) / 总,看多 = {call 吃 ASK, put 吃 BID}, 合约级聚合 3 个交易日;ratio 低 → +1(滚动阈值) 低 ratio = 最近还没被大量看多流挤过,抓的是"第一波 / 早期布局"。contrarian 抢早入场
ret_5d_roll_deep
第 8 · 8f
ret_5d = close[T−1]/close[T−6] − 1近 5 日跌得越深 → +1; 阈值只从 [5,10,15,20,25] 深分位里选 大单 + 标的刚刚深度回调 = 拉升前低吸。窄尾分位是设计,为了避免在宽尾上搜出一个过拟合的漂亮阈值
rv3d_band_15_30
第 9 · v9_2
rv_ratio = rv3d / 当日 IC 阈值;落在 [1.5, 3.0] → +1,否则 0 3 日已实现波动的"金发姑娘区间"。<0.6 太死寂、[0.6,3.0] 好区、>3.0 太狂暴(反指)。 walk-forward 显示最优 band 随样本单调收敛到 (1.5, 3.0),不是拍脑袋
dvol_10d_low_pen
第 10 · main
dvol_10d_ratio < 0.80 → −1惩罚方向,不对称),NaN → 0 近 10 日期权美元成交量低 = 低流动性低关注度环境,这类信号统计上负 EV 偏多。 对抗式验证证明它删掉的 236 笔确是真负 EV,是真"删坏单"不是单纯降仓
事实 · 这 4 个因子当初怎么被选出来 / 定参数的
  • nbp3d_low:方向 low(弃 high,四年 Calmar 全升)· binary(弃 ternary 防误杀)· tol=0 合约级精确 · 只用 STOCK/ADR 训阈值 · no_lag(lag_1week 的优势经查是单笔 SMCI 假象)
  • ret_5d_roll_deep窄尾 [5,10,15,20,25](弃宽尾 p10–90,后者退化成"恒 +1"蹭 beta)· static −10% 只作 sanity baseline(阈值 ±5% CAGR 差 <2pp,证明不过拟合)
  • rv3d_band_15_30:walk-forward expanding window 最优 band 单调收敛 (train 2023 → (1.0,3.0),+2024 → (1.2,3.0),+2025 → (1.5,3.0);hi=3.0 完全稳定)· 6/6 段 OOS 全正 · 63 格参数网格里 25 格四年同号且全部聚在同一个 plateau · 月度 expanding 27 个月 100% 落在 plateau 内 · 增量 100% 真新增 0% reshuffle
  • dvol_10d_low_pen:先做窗口选择(3d 的 F=0.80 p=0.552 无分层;5d 是奇怪的单尖峰; 10d 的 F=8.83 p=2.2e-8 带状结构干净)· 与 9f 每一个分项 |ρ| < 0.03 · 阈值 plateau 取整 0.80 · 8 维对抗式验证 ship-with-caveats 信心 0.80

因子角色三分法 —— 决定 IC 漂移该怎么读

这是第二层过滤的核心纪律:不同角色的腿,IC 漂移的含义完全不同。 别对着 IC 乱开药。

角色成员IC 漂移怎么读该用什么证据
排序腿
ranking
ret_5d_roll_deep rv3d_band_15_30 nbp3d_low atr_low_pen_roll otm 真信号 rank-IC 漂移是真的失 grip可考虑 rolling 刷新;但别单年定案,先复检多窗
风控腿
risk
iv stock_price time_slot 多为假警报 rank-IC 测的是均值排序,而它们的组合价值在方差、尾部、集中度上——IC 看不见 方差 + ablation 判,默认保持静态,别只看 IC 动手
门型腿
gate
rule_hit asset_type dvol_10d_low_pen IC 不可靠只看 ablation

写因子的代码契约

成员类型契约
nameabstract property → str短 key,如 'iv'
descriptionabstract property → str一行中文描述
column_nameabstract property → str输出列名,如 'iv_score'
scoring_tableproperty → dict | None 返回 None 是承重语义,不是装饰——它是 build_factors.py 判断"这是不是 rolling 因子、要不要删 warmup 行"的唯一依据
score(df)abstract → DataFrame 纯函数、幂等、无副作用;必须返回 copy,不得修改入参;只加 self.column_name 一列

输入 df 必须含 signal_id / signal_date_et / ticker / y_2d。 输出值域由因子自定(−1/0/+1 或 0/1)。热身期(历史不足)的行输出 NaN,由调用方决定怎么处理。

NaN 的两种语义,必须分清

rolling 校准未就绪 → 输出 NaNbuild_factors.py整行删掉。 这样 score_raw 才不会在校准窗口内被 fillna(0) 静默"中性化", 避免早期窗口因部分覆盖被系统性 inflate / deflate。

校准已就绪、但单行特征缺失 → 因子内部应该给中性 0 分保留该行rv3d_rolling 已这样实现)。其它 rolling 因子目前仍把特征 NaN 一并当 NaN 丢。

因子不得偷看 score_raw 或别的因子分数

这一点靠调用顺序保证,而不是运行时断言score_raw所有因子跑完之后才创建(build_factors.py), 调用 score() 的时候它在 DataFrame 里根本不存在。 全量 grep factors/*.py 确认过:score_raw 只出现在 docstring 里。

所以这是一条你必须自己遵守的纪律——写出依赖它的代码不会报错,只会在 review 时被打回。

官方因子集

集合成员理论上限min_score状态
8f6 核 + nbp3d_low + ret_5d_roll_deep+116探索基线
9f8f + rv3d_band_15_30+126历史
10f9f + dvol_10d_low_pen+12(实测 +11)6生产基线
11f +atr10f + atr_low_pen_roll+12(实测 +11)6候选
12f11f + vwap_deep_*6非生产 vwap 腿已否决,12f 只是组合测试留档
为什么加了 atr 上限还是 +12

atr_low_pen_roll 是 0/−1 的门型因子,只会往下压不会往上加,所以 不抬高 max score,6/7/8+ 的仓位权重档语义不变。同理,实盘 2026-08-17 拔掉 nbp3d(一个 0/+1 booster)等价于把它恒置 0,MIN_SCORE=6 和权重档也都不用改。

§11上线与上线之后

Phase 4 全绿只是研究结束。一个因子真正"活着",要走完 PR → shadow → 分阶段 era → 逐笔对账 → 月度体检这条链。

1. PR 描述模板

命名沿用 <topic>_pr_des.md(现有范本 14_flow_factore_pr_des.md, 注意 factore 这个拼写是用户定的,不要擅自改)。骨架八段:

段落必须包含
TL;DR一句话说清规则 + baseline / 新版的四项核心数字 + Δ
直觉 / Why关 1 的那句话。并说明与已有因子为什么正交
复现命令一段可直接粘贴的 bash,含完整 --start-date / --end-date, 并写出期望输出("期望 CAGR=xx.xx, DD=xx.xx, N=xxxx")
全阈值扫描整张表,含触发率、四项指标、"四年正?"一列。要能看出是 plateau 还是尖峰
按年表现逐年 vs baseline 的 Δpp 与 Δ笔数
文件改动逐文件 + 行数;明确"无主干现有代码改动 / 无依赖升级"
Risk 与已知限制把弱点自己写出来(哪一年退化、阈值是不是 rolling、哪个 caveat 没解)
实盘同步 + 验收清单实盘 compute_score() 要加什么;勾选式验收项

2. 分阶段 era 上线(对账的前提)

因子是一个一个加进实盘的,实盘日志标题却一直写同一个版本号。对账必须按 era 切信号表, 否则会把"评分函数不同"误判成 bug 或数据延迟。

阶段日期实盘评分组成总分上限
Phase 14/1 – 4/19核心 6 因子≤7
Phase 24/20+ NBP3D 影子(不进 total)≤7
Phase 34/21 – 4/226 核 + NBP3D 进 total(7 因子)≤8
Phase 44/23+ RET5D 影子≤8
Phase 54/24 起7 因子 + RET5D 进 total(8 因子完整版)≤9

2026 年的 era 同理,而且三个账户各不相同:4061 从 6/4 起 10f; 4001 是 8f→6/18、10f→6/22、11f→7/7;4081 在 4/28–7/5 停跑、7/6 复跑即 10f。 所有锚点记在 era_config_zlj.json

shadow 期

不确定的因子先走影子模式:照常打分、记录、但不进 total。 信心 ~0.65 的改动(如 walk-forward day2-gate)建议 shadow ≥1 个季度再谈上线; 杠杆类改动(如 up-lever 规则)建议 shadow 6–8 周并从单一账户起步。

3. 组合层:分数怎么变成一笔交易

  1. 准入(三个条件同时满足)score_raw ≥ 6 otm_admission_ok(OTM ∈ [5,50]) 不是 ETF_ETN。 同一时点排序按 score_raw 降序——高分票优先抢稀缺资金。
  2. 动态基础仓位base_pct = max(2%, target_util / 过去 10 个交易日的中位合格信号数)。 正常 target_util = 0.60 × leverage,guard 触发时 0.30不乘杠杆)。
  3. 分数权重score 6 → ×1.00,7 → ×1.10,8+ → ×1.20。
  4. 级联约束(任一不过就跳过这笔)同票同日去重 → gross cap(权益 × 有效杠杆 − 已开敞口) → 行业 cap(正常 50%,guard 时 25%,"Unknown" 行业豁免)→ 单票 cap 20% → 单笔上限 15% → 最小 $500。 中间任何一层不够都允许部分成交
  5. SPY MA20 guard今日 SPY 开盘 < 昨日 MA20 → 杠杆降到 0.5、行业上限收窄到 25%。

摩擦:滑点 5bp、佣金 $0.005/股(最低 $1),PnL 已扣双边佣金。权益只认已实现 PnL。

4. 出场规则

出场在 Stage 1-2 就烤进 base_xy,回测只是回放,不做决策。

  • 止损 −15%,gap-aware(开盘价已破线就按开盘价成交),优先于 max-hold
  • max hold = 入场起 2 × 86,400 秒挂钟时间,但只能落在真实存在的分钟 bar 上。 因为 bar 只在交易时段存在,周四 / 周五入场的 48 小时点落在周末缺口里, 第一根合格 bar 就是周一 09:30monday_open_max_hold=True 让它取该 bar 的 开盘价而不是收盘价。这就是所谓"周一卖出"。周中入场则取 48 小时后第一根 bar 的收盘价
  • 止盈生产关闭(TP disabled)。
为什么"周一开盘卖"是对的

A(周一开盘,2 自然日)vs C(严格 2 个交易日收盘)对照实验: A 的 CAGR 105% / MaxDD −10.4% / Calmar 8.89;C 的 107% / −17.9% / 6.21。 A 的优势是风险(MaxDD 砍半),不是收益;单票集中度两者一样,没有过拟合到某只票。 alpha 主要来自周四 → 周一 09:30 的周末跳空。

执行层也验过:周一 / 跨假期的 max_hold 卖出走 MKT + tif=OPG(09:25–28 预挂竞价单)。 历史 25 个 OPG 批次 / 190 票×日 / $17.3M 名义:相对 9:28 盘前 mark 中位蒸发 −4.8bp、 累计约 $0;参与率无剂量反应(没有自我砸价);卖后无反弹签名。 开盘竞价即卖没有执行损耗。"周一晚 5 分钟卖"反而更差(Calmar −11%)。

5. 实盘 vs 回测逐笔对账

第一条纪律

PnL 一律用 trades 表,不要用 positions 表。 positions.quantitypnl 在分批成交时只记末段—— CALX 真亏 $22k 只记了 $1.6k;WOLF 实买 10,602 股只记 702 股。 大单($250k 级)必然分批(3–6 分钟一段,最多 12 段)。逐笔对照 live 数量必须用 trades 的 BUY 累计。

其它对账要点:SELL → lot 分配优先用 position_id,7 天 FIFO 兜底, 别用朴素 FIFO;close_reason 里的 sync_correction / sync_rescue 是券商同步纠错,要排除(已结案:纯记录问题,零经济影响,病根是 trades 写入丢失); 一致性核对实盘侧直接 ssh 上生产机看日志,RDS positions 不可信。

典型对账产出(全窗口 4/27–8/11,三账户,链乘):live 全面 ≥ bt 是执行层现象 (同信号 sizing 更大 + 人工收缩),不是 alpha;信号层价差约 10bp、平仓匹配 93–99.5%、 vintage 漂移三组全 0

6. 月度因子健康仪表盘

python factor_health_dashboard.py \
    --signals exp_result/scored_v9_10f_asof0810/signal_table.parquet \
    [--factors iv stock_price otm asset_type rule_hit time_slot ret_5d_roll_deep rv3d_band_15_30 dvol_10d_low_pen] \
    [--with-ablation] [--history exp_result/factor_health_history.json]

三层证据,各自回答不同问题:

算什么怎么读
A · 逐年去聚类 rank-IC (ticker, signal_date)y_2dmedian 去聚类,剔 ETF_ETN, 逐因子逐年算 rank-IC;基线用最近 2 个先前年(不用全历史,避免早年异常高值把"早就低位"误判成"新漂移") 裁决按角色分流:排序腿失 grip → 🔴 复检多窗 + 考虑 rolling 刷新(勿单年定案); 风控腿 → 🟡 多为假警报,勿据 IC 动手;门型腿 → ⚪ 只看 ablation
B · RF canary 用随机森林拟合同一批因子分列,比较 RF 的 OOS IC 与整张评分卡的 IC;再做逐因子 OOS 置换重要性 RF_IC / 卡_IC ≥ 1.5 → 🟡 权重陈旧。 注意:这是重标定复检信号,不是"上 RF"——RF 整体替换评分卡已被决定性否决(Calmar 7.97→0.74)。 置换重要性掉到 ~0 的因子 = 模型也不再倚重它
C · Ablation 承重性 逐个因子把它的分数拔掉,看组合 Calmar / MaxDD 掉多少 ΔCalmar < −2 → 🔴 强承重,碰不得;< −0.3 → 🟡 有贡献;其余 → ⚪ 弱 / 可议。 这是风控腿与门型腿的真判据

每次跑完追加进 factor_health_history.json,可以对比 IC 趋势。 建议再加一项监控:逐年"被踢单质量"(某门型因子踢掉的信号 median y_2d vs 当年全体)——见 §12 的 atr 案例,那是漂移前兆。

7. 被否因子的重验

只重验 B(共线残差层)C(回测容量层) 两类—— 基线换了,共线对手和容量对手才会变。A 类(特征层无信号)与基线无关,不值得重跑。 重验必须用 harness 先在旧基线上复现已知数字(atr_reexam_job/harness.py 就是先用 USAGE 的六格数字精确复现,排除口径假象),再上新基线。

§12后来才写下的硬规则 + 已核实的口径不一致

两条方法论硬规则

规则一 · 残差网格同向率必须报加权版

残差网格同向率必须报样本量加权版n ≥ 8 主力格子版

来源:rvol0 早退规则的未加权同向率是 83%,看起来过线; 但只看 n ≥ 8 的主力格子是 3/5 = 60%,样本量加权是 65%。 而它的符号检验 p = 0.738、Wilcoxon p = 0.766、median 的 bootstrap 95% CI [−0.697, +0.554] 含 0n=1~2 的小格子把一个 p=0.74 的规则抬成了"83% 通过"。

规则二 · 全周期 MaxDD 在多年谷深接近时是脆弱度量

压平当前的最深谷,会让次深谷浮上来,使一个逐年 3/4 改善的因子在全周期 Calmar 上显示为负。

来源:atr_low_pen_roll 在去 nbp3d 的新基线上,三档杠杆的 ΔCalmar 从 +1.20 / +1.35 / +1.16 整段翻成 −1.23 / −0.18 / −0.54。 但美元拆解显示因子本身完全健康:common 占总增益 85%(+$3.378M)、 逐年全正、top-5 集中度只有 37%、踢出的 46 笔 median y_2d −0.479% / 胜率 43.5%(全体 55.8%)。 问题 100% 出在 2026 单年 MaxDD +2.03pp,叠加"全局最深谷从 2025 换位到 2026"的度量脆弱性。

结论:不建议拔 atr(拔了丢 $3.96M 和 2023/24/25 三年的 Calmar), 应该在 sizing 层处理 2026 的集中度反噬。因子体检必须同时看逐年 Calmar/MDD + 美元拆解, 不能只看全周期 Calmar 一个数。

附带发现 · 1/N 定仓让门型因子被迫兼任 sizing 决策

base_pct = target_util / 过去 10 日中位合格信号数 × score_weight, 而这个"合格信号数"是在因子减分之后score_raw ≥ 6 上统计的。 于是任何 gating 因子踢掉信号 → 分母变小 → 其余每笔仓位机械抬高

结构性补丁(无调参):准入用 score_raw(含惩罚),sizing 的分母用 sizing_score_raw(不含 gating 惩罚)。三档杠杆方向一致(ΔCalmar +0.28 / +0.21 / +0.21), 几乎不花收益。它对所有未来的门型因子都生效,不只是 atr。

反过来,不要util / lookback / max_position_pct 调参来救 Calmar:util 单调(降 util = 降杠杆,是风险偏好旋钮不是 alpha); lookback 无 plateau(10→11.02、20→9.97、30→11.57 非单调,20 是尖峰 = 路径噪声); 收紧 max_position_pct 直接砍掉一半终值。

已核实的口径不一致(读代码确认,待团队决定是否开工单)

#不一致证据影响
1atr_low_pen_roll 的 warmup 行没有被删 它的 scoring_table 返回非 None,因此不在 build_factors.pyrolling_cols 里,warmup NaN 被 fillna(0) 静默中性化——但它自己的 scoring_table 里写着 "warmup": "NaN(删行)"。 旁证:asof0810 的 10f 与 11f 行数都是 68,923 11f 的 warmup 期分数被系统性中性化,与其它 rolling 因子的语义不一致
2factor_registry.py show atr_low_pen_roll 会崩 同一张表混入了字符串值 "NaN(删行)"factor_registry.pypoints > 0 会抛 TypeError查因子详情的常用命令对这个 key 不可用
3实盘 ≠ 回测文档 实盘 2026-08-17 已物理拔除 nbp3d(11f→10f、10f→9f,有 ablation 支撑,主动下线非故障), 但 BIGOPEN_V9_USAGE.md 的 10f / 11f 定义里仍列 nbp3d_low 对账必须用 *_nonbp_* 信号表;否则准入信号会差约 19–20%
4--min-score 只在 6 上校准过 6/7/8+ 的仓位权重档是硬编码的,引擎自己会打 warning改 min_score 会得到未定义的 sizing 行为
5factors/atr_pct_static.py 默认阈值是 6.5 其 docstring 引用的实证 Q5 边界是 6.49轻微,且该因子已被否决
6SPY guard 数据缺失时 fail-open 拉不到 bars 或 SMA 时返回空 map,整个 risk-off 层静默消失 已经造成过 121% vs 138% 的不可复现(见 §01)。应改 fail-loud

§13附录:命令速查与源文件索引

命令速查

# 0. 每次开 shell 必做(否则 SPY guard 静默 fail-open)
set -a; source .env; set +a

# 1. 看有哪些因子 / 看某个因子的打分表
python3 factor_registry.py list
python3 factor_registry.py show dvol_10d_low_pen

# 2. Stage 2 打分(10f 生产基线)
python3 build_factors.py \
  --base exp_result/bigopen_v9_base_asof0810/bigopen_v9_stage1_base_xy.parquet \
  --factors iv stock_price otm asset_type rule_hit time_slot nbp3d_low ret_5d_roll_deep rv3d_band_15_30 dvol_10d_low_pen \
  --out-dir exp_result/scored_v9_10f_asof0810

# 3. Stage 3 回测(唯一引擎 run_backtest_v9.py;时间窗必须显式传)
python3 run_backtest_v9.py \
  --signals exp_result/scored_v9_10f_asof0810/signal_table.parquet \
  --factors iv stock_price otm asset_type rule_hit time_slot nbp3d_low ret_5d_roll_deep rv3d_band_15_30 dvol_10d_low_pen \
  --start-date 2023-04-20 --end-date 2026-08-10 \
  --capital 5000000 --leverage-ratio 1.0 --min-score 6 \
  --out-dir exp_result/bt_v9_1x_10f_asof0810

# 4. 缓存体检与修复
python3 utils/cache_repair_zlj.py scan-truncated              # 默认 dry-run
python3 utils/cache_repair_zlj.py purge-year 2026 --apply     # vintage 漂移的决定性一招
python3 utils/cache_repair_zlj.py dedupe-daily-bars
python3 claude_explore/daily_bars_prewarm_zlj.py --base <base.parquet>

# 5. 月度因子健康体检
python3 factor_health_dashboard.py \
  --signals exp_result/scored_v9_10f_asof0810/signal_table.parquet --with-ablation

全链路刷新已收敛成 /refresh-backtest.claude/commands/refresh-backtest.md): 上传新 CSV 后一句话触发,自动推导 asof、后台连跑、自动验收与自愈,默认范围 10f + 11f 的 1x。

验收三件套(每次刷新后必做)

  1. 复跑一致:同 base + 同 scored 表重跑,portfolio_report.json 三文件应逐位一致。 不一致说明差异来自缓存内容,不是引擎随机性。
  2. guard 在位:检查 guard_active 的笔数不为 0(asof0810/0824 都是 505 笔)。
  3. NaN 本底正常:dvol 约 6.9% / rv3d 约 4.5%;base ok 率约 82%。

源文件索引

用途路径
主流程(本手册 §04 §05 的源).claude/commands/explore-factor.md
9 关清单原文claude_explore/onboarding_report_zlj/index.html §06–§09(仅 HTML,无 md 源)
通用研究工作流(§03 的源)skills/youthquant-strategy-research/SKILL.md
现役评分卡 + 官方数字BIGOPEN_V9_USAGE.md
因子实现factor_registry.py(62 个)· factors/_base.py · factors/rolling_ic_base.py · build_factors.py · run_backtest_v9.py · bigopen_v9.py · utils/bigopen_stage1_build.py
活模板 · 判例来源reports/ 下 29 份共 8,283 行。重点: nbp3d_factor_exploration.md(516) · ret_5d_rolling_factor_exploration.md(355) · atr_pct_rolling_factor_exploration.md(430) · uw_directional_factor_unified_evaluation.md(262) · realized_vol_3d_band_factor_exploration.md(1075) · dvol_10d_low_pen_factor_exploration.md(655) · atr_pct_low_pen_exploration.md(349) · bear_pen_ex_count_roll_factor_exploration.md(340) · uw_14d_flow_factor_re_exploration.md(189) · post_rv3d_factor_exploration_negative.md(160) · vwap_deep_factor_exploration.md(296) · rvol_tod_gate_exploration.md(67)
只在分支上的三份(工作树里没有) git show b2e5f5e:atr_reexam_job/README.md(死法四分类 + 两条硬规则)
git show 58c5ece:factor_health_dashboard.py(角色三分法 + 三层证据)
git show origin/dev_lrh_v2:reports/industry_index_factor_exploration.md
对抗式验证范例claude_explore/dvol_verify_zlj/dvol_PR_verification_report_zlj.md
PR 模板14_flow_factore_pr_des.md
叙事版(给新人 / 投资人)claude_explore/onboarding_report_zlj/index.html
给新人的一句话

拿一个候选因子,跑 /explore-factor,对照 §05 的 9 关和 §05b 的 10 级逐项过——这就是我们筛因子的全部标准。 过不了就写进 §09 的坟场,注明是哪一关塌的。 留档一个被否的因子,和上线一个因子同样有价值。

因子筛选逻辑 · 团队手册 · Bigopen V9 美股策略
素材来源:/explore-factor 命令 · onboarding 报告 §06–§09 · reports/ 29 份探索报告 · 58 条项目 memory · factor_registry 与回测引擎源码 · git 提交记录
整理于 2026-09-14 · 所有数字均标注 asof,缓存 vintage 会漂,引用前请确认口径