Bigopen V9 · 美股期权流股票策略
我们怎么筛因子
一套已经成型的方法论:四阶段漏斗、9 关判据电池、10 级判据阶梯、9 维健康度、 对抗式证伪,以及上线之后的对账与体检。每一条判据背后都有一个被它毙掉或救下的真实因子。
§00怎么用这份文档
这份手册是操作手册,回答"怎么做"。团队的 onboarding 报告
(onboarding_report_zlj/)是叙事报告,回答"为什么"。两份配套看,不重复。
我想知道某个想法是不是试过了
查 §09 因子坟场。里面按"哪一关塌了"归类,并标明换基线之后 还值不值得重验——A 类死法不值得,B/C 类值得。
我做月度体检 / 怀疑某条腿失效
看 §10 因子角色三分法 决定该用什么证据读它,再跑
§11 的 factor_health_dashboard.py。
一个因子的完整生命周期
[事实] = 有文件、回测产物或代码支撑,可以倒推验证。 [经验] = 团队判断,可以挑战。所有数字都标了 asof—— 缓存 vintage 会漂,不标 asof 的数字在这份文档里一律不写。
§01前置:不干净的数据不配谈因子
这一节不是背景知识,是开工前的六条必检。历史上最贵的几次翻车 都不是因子设计错了,是环境不干净——整轮 V8 探索(83 组矩阵)的"+pp"几乎全是数据 bug 造出来的人工产物。
1. 时间窗:永远显式传 start / end
引擎默认 --start-date 2023-03-10、--end-date 2025-12-31,
两个默认值都会坑人。
| 参数 | 引擎默认 | 官方口径 | 不改会怎样 |
|---|---|---|---|
--start-date | 2023-03-10 | 2023-04-20 | 0310→0420 属于 rolling 因子 warmup 窗(ret_5d 90d / nbp3d 90d)。同样的终值用更长窗算 CAGR, 凭空少 4–7pp,还多出 9 笔早期低质量交易。0420 也是 V9 实盘启动日。 |
--end-date | 2025-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<300、Dropped ~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.json 的 price_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_ratio | 30 session | 100% | dvol 门型因子不罚分 |
realized_vol_3d | 4 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:97 | searchsorted 永远选当日最后一根 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
比较因子时不要跨会话比绝对数字。做法:钉死同一张 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_2d 与 entry_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) | CAGR | MaxDD | Sharpe | Calmar | Trades | 终值 |
|---|---|---|---|---|---|---|
| 8 核 | 105.35% | 13.65% | 6.04 | 7.72 | 1,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。
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 表,可跨档直接比)
| 版本 | CAGR | MaxDD | Calmar | Sharpe | Trades | Win% | PF | 终值 |
|---|---|---|---|---|---|---|---|---|
| 10f 1x 生产基线 | 114.99% | 10.93% | 10.52 | 6.72 | 1,900 | 55.5% | 1.57 | $62.86M |
| 11f +atr 1x 候选 | 119.17% | 10.17% | 11.72 | 6.86 | 1,878 | 55.5% | 1.58 | $67.00M |
| 10f 1.5x | 190.56% | 18.06% | 10.55 | 6.72 | 1,879 | 55.5% | 1.51 | $170.22M |
| 11f +atr 1.5x | 199.43% | 16.76% | 11.90 | 6.78 | 1,852 | 55.6% | 1.52 | $188.03M |
| 10f 2x | 280.79% | 24.24% | 11.58 | 6.66 | 1,860 | 55.6% | 1.47 | $416.36M |
| 11f +atr 2x | 292.97% | 23.00% | 12.74 | 6.69 | 1,833 | 55.5% | 1.46 | $462.07M |
score_raw 实测区间:10f [−9, +11] / 11f [−10, +11](68,923 行)。准入门槛 min_score = 6。
数字为什么会漂:一定要标 asof
| asof | 10f CAGR | MaxDD | 状态与原因 |
|---|---|---|---|
| 0623 / r0714 | 122.15% | 10.93% | prewarm 后的对齐态 |
| 0722 | 105.31% | 11.46% | 非对齐 daily_bars 混合 vintage:V10 backfill + prewarm 改了 277 只票的 ret_5d 历史值 → ret_5d_score_low 在 495 个信号(0.79%)上翻转 → min-score=6 边界准入变化 → 组合路径混沌发散 |
| 0810 | 114.93% | 10.93% | 已对齐 purge-year 2026 + daily_bars 整删重拉 = 单一 fresh vintage |
| 0824 | 114.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 / live | Phase 4 九维 ≥7 绿 + 关键项全绿 → PR → shadow → 分阶段 era 上线 |
- 先固定前三层,再评估第四层。组合收益在前三层没定死之前没有意义。
signal_universe或single_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,做四件事:
- 过滤:可交易资产 + OTM 5%–50%
- 分桶(quintile):每桶 mean(
y_2d)、N、t-stat - ANOVA F 检验
- Spearman 相关性矩阵:对
score_raw、nbp3d_score_low、ret_5d_score_low、stock_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 核基线 | 新因子达标线 |
|---|---|---|
| CAGR | 105.35% | ≥ +3pp(≥ 108.35%) |
| MaxDD | 13.65% | ≤ 14.65%(+1pp 上限) |
| Sharpe | 6.04 | ≥ 5.5 |
| Calmar | 7.72 | ≥ 6.5 |
| 2026(1–4 月) | +15.51% | 不退化(≥ 12%) |
回测赢了照样可能被毙。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。
完整落地步骤
- 数据探索写
analyze_*_zljcc.py做 Phase 1 检验(做完即弃的小脚本) - Builder放
claude_explore/utils_zlj/,必须用 codex global dedup 模式(见 §05 关 3) - Factor 类放
claude_explore/factors_zlj/,继承同款FactorBase/RollingIcFactor,签名照抄主干factors/ - 注册在
claude_explore/factor_registry_zlj.py注入主 registry,主干factor_registry.py不动 - Build + 回测用 wrapper(自动注入 zlj 因子)
- Phase 4 审视跑
critical_analysis_v2_zlj.py - 报告写成
reports/<topic>_exploration.md风格的活模板(见 §09) - 若全绿写 PR 描述给用户确认,由用户提 PR 迁入主干
探索代码的结构必须和主干一致——因为实盘
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 方向 | 总 PnL | MaxDD | Calmar | 结论 |
|---|---|---|---|---|
| baseline | $19.58M | 10.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
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_low 和 ret_5d_roll_deep 都加了这个限制。
关 5搜索空间要窄;看 percentile,不看 ratio 最干净的过拟合物证
判据:自适应阈值的候选越多越容易过拟合。 诊断的正确对象不是阈值数值(ratio),而是算法每天选了哪个候选(percentile)。
经典案例:ret_5d 的 Deep vs Wide
| 口径 | 候选数 | pctile 中位 | pctile std | 阈值中位 | +1 覆盖率 | CAGR | Calmar |
|---|---|---|---|---|---|---|---|
| Deep p5–25 | 5 | 10 | 7.18 | −8.18% | 12.6% | 101.55% | 7.44 |
| Wide p10–90 | 17 | 45 | 36.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。
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 主导了"。
| 因子 | 全样本 diff | t | drop-2025 diff | drop-2025 t | trim-5 t | 判定 |
|---|---|---|---|---|---|---|
bear_C_tk | +0.289 | 2.80 | 0.058 | 1.01 | 2.12 | 毙 塌 5 倍、失去显著性 = 2025 单年异象 |
bull_B_tk | +0.131 | — | −0.019 | — | — | 毙 剔 2025 直接转负 |
bear_A_tk | +0.117 | 4.35 | +0.152 | 3.58 | 3.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 噪声子集"。
加 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),解释是"降噪"。 作者随后提出三条威胁并逐条验证:
- Threat 1 — 排序反了。阈值周变化 std 的排序与收益不符: 2024 的 std 0.507 是最稳定的一年,却正是 lag 赢得最多的一年; 2023(std 0.862,按"降噪"假说应当最受益)反而亏 $31k。
- Threat 2 — 57% 来自一笔。2024 的 +$347k 里,
SMCI 2024-02-20 单笔独占 +$179k——它的 ratio 恰好落在
threshold[wk]与threshold[wk−1]之间。 这是边界运气,不是机制性优势。 - Threat 3 — 2026 三个版本完全一致(Calmar / Sharpe 2.20 / 0.77 逐位相同), 因为当年信号密度上升到 93 只票/天。如果机制为真,不该在高密度下消失。
结论:+6pp 高度依赖单一事件,不具备统计稳健性 → 恢复 no_lag。
沉淀的三条方法:① 分年均值的改善可能来自单笔暴击; ② 评估 A/B 必须拆解仓位复利 × 独有交易 × 对手独有交易; ③ 阈值时间序列的抖动统计,是检验"降噪"类假说最直接的手段。
§05b判据阶梯 · 10 级
9 关是原则,这 10 级是硬线——每一条都是被某个具体因子逼出来的, 按它们出现的时间排。复查别人的 PR 时对着这张表走最快。
| # | 硬线 | 数值判据 | 是谁逼出来的 |
|---|---|---|---|
| 1 | Step 1 原始分桶 | ANOVA p < 0.05;最优桶 t;四年同号 | 入门线。注意:raw 四年同号只是必要条件 |
| 2 | Step 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 反向) |
| 4 | rolling 对照反过拟合 | 固定阈值因子上线前必须出 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) |
| 5 | percentile 分布诊断 | 看算法选了哪个候选,不看阈值数值。std、双峰、大跳率、逐年中位 | ret_5d Wide(见 关 5) |
| 6 | 阈值 plateau + 整数纪律 | 两位小数的阈值一律怀疑数据窥探;取 plateau 中段的整数,刻意避开 in-sample 峰 | dvol 0.76 → 0.80;atr_low_pen 2.31 → 2.5。见 §07 |
| 7 | added / 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,输出九项指标)。
| # | 维度 | 怎么算 | 健康阈值 |
|---|---|---|---|
| 1 | PnL 集中度 关键 | top-1 / top-5 trade 占 added 总 PnL 的比例 | top-5 < 80% |
| 2 | Median 回报 | added trades 的 median(realized_return_pct) | ≥ baseline median 的 80% |
| 3 | 真 promote vs existing 关键 | 真 promote(加因子后 6 分、原 8 核 5 分)的 mean,对比 base8 里原本就 6 分的 existing | ratio ≥ 0.9 |
| 4 | 季度稳定性 关键 | 季度 added PnL 为净正的比例 | > 70% |
| 5 | 2026 trade-level 关键 | 2026 added 的 median 与胜率 | median ≥ baseline 且 win ≥ baseline |
| 6 | 阈值平台 | 上下相邻 2 个阈值是否都同向改善 | 平台稳定 |
| 7 | Trigger rate | +1 占比 | 8% – 18% |
| 8 | Sizing 杠杆贡献 关键 | 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) 分组 bug | flow 漏看 → 因子值失真。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 contamination | build 时混入 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 杠杆"假 alpha | score+1 让其他高分票仓位放大 | Phase 4 #8:拆 added / boosted / same |
| 2026 退化 | 整体 +pp 但 2026 反降 | Phase 4 #5 必查 2026 trade-level |
§07阈值与 rolling 校准(反数据窥探)
整数纪律:两位小数的阈值一律怀疑
初版 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.70 | 0.72 | 0.74 | 0.76 | 0.78 | 0.80 | 0.82 | 0.85 |
|---|---|---|---|---|---|---|---|---|
| Calmar(asof0601) | 11.31 | 11.50 | 11.47 | 11.56 | 11.60 | 11.53 | 11.74 | 11.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 因子的两段式构造
- Stage 1 — 每日 IC 搜阈值训练窗
[D−97, D−7)。 那个 7 天 gap 是关键:2 日标签需要时间 settle,不留 gap 就是前瞻。 训练池只用STOCK/ADR,min_samples=50,候选分位必须窄。 - Stage 1.5 — 桥接成 ratio
rv_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) → +1 | dvol < 0.80 → −1 |
| 受影响比例 | ~60% | ~20% |
| Trades | +674(涌入) | −179(拦下) |
| Calmar(asof0601) | 7.09(−29%) | 11.53(+14.8%) |
| MaxDD | 15.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 plateau | 0.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 年改善"是真广泛吗? | 部分 符号广,增益集中 2025 | 0.78 |
| Methodology | 方法论足以上线吗? | 部分 ship with caveats | 0.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)。
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 · 搁置 | 证据不足以判死,也不足以上线 | 看新证据 |
在"去 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_oi | score_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.18pp | A |
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_lag | A |
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 全部变差约 1pp | C |
gamma_abs / high_gamma | t=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_static | Raw p=0.03,四年同号,重验时是唯一接近显著者(p=0.088) | 阶梯 3 + 回测 与 otm_score ρ=+0.334,A 档残差 t=−0.08、网格 65%;上卡回测 Calmar 10.96→7.81 | B |
opex_dist_days | raw ANOVA F=36.6,p=1.23e-30,形状干净单调 | 阶梯 2 A 档残差 t=1.13,逐年符号乱翻。raw ANOVA 显著 ≠ A 档 booster 有效 | B |
dist_52w_high | — | 阶梯 3 被 rule_hit 吸收(BigOpen 的 rule 里本来就含 breakout / new high 类) | B |
rvol_tod gate | Calmar 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.98 | C |
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 是全语料里唯一一个所有稳健性测试都通过的候选:
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 是唯一残差网格拿到 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。
booster 当年死于容量挤占——新增 492 笔(+1.08%/笔)挤掉 114 笔更好的(+2.07%/笔)。 两年后的假设是:既然死因是容量,那么换干净基线 + 叠加 rvol0 早退(提前一天释放资金)应当复活。
| 基线 | base | +atr_pct 静态高尾 | +atr_pct_roll_high | +dte_static |
|---|---|---|---|---|
| new_10f | 10.96 | 8.40 | 6.11 | 7.81 |
| new_10f + rvol0@0.7 | 11.70 | 8.92 | 6.37 | 8.29 |
| new_9f | 12.19 | 9.02 | 6.45 | 8.46 |
| new_9f + rvol0@0.7 | 11.99 | 9.29 | 6.60 | 8.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_type | STOCK / ADR → +1 ETF_ETN → −1 |
个股期权流带 idiosyncratic 信息;ETF 大单多是对冲或宏观仓位 |
rule_hit | rule 非空 → +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,是真"删坏单"不是单纯降仓 |
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% reshuffledvol_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 |
写因子的代码契约
| 成员 | 类型 | 契约 |
|---|---|---|
name | abstract property → str | 短 key,如 'iv' |
description | abstract property → str | 一行中文描述 |
column_name | abstract property → str | 输出列名,如 'iv_score' |
scoring_table | property → 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,由调用方决定怎么处理。
rolling 校准未就绪 → 输出 NaN → build_factors.py 把整行删掉。
这样 score_raw 才不会在校准窗口内被 fillna(0) 静默"中性化",
避免早期窗口因部分覆盖被系统性 inflate / deflate。
校准已就绪、但单行特征缺失 → 因子内部应该给中性 0 分保留该行
(rv3d_rolling 已这样实现)。其它 rolling 因子目前仍把特征 NaN 一并当 NaN 丢。
这一点靠调用顺序保证,而不是运行时断言:
score_raw 在所有因子跑完之后才创建(build_factors.py),
调用 score() 的时候它在 DataFrame 里根本不存在。
全量 grep factors/*.py 确认过:score_raw 只出现在 docstring 里。
所以这是一条你必须自己遵守的纪律——写出依赖它的代码不会报错,只会在 review 时被打回。
官方因子集
| 集合 | 成员 | 理论上限 | min_score | 状态 |
|---|---|---|---|---|
8f | 6 核 + nbp3d_low + ret_5d_roll_deep | +11 | 6 | 探索基线 |
9f | 8f + rv3d_band_15_30 | +12 | 6 | 历史 |
10f | 9f + dvol_10d_low_pen | +12(实测 +11) | 6 | 生产基线 |
11f +atr | 10f + atr_low_pen_roll | +12(实测 +11) | 6 | 候选 |
12f | 11f + vwap_deep_* | — | 6 | 非生产 vwap 腿已否决,12f 只是组合测试留档 |
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 1 | 4/1 – 4/19 | 核心 6 因子 | ≤7 |
| Phase 2 | 4/20 | + NBP3D 影子(不进 total) | ≤7 |
| Phase 3 | 4/21 – 4/22 | 6 核 + NBP3D 进 total(7 因子) | ≤8 |
| Phase 4 | 4/23 | + RET5D 影子 | ≤8 |
| Phase 5 | 4/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。
不确定的因子先走影子模式:照常打分、记录、但不进 total。 信心 ~0.65 的改动(如 walk-forward day2-gate)建议 shadow ≥1 个季度再谈上线; 杠杆类改动(如 up-lever 规则)建议 shadow 6–8 周并从单一账户起步。
3. 组合层:分数怎么变成一笔交易
- 准入(三个条件同时满足)
score_raw ≥ 6且otm_admission_ok(OTM ∈ [5,50])且 不是ETF_ETN。 同一时点排序按score_raw降序——高分票优先抢稀缺资金。 - 动态基础仓位
base_pct = max(2%, target_util / 过去 10 个交易日的中位合格信号数)。 正常target_util = 0.60 × leverage,guard 触发时0.30(不乘杠杆)。 - 分数权重score 6 → ×1.00,7 → ×1.10,8+ → ×1.20。
- 级联约束(任一不过就跳过这笔)同票同日去重 → gross cap(
权益 × 有效杠杆 − 已开敞口) → 行业 cap(正常 50%,guard 时 25%,"Unknown" 行业豁免)→ 单票 cap 20% → 单笔上限 15% → 最小 $500。 中间任何一层不够都允许部分成交。 - 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:30;
monday_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.quantity 和 pnl 在分批成交时只记末段——
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_2d 的 median 去聚类,剔 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] 含 0。n=1~2 的小格子把一个 p=0.74 的规则抬成了"83% 通过"。
压平当前的最深谷,会让次深谷浮上来,使一个逐年 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 一个数。
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 直接砍掉一半终值。
已核实的口径不一致(读代码确认,待团队决定是否开工单)
| # | 不一致 | 证据 | 影响 |
|---|---|---|---|
| 1 | atr_low_pen_roll 的 warmup 行没有被删 |
它的 scoring_table 返回非 None,因此不在
build_factors.py 的 rolling_cols 里,warmup NaN 被
fillna(0) 静默中性化——但它自己的 scoring_table 里写着 "warmup": "NaN(删行)"。
旁证:asof0810 的 10f 与 11f 行数都是 68,923 |
11f 的 warmup 期分数被系统性中性化,与其它 rolling 因子的语义不一致 |
| 2 | factor_registry.py show atr_low_pen_roll 会崩 |
同一张表混入了字符串值 "NaN(删行)",factor_registry.py 的
points > 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 行为 |
| 5 | factors/atr_pct_static.py 默认阈值是 6.5 |
其 docstring 引用的实证 Q5 边界是 6.49 | 轻微,且该因子已被否决 |
| 6 | SPY 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。
验收三件套(每次刷新后必做)
- 复跑一致:同 base + 同 scored 表重跑,
portfolio_report.json三文件应逐位一致。 不一致说明差异来自缓存内容,不是引擎随机性。 - guard 在位:检查
guard_active的笔数不为 0(asof0810/0824 都是 505 笔)。 - 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 会漂,引用前请确认口径