12 — Verification(实测验证 / "科学证据")
这章不讲架构,讲用真实数据证明这工具有用。给愿意深读的人看,给质疑"LLM 委员会到底比躺平好不好"的人看,也给我自己将来 review 设计是否还成立看。
这章为什么存在
读者第一次看到"4 个 LLM 互相辩论给投资建议"会有两个反应:
- 听起来挺酷 → 想 fork 用
- 听起来像玄学 → 跑路
这章就是给第 2 类人的——把所有"我们认为有用"的主张,拉到回测数据上证伪/证实。 含正向结果也含 negative results(同等重要)。
⚠️ 污染桶标注(ADR-022,issue #179 P0-3):本章主张 1–6 的实验窗口全部落在
2024-12-31(jobs/verdict_review.py的CONTAMINATION_CUTOFF,即 LLM 训练知识 截止)之前——按 ADR-022 的分桶纪律,这属于污染桶:LLM 可能在"回忆"训练数据里见过的行情而非预测, 绝对收益/年化/Sharpe 等数字不可当业绩宣称。这些实验仍然合法的用途是 行为一致性对比(同窗口下 v0 vs v1、参数 A vs B 的相对差异)。 干净业绩证据只认 cutoff 之后的 holdout(scripts/research/full_validation.py+verdict_review干净桶)。
主张 1:v0 baseline 是个 100% HOLD 复读机
实测(2026-05-11,6 个月窗口 2024-05-15~11-27, NDQ.AX + GC=F, 33 dates × 2 资产 = 66 verdict):
| 指标 | 实测值 |
|---|---|
| HOLD 占比 | 100% (66/66) |
| 月度 verdict 多样性 | 1 种类型 / 月 |
| confidence 方差 | ≈ 0(固定 0.65±0.05) |
| 总收益 | 0%(账户全程 100% 现金) |
| vs 余额宝 | -0.12%(连躺平都跑不赢) |
| Reward signal | -0.0006(噪音水平) |
→ 主张证实:未优化的 prompt 让 LLM 退化成复读机。
主张 2:CIO prompt 加"现金机会成本"规则后 unblock
改动(commit 2b69c83):CIO prompt 加硬规则:
CONCENTRATION_PCT < 20%(仓位 < 20%,子弹 ≥ 80%):
- 不允许 HOLD
- 默认至少 ACCUMULATE,alloc = dry_powder × 5~10%
- 豁免:Macro=risk_off AND Risk=high_risk(两个 AND)v0 → v1 对比(同样 6 个月窗口):
| 指标 | v0 baseline | v1 |
|---|---|---|
| HOLD 占比 | 100% | 44% |
| BUY+ACCUMULATE | 0% | 56% |
| 总收益 | 0% | +15.55% |
| 年化 | 0% | +25.39% |
| Sharpe | 0 | 1.58 |
| Max drawdown | 0% | 9.0% |
| Reward | -0.0006 | +0.3978 |
| vs 余额宝 | -0.12% | +14.72% |
| vs 沪深 300 | 0% | +15.55% |
| vs 等权 buy-hold | 0% | -1.65% ⚠️ |
→ 主张证实:单点 prompt 改动让账户从持币 → 半年 +25%。
⚠️ Caveat:输给等权 buy-and-hold -1.65%——委员会在牛市不如躺平四资产 25% 买死。这是当前 v1 prompt 的局限,留作下次实验。
主张 3:v1 prompt 的方向准确率不高,但"不犯大错"
实测 v1 verdict vs 7 天后实际涨跌(66 样本,scripts/archive/build_dspy_trainset.py):
| 命中分档 | 比例 |
|---|---|
| 方向对(+1) | 47.0% (31/66) |
| 中性(0) | 39.4% (26/66) |
| 方向错(-1) | 13.6% (9/66) |
| 策略 hit rate | +33.3% |
按 verdict 类型拆:
ACCUMULATE: 37 个,仅 38% 方向对(LLM 不擅 7d timing)HOLD: 29 个,59% 方向对(横盘判定较准)
→ 主张证实:LLM 不善 timing 但善避大错。半年 +25% 收益 = 反向错率仅 13.6% × 累积复利的结果,不是高命中率。
主张 4:Optuna 30-trial 找到了天花板
实测(30 trial × 5 维参数空间,~60min wall clock 3-worker 并行):
| 排名 | Reward | 年化 | Sharpe | DD% |
|---|---|---|---|---|
| 🥇 #20 | 0.4223 | 34.4% | 1.37 | 15.4 |
| 🥈 #4 | 0.4223 | 34.4% | 1.37 | 15.4 |
| 🥉 #9 | 0.4205 | 29.2% | 1.56 | 10.9 |
| 🔚 #25 | 0.2621 | 20.6% | 1.28 | 10.1 |
reward 统计:mean=0.395, median=0.417, stdev=0.037(仅 1.5%)。
→ 主张证实:reward 0.42 是当前 prompt 架构的天花板,bayesian search converged, hyperparameter 调不动 prompt 内容决定的上限。
意外发现:5 个参数里 4 个是 placebo(max_rounds=1 vs 3 reward 相同; alloc_aggressiveness 0.06 vs 0.25 reward 几乎相同;regime 阈值变化也不解释)。 只有 alloc_aggressiveness 真有效(控制 SKIP 率,影响 1-2pp reward)。
主张 5:Optuna 找到的参数不 overfit(hold-out 验证)
实测:train 集 (2024-05-13 ~ 11-15) best 参数,在 hold-out (2024-11-18 ~ 12-31, 6 周) 跑一次:
| 指标 | Train (26w) | Hold-out (6w) |
|---|---|---|
| Reward | 0.4223 | 0.2880 |
| 年化 | +29.2% | +17.4% |
| Sharpe | 1.56 | 1.59 |
| Max DD | 10.9% | 2.6% |
| vs 余额宝 | +14.0% | +1.9% |
| vs 沪深 300 | +14.7% | +2.1% |
| vs 等权 | +2.7% ✓ | -1.9% ⚠️ |
判定标准:holdout_reward > 0.5 × train_reward → 0.288 > 0.211 ✅
→ 主张证实:参数泛化 OK,不是 in-sample overfit。Hold-out 期 NDQ 震荡所以策略 变保守(BUY 2 次 vs train 12 次),但风险控制反而更好(Max DD 缩水 75%)。
⚠️ 这是参数 holdout(防参数过拟合),不是业绩 holdout(防记忆穿越)—— 窗口 2024-11-18 ~ 12-31 整段 ≤
CONTAMINATION_CUTOFF,落在污染桶内。 此表只支持"参数换相邻窗口仍稳"的主张,不支持任何业绩宣称(ADR-022 §5: 两个正交的 holdout 谁也不能替谁)。
主张 6:DSPy 自动调 prompt 比人手写更准
实测(scripts/archive/rl_optimize_prompts.py):
| Run | Train | Dev | Candidates | Bootstrap | Baseline | Optimized | Δ |
|---|---|---|---|---|---|---|---|
| Smoke | 10 | 5 | 3 | 2 | 0.600 | 0.700 | +10pp |
| Full | 46 | 20 | 10 | 4 | 0.725 | 0.825 | +10pp |
两次跑都见 +10pp 稳定改善(dev set 单步 verdict 预测准确率 60→70% / 72→83%)。
→ 主张证实:DSPy 自动选 few-shot examples 比人手写 prompt 更准。
⚠️ Caveat:dev set +10pp 不直接等于回测 reward 突破 0.42。需要手工把 experiments/dspy_optimized_v1_full.json 里的 10 个 best demos 注入 capabilities/committee/cio.py 重跑 walk-forward 才知道。这一步没做。
主张 7:WealthContextOfficer 解决"低现金=高风险"误判
已退役(历史记录):WealthContextOfficer 于 2026-07-08(
a425e9d)删除,src/零引用;下文实测与路径均为当时快照。Risk Officer 现统一按 portfolio cash 判流动性。
问题(2026-05-12 用户实盘报告):
"agent 一直说我现金太少,让我卖掉换现金。但实际我的现金 backup 还有家族, 这些只是零花钱。"
根因:Risk Officer prompt 里 DRY_POWDER_CNY < 1000 → high_risk, 没有"off-portfolio 兜底"概念。
实测 E2E(同样 portfolio 上下文:cash ¥500 + 单一 ETF 重仓 99.9%):
| 维度 | A (无 backup) | B (大额家族 backup) |
|---|---|---|
| Risk SIGNAL | high_risk | concerned |
| Risk STRENGTH | 10 | 8 |
| 建议动作 | 立即减仓 60% | 加仓上限 ≤¥50(10% 子弹) |
| ONE_LINER | "可用子弹仅¥500 无补仓能力" | "虽家族兜底消除流动性风险,但集中度仍高" |
关键语义(commit 待 / capabilities/committee/wealth_context_officer.py):
INVESTABLE_CASH_CNY = portfolio cash(永远不加 backup)BACKUP_BUFFER_CNY = off-portfolio 应急金(仅风险兜底,不可投资)- 大额家族兜底只让 SIGNAL 从 high_risk → concerned,不让 CIO 喊"BUY ¥1M"
→ 主张证实:用户上下文缺失是真问题,加一个独立角色能精准修复——LLM 推理逻辑 没错,错的是它看到的前提条件。
Negative results(同等重要)
记下来防"事后选择性叙事"。(数字同样来自污染桶窗口——见页首 ADR-022 标注; negative result 在污染桶里只会更乐观不会更悲观,所以"跑不赢"的结论方向可信):
委员会跑不赢等权 buy-and-hold(v1 -1.65%,hold-out -1.9%) → 4 角色辩论在牛市单边涨时反而比"什么都不做的四资产 25% 买死"差 → 暗示我们应该有"识别牛市无脑买死即可"的能力
多轮辩论是 placebo(max_rounds=1 vs 3 reward 几乎相同) → 之前花精力做 cross-challenge 机制可能 over-engineering → 或者 Round 2 prompt 让 agent 倾向"维持原判"
alloc_aggressiveness 0.06 vs 0.25 reward 几乎相同 → 这个 hyperparameter 主要影响"SKIP 率",不影响策略本质 → 真正的 alpha 来自 LLM 在哪个时点喊 ACCUMULATE,不来自每次买多少
ACCUMULATE 准确率仅 38% → LLM 不擅长 7 天 timing → 长期累积仍 +25% 是因为"不犯反向错"(仅 13.6% 错),不是"会择时"
TradingAgents 式分析师 subagent 无方向增量(2026-06-11,test_ta 实验, 预注册 Gate 0/3 过线,n=244/分析师) → fundamental 50.3%(真 COT 数据也 ≈ 抛硬币)、news 57.4%、sentiment 64.7% ——全部低于"无脑猜多数方向"基率 ~73% → sentiment 赢了自己输入数据的机械映射(57.5%):LLM 的价值在解读不在 方向投票,支持"确定性事实块 + CIO 否决权"路线 → 2026-06-12 复测加固(wiki 16 §5b):2022 熊市窗口 × 2 厂商 3 模型 × ensemble 三种组合,仅 fundamental-2022-flash 单格过线(跨模型 1/3, 孤证不立);"更强模型"假设反向证伪(reasoner 在 2024 窗口更差) → 全文 16-ta-analysts-experiment.md, 决策 adr/009
黄金 VIX 防御腿"语义错误"假设——验收结论被一次窗口口径 bug 翻转(2026-06-13 漂移审计) (
scripts/research/validate_gold_defense.py) → 假设:黄金双相(流动性挤兑先跌、危机买盘接力),高 VIX 拦黄金买入是 股票语义错装(2020-03 案例:挤兑 -8.6% 后 90d +30.7%) → 探索性全样本(2002-2026, n≈5963)强烈支持:VIX≥85 分位桶各窗中位全面右偏 → 2026-06-12 初判 FAIL(60d 跌破概率两时代边际不满足)——后查明是 window 单位 bug:当时用shift(-w)= w 交易日(≈1.4w 日历天), 与生产路径分布的 日历天 口径不一致。 → 2026-06-13 漂移审计修正口径(日历天,与 forward_return 单源一致)后翻转为 PASS:fit/OOS × 30d/60d 四格全满足(中位右偏 + 跌破概率 ≤ 无条件), 但 fit-60d margin 仅 1pp(40% vs 41%),偏薄。 → 生产防御行为未据此改动——是否豁免黄金 VIX 腿是独立的、需结合反事实 记账(interventions.jsonl)钱口径证据的 gated 决策,留待另议。本条只记录 "验收口径修正后结论翻转"这个事实。 → 元教训:验收脚本的 forward-return 必须用与生产同口径(日历天),否则 一个 ±40% 的横轴误差就能翻转 PASS/FAIL。这正是 forward_return 单一可信源 (core/regime_probability.py)要解决的根因。趋势择时 overlay 扛不住交易——Q2 的"黄金 MA200 信号"是 beta(2026-06-28,
experiments/signal-eval/trend_dca.py) → 背景:signal-eval Q2 发现"黄金价在 MA200 上方 90d forward 中位 +3.43% vs 下方 −0.64%,p_holm=0.016",一度被当作"唯一显著信号"。 → 把它做成可交易的 long/flat 择时,对照匹配平均敞口的被动常仓(隔离 beta)、 两腿都扣成本(GC=F 0.38%/侧)、DSR 对 MA 网格搜索 deflate:- 黄金 post-2000 择时终值 3.07 vs 满仓买持 15.10 vs 匹配被动 7.50(只赚买持 1/5)
- Sharpe +0.36 vs 被动 +0.68;maxDD −57% vs 买持 −44% → "躲跌"实测失败,回撤反而更深
- 10 变体(MA{100..300}×buf{0,0.01})× 3 成本 × 3 资产(黄金/A股/纳指)无一过 DSR>0.95 → DCA 投入 tilt(双 sleeve"今天少投/明天多投",core 永不卖):同总额按 MA200 趋势 tilt vs 平均投入, 终值倍数(抄底/平均/顺势)黄金 4.85/4.91/4.98、A股 1.62/1.55/1.49、纳指 3.20/3.18/3.15—— 两向只动 ±1–8%、无一致赢家(≈wash),按趋势调投入额不是收益杠杆。 → 含义:Q2 的统计显著是 beta(在 MA200 上方=更牛的日子),不是择时能力;MA200 滞后→ 跌完才出场(锁亏)、涨起来才回场(踏空)。单资产趋势=噪声,不与 time-series momentum 文献矛盾 (那套靠 ~50 市场跨资产分散才稳),本 3 资产篮子做不出分散趋势组合。独立实现交叉验证数字逐位吻合。 → 全文 experiments/signal-eval/README.md "趋势择时闸" 一节。
怎么复现这些数据
每个主张都对应一个可复现的 pipeline:
# 主张 1-2 (诊断 + v1 改动)
python -m scripts.backtest_runner --workspace /tmp/diag \
--start 2024-05-13 --end 2024-11-15 --assets NDQ.AX,GC=F
# 主张 3 (trainset 命中率)
python -m scripts.archive.build_dspy_trainset \
--workspace /tmp/diag --output /tmp/trainset.json
# 主张 4 (Optuna 30-trial)
python -m scripts.rl_train --workspace /tmp/optuna --n-trials 30 ...
# 主张 5 (hold-out)
python -m scripts.holdout_validate --optuna-workspace /tmp/optuna ...
# 主张 6 (DSPy)
python -m scripts.archive.rl_optimize_prompts \
--trainset experiments/dspy_trainset_v1_2024_05_to_11.json \
--output /tmp/dspy_out.json --n-candidates 10完整流程见 docs/training_report.md(约 1200 行原始数据), 方法论解释见 11-rl-training.md。
主张 8:计分板诚实化——没有一个裁决桶好于"永远喊同一个方向"(2026-10-17)
2026-10 全面评审发现计分板把结果做漂亮了三处:UNCLEAR(CIO 解析不出合法裁决)被 .get(verdict, "flat") 折进 HOLD 桶;HOLD "命中" = 市场没动,但没减基率;命中率没有置信区间。 修法(PR 计分板诚实化):
- UNCLEAR 独立桶,不计命中:
review_calc.VERDICT_BUCKETS含 UNCLEAR,review_one不给它打 hits,export_accuracy公开 JSON 多一个unclear.total。 - 每桶同尺子基率 + 提升 + Wilson 95% CI:基率 = 同桶样本里"一个永远喊这个方向的傻瓜"的命中率 (ACCUMULATE 按涨 >0 计命中,基率就是 P(涨 >0);HOLD 是 P(留在 flat band 内))。
lift = hit_rate − base_rate。注意:若错拿directions.up(涨 > flat band,口径更严)当基率, ACCUMULATE 会被高估成 +15pp——这是实现时真踩过的坑,测试test_input_health.py守着。 - 强制 HOLD / 残缺输入单独计数不剔除:transcript 新增
## Input Health (deterministic)块 (哪些输入为空、行情最后一根、worker 失败、规则改写了什么、是否强制 HOLD),verdict_review据此标forced_hold_count/degraded_input_count;老存档退回 "HOLD 且自报 ≤0.4" 启发式。 - 同日重跑不再覆盖:live 重跑把旧 transcript 归档到
<date>/runs/,<sym>.md仍是当日最新。
实测(live 全量 390 条 → holdout 323,30d 窗口 n=246,2026-10-17 重算):
| Verdict | N | 30d 命中 | 同尺子基率 | 提升 | 95% CI |
|---|---|---|---|---|---|
| ACCUMULATE | 32 | 26.7% | 29.7% | −3.0pp | 14–44% |
| HOLD | 267 | 67.7% | 68.7% | −1.0pp | 61–74% |
| TRIM | 18 | 66.7% | 70.3% | −3.6pp | 44–84% |
| UNCLEAR | 6 | 不计 | — | — | — |
结论:三个桶的提升全部为负且 CI 都覆盖基率——现在的委员会与"永远 HOLD / 永远喊涨"在统计上 无法区分。这不是新发现(主张 1 的 100% HOLD 复读机、主张 3 的方向准确率不高),新的是它现在 每天自动算出来并带区间,不再靠人眼对比两张表。后续任何"改进"必须让某个桶的提升 CI 下界过 0。
复现:INVEST_HOME=<只读拷贝> uv run python -m openinvest.jobs.verdict_review → docs/verdict_accuracy.md; uv run python scripts/export_accuracy.py → docs/accuracy_summary.json。
8.1 反事实账本(Track A3,2026-10-17)
命中率只回答"方向对没对",不回答"照做赚不赚、比基准强不强"。每条裁决现在配两个反事实 (review_calc.counterfactual_pnl / verdict_review._benchmark_return):
- 同窗口基准超额
excess_returns[w]= 标的收益 − 组合基准(config portfolio.benchmark_symbol,默认 510300.SS)同窗口收益。 verdict 无关,所以 HOLD 也有:它回答"你拿着的东西这段时间跑没跑赢基准"。 - 照做净损益
followed_pnl[w](基准币种)= 按 CIO 金额执行并在窗口末平仓、扣往返成本(ROUNDTRIP_COST_BPS按市场后缀的保守估计)、对比留现金。 买侧 = 金额 × (收益 − 成本);卖侧 = |金额| × (−收益 − 成本)。HOLD 金额为 0,不算钱。
实测(同一批 live holdout 323 条,30d 窗口):
| Verdict | N(有基准) | 30d 超额均值 | 跑赢基准 | N(有金额) | 照做净收益率* | 照做为正 |
|---|---|---|---|---|---|---|
| ACCUMULATE | 30 | −2.4pp | 27% | 28 | −6.9% | 29% |
| HOLD | 198 | −1.1pp | 35% | — | — | — |
| TRIM | 18 | −3.0pp | 39% | 17 | +4.9% | 71% |
| 全部有金额 | 45 | +3.2% |
* 照做净收益率 = Σ净损益 / Σ|CIO 建议金额|(名义金额加权,扣往返成本,30d)。本页只给比率;绝对金额在本机 docs/verdict_accuracy.md(gitignored)里,不进公开仓库。
解读(都要带 n 看):买侧照做在扣成本后是亏的(名义加权 −6.9%),且被买的标的 30 天内 73% 跑输沪深300—— 与主张 8 的"提升为负"互相印证。拿着的持仓整体也跑输基准 1.1pp。唯一赚钱的是减仓:17 次 TRIM 照做净赚 +4.9% (71% 为正,Wilson 95% 区间约 47–88%)。这与委员会"结构性不会卖"(评审 A3:7 条规则全朝 HOLD 推、0 条朝离场推) 放在一起看,说明被压制的恰好是有价值的那一侧——但 n=17、名义加权对个别大额减仓敏感,只能当 B5(解锁基本面离场) 的立项依据,不能当结论。
局限:成本是常量表不是券商实价;基准对黄金/美股用沪深300 是用户配置口径(fork 用户改 benchmark_symbol); 金额是 CIO 建议额不是实际成交额;窗口末平仓是假设;币种:标的收益按报价币种算(美股是 USD 收益),金额却是 基准币种,汇率变动没算进去,超额也是拿报价币种收益减基准——中途改过基准币种的行不进金额账本(按 transcript 的金额币种过滤)。 方向由 verdict 定、金额取绝对值(regex 时代 TRIM 负号曾出过 bug;本次复核 390 条存档符号 0 条异常)。 公开 JSON 只出比率(excess_mean / beat_benchmark_rate,按 n_benchmark 抑制 n<30),不出金额。
8.2 自报置信度校准 + 裁决中性度(评审路线 Sprint 2 · A5,2026-10-17)
展示层早就不给用户看 CIO 自报的 confidence(改成"同类决议 30 天后怎样"的查表),但自报分仍落盘、仍进 transcript。这一节回答两个量化问题:自报分到底有没有信息、裁决分布和市场实际分布差多少。 只测量与展示,裁决逻辑一个字没动。实现:review_calc.calibration_summary / neutrality_summary(纯函数)→ verdict_review summary 的 calibration / neutrality 两节 → docs/verdict_accuracy.md 两张小表; 中性度一行进日报(email / chat 同一文本;复盘结果超过 3 天就不出这行);公开 JSON 每个窗口只加整体数字 calibration: {ece, brier, base_rate_brier, n, n_excluded, n_zero_conf}。
怎么算:把自报 confidence 当"命中概率",和 30d 实际命中(hits["30d"])对账。 Brier = mean(自报 − 命中)²;基率 Brier = 一个永远报"整样本平均命中率"的傻瓜的 Brier(= p̄(1−p̄)); ECE = Σ 档权重 × |档平均自报 − 档命中率|。分档(左闭右开,0.6 归 0.6–0.7、0.8 归 0.7–0.8):≤0.4 / 0.4–0.6 / 0.6–0.7 / 0.7–0.8 / >0.8。 样本 = live、非污染、非周末重复、30d 已成熟、有方向的裁决(UNCLEAR 不打 hits,自然排除;强制 HOLD 的 ≤0.4 行照算)。 自报恰为 0.0 的行是 cio_parse 解析失败的兜底值、不是"0% 把握",不进校准,单列 n_zero_conf(本样本 0 条,下表数字不变)。 n<30 的档不出数字(红线 #2)。
实测(live 干净样本 n=246,30d 窗口,2026-10-17 重算):
| 自报档 | N | 平均自报 | 实际命中率 | 95% CI | 本档 Brier | 本档基率 Brier |
|---|---|---|---|---|---|---|
| ≤0.4 | 49 | 0.39 | 53.1% | 39–66% | 0.272 | 0.258 |
| 0.4–0.6 | 42 | 0.54 | 61.9% | 47–75% | 0.230 | 0.236 |
| 0.6–0.7 | 90 | 0.63 | 71.1% | 61–80% | 0.218 | 0.213 |
| 0.7–0.8 | 62 | 0.74 | 58.1% | 46–70% | 0.274 | 0.246 |
| >0.8 | 3 | — | — | — | — | — |
| 整体 | 246 | 62.6% | 0.2452 | 0.2341 |
整体 ECE = 0.114。
结论:(1) 自报分的 Brier(0.245)不优于常数预测(0.234)——"自报 0.7"并不比"自报 0.6"更可能对; (2) 非单调:0.6–0.7 档 71.1% 反而高于 0.7–0.8 档 58.1%(差 13pp,两比例 z≈1.7,不显著,别当成"高置信更差"); (3) 自报分几乎是几个常量——246 条只有 20 个不同取值,0.4 / 0.55 / 0.6 / 0.62 / 0.65 / 0.75 六个值占 80%。 与展示层改用查表的决定一致:自报分没有可校准的信息。
裁决中性度(同一批 live 干净样本里 30d 已成熟的 252 条,含 UNCLEAR 6 条):
| HOLD | ACCUMULATE | TRIM | BUY | SELL | UNCLEAR | |
|---|---|---|---|---|---|---|
| 裁决占比 | 78.6% | 11.9% | 7.1% | 0% | 0% | 2.4% |
| 同期市场(directions,verdict 无关) | 横盘 | 下跌 | 上涨 |
|---|---|---|---|
| 占比 | 68.7% | 19.8% | 11.5% |
一行人话(日报同款):"过去 252 条已成熟裁决(30d)中 79% 是 HOLD,同期市场 69% 横盘(20% 下跌 / 12% 上涨);BUY 0 条、SELL 0 条。" HOLD 比横盘占比高约 10pp,且一次 BUY / SELL 都没有——与主张 8 的"提升为负"、A3 的"结构性不会卖"互相印证。
局限:相邻交易日的 30d 窗口高度重叠,真实独立样本远少于 n,CI 比表里更宽;HOLD(命中 = 市场横盘)占样本 80%, 整体校准主要是 HOLD 的校准,HOLD 与方向性裁决的"命中"含义不同,分开看要等方向性样本 ≥30(ACCUMULATE 30 条边缘、TRIM 18 条); 自报分本身也只是 CIO 的一次采样——A4 在同一输入上重跑 90 次(experiments/verdict_variance/):自报分 90/90 全是 0.62(标准差 0),方向翻转率 26.7%(8/30 条,只对应 4 个标的),其中一个标的就是 11 次 ACCUMULATE / 10 次 HOLD 的抛硬币;而生产里 2026-09-16 起的 84 条里已有 89% 自报恰好 0.62(其中只有 3 条 30d 已成熟,还没进上面的表)——等它们成熟,校准表会被一个常量档主导。公开 JSON 的 calibration 受"互补抑制"约束: base_rate_brier = p̄(1−p̄) 一步还原整体命中率,所以只要窗口的 direction_hit_rate 被抑制(有方向桶 n<30,当前 TRIM 18 条), 该窗口校准数字也置 null;被校准排除的行数 n_excluded = sample_size − n(不管原因:confidence 缺失 / 非数字 / 恰为 0.0)落在 (0, 30) 时同理(校准排除了那几行而 direction_hit_rate 含它们,p̄ 与 n 的差能逆算这几行的命中)——当前只有 30d 窗口出数字。
复现:INVEST_HOME=<只读拷贝> uv run python -m openinvest.jobs.verdict_review → docs/verdict_accuracy.md「置信度校准」「裁决中性度」两节。
8.3 B4 四臂 A/B:对照 / 密封投票 / 辩论 / macro-soft(评审路线 B4 + #461,2026-10)
实验已开跑,判读日期 = 开跑日 + 65 天。 开跑日 = 舰队 crontab 加上新 flag 之后第一个落盘日 (以
memory/.backtest_{sealed,debate,macrosoft}/里最早的日期目录为准)。 一句话翻译:舰队每天多跑三臂对照(同一批输入上:角色不互相看 + 每个标的跑三次投票 / live 日报同款的 4 轮辩论 / Macro 偏空时打折而不否决),你收到的裁决不变;65 天后出判读。
只加实验臂,不改任何 live 裁决,方式同 T2 置信度臂:产物只在数据目录(.backtest_sealed/ / .backtest_debate/ / .backtest_macrosoft/、 .dreams/verdict_review_<臂>.jsonl、docs/verdict_accuracy_arms.md,均 gitignored),不进 live 命中率页 / 公开 JSON / confidence_lookup。 每臂和现有舰队产物(对照臂,rounds=1 单次抽样)同日同标的、同一份输入(含同一份 macro_view),只多一个变量:
| 臂 | 多的那个变量 | 回答 |
|---|---|---|
| sealed | 每标的独立抽 N=3 次 rounds=1,按严格多数聚合;最大票占比代替自报置信度;金额取多数票中位数 | Q1 投票有没有用(A4:同输入重跑方向翻转 26.7%、自报置信度恒 0.62) |
| debate | rounds=4 单次抽样(= daily_report 的 max_debate_rounds=4) | Q2 辩论有没有害(调研:多轮辩论趋同、囤现金;舰队对照臂本来就是 rounds=1) |
| macro-soft | CIO 规则 2 在“Macro risk_off 且 STRENGTH<8”时由否决改为打折(金额 ×(10−STRENGTH)/10;≥8 仍否决) | Q3 #461:#256 修数据窗口后 Macro 恒为 risk_off,Quant 看多的行全被压成 HOLD |
判据(开跑前冻结):过其一才起草 live 切换提案(另一个 PR,附一句话信号翻译 + 确定性回算差异报告,用户签字):
- H1(sealed 准):整侧 30d「提升」的 95% CI 下界 > 0。
- H2(sealed 稳):配对逐日方向翻转显著少于对照臂(精确符号检验 p < 0.05)且 HOLD 占比不显著更多;功效闸:非全票一致的行 < 30 判“功效不足”。
- H3(macro-soft 值):被“解锁”的买入(macro-soft 买、对照臂同日同标的没买)成熟 n ≥ 30,30d 相对基准超额均值 95% CI 下界 > 0,且 30d 照做净收益率(反事实账本,扣往返成本)> 0。
- debate 臂不设通过门槛,是对照:与对照臂比 HOLD 占比、逐日翻转、提升。
都没过就关臂,negative result 写回本节。判读 +65 天(不是 +35):30d 窗口要满,且需要约 35 个交易日的成熟样本;+35 天时只有头约 5 天的决议满了窗口。 成本三臂合计 ≈ +¥27 / 月(总舰队 ≈ ¥9 → ¥36)。
读之前先知道:(1) sealed 与对照臂都是 rounds=1,sealed 隔离的是“多数票 + 分裂度”;(2) 自 10-08 起对照臂约 330 行 0 行非 HOLD,原因是 Macro 恒为 risk_off(#461), 所以 H1 / H2 在 Macro 回到 neutral 前很可能“功效不足”,macro-soft 臂就是为在这个饱和下拿到有分歧的行;(3) 首跑:macro-soft 臂在 Macro risk_off 下出现了对照臂没有的 ACCUMULATE(备忘录逐字执行“不否决 + 折扣”),其余臂与 #461 的现象一致。 设计、聚合规则、变体原文与开关、成本明细、判据全文与全部局限见 experiments/sealed_vote/README.md。
结果:未出(判读日前不写任何数字)。
8.4 换模型后实验怎么延续(#483,2026-10-18 起)
一句话翻译:复盘页和公开数据现在按模型分开算,换模型不会把新旧账混在一起; 新模型(Sonnet 5.5)的账从 2026-10-19 起另开一本,数够了才出数字,在此之前旧模型的数字不受影响。
2026-10-18 生产委员会的 LLM 从 deepseek-v4-flash 换成 claude-sonnet-5-5(#483)。 8 / 8.1 / 8.2 的计分板、校准、中性度、反事实账本、置信度查表此前都把两种模型的裁决混在一起算—— 换模型后实验没法继续(新模型的 n=0 被旧模型的 n=300 淹没,看不出变没变)。现在每条裁决带一个派生字段 model_era = "<谁跑的>:<模型名>"(review_calc.model_era_of,单一可信源;落进 verdict_review.jsonl):
| 人群 | 谁在跑 | 模型 | model_era | 备注 |
|---|---|---|---|---|
live Direct(日报 cron / run_committee) | 后端 Direct 路径 | claude-sonnet-5-5(2026-10-19 起的 transcript) | direct:claude-sonnet-5-5 | Input Health 的 model = 落盘时刻的全局 LLM 配置 |
| Hermes Coordinator | Hermes 的 delegate_task 子代理 | Claude Haiku(如 claude-haiku-5-5) | hermes:claude-haiku-5-5 | host 必须传 save_committee --model <名>(MCP:submit_committee(..., model=<名>));没传记 unknown(hermes:unknown / claude:unknown),不猜 |
| 纸面舰队 + B4 四臂 | 后端 Direct 路径(cron 钉 DeepSeek) | deepseek-v4-flash | direct:deepseek-v4-flash | 舰队继续钉 DeepSeek,见下。对照臂 / T2 臂的 transcript 自 2026-10-18 起在 Input Health 记 model;更早的行没记,是 direct:unknown(复盘的“不止一个模型”警告不把 unknown 当一个模型) |
| 老存档(没有 Input Health 块) | — | 决议日 ≤ 2026-10-18 的 Direct 行一律 DeepSeek | direct:deepseek-v4-flash(pre-health) | 10-18 之后没块的 → unknown(不猜)。transcript 有 **Provider**: X (skill mode) 行的是 Coordinator 老存档(模型名当时不落盘),归 X:unknown,不混进 DeepSeek 基线(live 里目前 6 条 claude) |
哪些数字按模型分开了(只测量 / 展示,裁决逻辑一个字没动):
- 复盘页(
docs/verdict_accuracy.md)新增「按模型」一节:holdout 内按model_era分子桶,各自 n、命中、同尺子基率 / 提升 / CI、反事实账本、校准、中性度; 原来的全局校准 / 中性度节标明「全部模型合并」。 - 公开 JSON(
docs/accuracy_summary.json)每个窗口加by_model: {<model_era>: {sample_size, direction_hit_rate, ...}}(同一份窗口聚合 + 同一套 n<30 抑制;模型名不算敏感)。 互补抑制:窗口合计是公开的,而命中数、命中总数、Brier×n、市场标签数这些量跨模型都是可加的——某个模型的某个量因 n<30 被藏、窗口合计和其余模型的同一个量都公开, 「窗口 − 其余模型」就能精确还原那几行。所以按量族逐个检查(命中总数 / 各方向桶的命中·提升·CI / 各桶超额 / 校准 / 市场基率 / 各桶 base_rate): 某族在某模型里"有数据却被藏"且窗口该族公开 → 其余模型的同一族也置 null(计数保留)。方向桶粒度也算(评审 #497 的复现: A 看涨 35 条 + B 看涨 5 条,窗口桶 − A 的桶 = B 的 4/5)。同时,方向桶里的base_rate(口径与命中同尺子,HOLD 桶的 base_rate 就是命中率)在样本 <30 时一律抹掉。 实际效果:新模型 30d 还没有成熟样本(sample_size=0)时旧模型数字照出;新模型成熟到 1–29 条、或某个方向桶还小的那一段,旧模型对应的by_model数字也暂时置 null,到 ≥30 条两边一起出。 - 置信度查表(邮件 / 事件提醒 / API 里「同类 HOLD 之后 30 天……」那串字)只用与当前 config 的模型同口径的样本; 某个 verdict 同模型样本不足 30 → 退回包内默认表(只含舰队 DeepSeek 样本),并注明 「默认表,deepseek-v4-flash 舰队样本,当前 claude-sonnet-5-5 样本不足」。所以 Sonnet 期里查表显示的是 DeepSeek 默认表的数,不是 Sonnet 的;这是有意的,要等同模型样本 ≥30。 只用 Coordinator 的用户(没有 Direct 配置)同理:过滤用的是
LLM_MODEL(没配时取默认deepseek-v4-flash),而他们自己的 live 行是claude:unknown/hermes:<model>, 对不上,本机表是空的、全部退到默认表,展示串只写「默认表」(默认表的模型和他们“当前配置”都是 deepseek,不加“当前模型样本不足”那段)。 这是行为变化:合入前这些行进本机表,合入后不进。想继续用自己的 Coordinator 样本,就把LLM_MODEL设成 host 传给--model的那个名字。 - B4 四臂报告(
docs/verdict_accuracy_arms.md)的臂一览加「模型」列;臂间 / 臂与对照臂用了不止一个模型时报告里直接警告。
子桶的闸按「30d 已成熟可评分行数」,不是总行数:Sonnet 期的行要等 30 个日历日才有 hits["30d"], 总行数先到 30 并不意味着有可展示的数。n<30 的子桶报告页只给 n(含 n_matured_30d)。
Sonnet 期什么时候有数字:生产每个工作日约 4 条 live 裁决(周末休市资产的周末决议被剔除)。 总行数到 30 约在 2026-10-28;但 30d 成熟要到决议日 +30 天,所以 30d 命中 / 校准 / 中性度的 n≥30 最早在 2026-11-27 前后(不是 11 月中)。 在那之前「按模型」一节里 Sonnet 那行只有 n。看进度(只读、不写):
# Sonnet 期 live 裁决条数与 30d 已成熟条数(model_era = direct:claude-sonnet-5-5)
uv run python - <<'PY'
import json, pathlib
rows = [json.loads(l) for l in pathlib.Path("memory/.dreams/verdict_review.jsonl").read_text().splitlines()]
s = [r for r in rows if r.get("model_era") == "direct:claude-sonnet-5-5" and r.get("source") == "live"]
print("sonnet rows", len(s), "matured 30d", sum(1 for r in s if "30d" in r.get("hits", {})))
PY舰队四臂继续钉 DeepSeek,判读日 2026-12-22 不变。 8.3 回答的是"结构"问题(投票 / 辩论 / macro-soft 有没有用), 需要的是各臂之间只差一个变量;中途把舰队换成 Sonnet 会让臂内数据一半 DeepSeek 一半 Sonnet,对照臂也要跟着换,等于重开实验。 所以 Sonnet 的账只从 live Direct 这一本里长,舰队这本不动。四臂报告现在标模型,若有人改了舰队的 LLM_MODEL,报告会直接警告。
舰队里的 Sonnet 模型臂(backtest_committee --sonnet-arm):舰队其余臂钉 DeepSeek,但"换模型本身"的效应需要同一批输入上的配对对照, live 每天只有 ~4 条,Sonnet 期的样本要等很久。这一臂同一批标的、live 配置(rounds=1、单次抽样)只把 LLM 换成 Sonnet 5.5 (config fleet.sonnet_arm,key 来自环境变量 FLEET_SONNET_API_KEY),每天 ≈ 50 条;Macro 由 Sonnet 自己生成,所以复盘不按 macro_shared 剔行, 在 docs/verdict_accuracy_arms.md 单列「模型对照」与对照臂同日同标的配对(差异 = 整条流水线换模型的总效应;稳定度那组 n/a)。 每天 ≈ ¥20(实测 ¥0.405/标的 + ¥0.143 Macro,50 标的)。设计、cron 行、成本明细见 experiments/paper_fleet/README.md。
#461 宏观否决要在 Sonnet 跑满一周后重看 Macro 信号分布。 #461 的前提是"Macro 恒为 risk_off、把 Quant 看多的行全压成 HOLD", 这是在 DeepSeek 上观察到的;换模型后 Macro 的 SIGNAL 分布可能不是同一个样子。Sonnet 期满一周(2026-10-26 前后)后,统计 memory/.committee 近 7 天的 Macro SIGNAL, 并和换模型前的同长度窗口比(只看分布,不改判据):
# 近 7 个有裁决的日期目录里,Macro Strategist 段的 SIGNAL 分布(live;Sonnet 期)
cd memory/.committee && for d in $(ls -d 20??-??-?? | tail -7); do for f in $d/*.md; do
awk '/^## Macro Strategist/{m=1;next} m&&/^SIGNAL:/{print $2; exit}' "$f"; done; done | sort | uniq -c | sort -rn
# 换模型前同长度窗口(DeepSeek 期):把 tail -7 换成 tail -14 | head -7Sonnet 方差基线(A4 在 Sonnet 上重测)见 experiments/verdict_variance/README.md 的「Sonnet 5.5 基线」一节。 DeepSeek 基线(26.7% / 自报分恒 0.62)不能直接外推到 Sonnet,所以同一天、同一批 30 条上两个模型各重跑 90 次(Sonnet ¥55.68,DeepSeek ¥0.60):
| DeepSeek 同日对照 | Sonnet 5.5 | |
|---|---|---|
| 方向翻转率(30 条) | 20.0%(6/30,区间 9.5–37%) | 6.7%(2/30,区间 1.8–21%) |
| 自报置信度 | 90/90 = 0.62,标准差 0 | 不是常数:0.55–0.62,68% 恰好 0.60,条内标准差均值 0.010 |
| 建议金额变异(有非 0 的条) | 6 条,CV 均值 1.33 | 2 条(CV=√3),另 28 条三次全 0 |
| 裁决分布(90 次) | ACCUMULATE 9 / HOLD 81 | HOLD 88 / TRIM 2 / ACCUMULATE 0 |
读法:Sonnet 方向上更稳(原来抛硬币的那个标的 18/18 全 HOLD),但稳是几乎不动换来的;有效样本只有 4 个输入,两个翻转率的区间重叠,别写成“显著更稳”。 自报置信度不再是常数,但是否带信息要等 Sonnet 期的校准样本(上面的进度命令)。
局限性 / 不能宣称的事
- 样本量小:66 verdict × 2 资产 × 6 个月。不足以说"100% 适用所有市况"
- LLM 记忆穿越(方向别写反):LLM 训练截止
2024-12-31(单一可信源jobs/verdict_review.py:CONTAMINATION_CUTOFF,2026-06-25 实测 MiMo 自报训练到 2024 末)。cutoff 之前的回测日期落在训练知识窗口内——"预测"可能是记忆回放, 本章全部实验窗口都在这个污染桶里;cutoff 之后的数据才是干净 holdout。 详见 ADR-022(含"绝对价位指纹"论证:匿名化日期救不了这个偏差) - paper trade simulator ≠ 真实交易:没算滑点、税费、流动性约束
- 未实盘验证:所有结论来自模拟。真接入用户实盘后效果可能不同
维护规则
新加一个主张要写:
- 实测数据(不是 vibe)
- 复现命令
- negative result 一起公布(avoiding survivorship bias)
- 不要为了"好看"删 caveat