Skip to content

refactor(state): 套保比率统一为无状态因子评分 — 消除两套并行逻辑 - #1

Merged
Scivor merged 35 commits into
mainfrom
feat/hedge-ratio-scoring
Jul 21, 2026
Merged

refactor(state): 套保比率统一为无状态因子评分 — 消除两套并行逻辑#1
Scivor merged 35 commits into
mainfrom
feat/hedge-ratio-scoring

Conversation

@Scivor

@Scivor Scivor commented Jul 20, 2026

Copy link
Copy Markdown
Owner

问题

1. 仓库里有两套互不相关的套保比率逻辑

DecisionEngine compute_hedge_advice
位置 core/state/engine.py reports/pipeline.py:720
输入 40 类事件(气候/持仓/库存/关税/ML) 主导情景方向 + RSI,仅此
消费者 CLI、paper trading、回测 每周发布的周报
相互引用

reports/ 从未 import DecisionEngine。README 架构图画的「三域并行 → Decision Engine → 动态套保比率」描述的是 CLI 路径;实际发给订阅者的周报走的是另一套 40 行启发式:

base_ratio = 0.75 if 下跌 else 0.45 if 上涨 else 0.65
ratio = base ± 0.10 (RSI < 35 / > 65),clamp  [0.40, 0.90]

它完全无视关税、霜冻、ONI、COT、库存、Polymarket。

2. DecisionEngine 本身是个有状态累加器

ratio ← clamp(ratio + Δ(event), 0.20, 0.95),由此带来:

  • 棘轮效应 — 贡献永不衰减,霜冻过去三个月比率也不会回落,系统单调爬向上界后卡死
  • 相关信号重复计数 — 一次巴西霜冻同时触发 5 个事件类型,被当独立证据各记一次
  • 路径依赖 — clamp 与冷却减半都是路径函数,同一组事件换顺序结果不同
  • update_ml_signal 双重施加(真 bug)raw_ratio = self._hedge_ratio + self._ml_bias 把 bias 加到已含 bias 的比率上,MLAdvisor 周期调用会复利叠加
  • 回测与实盘不同路径compute_hedge_from_eventsdatetime.now() 判冷却,历史事件互处冷却期而全部减半
  • 系数无来源FROST_CONFIRMED = 0.30 不对应价格影响、概率或方差

方案

比率改为纯函数 core/state/scoring.py::compute_score,三条路径统一。

contrib(e) = adjustment × (severity/3.0) × 0.5^(age_days/half_life_days)
cluster    = Σᵢ contribᵢ × rank_decay^i        # 按 |贡献| 降序,14 个因子簇
score      = Σ_clusters
ratio      = baseline + span × tanh(score/k)   # span 上 0.30 / 下 0.45
  • clamp 完全消失 — tanh 天然渐近到 0.95/0.20,且边界附近保留梯度(旧实现撞到 0.95 后彻底失去响应)
  • severity/3.0 替代 severity>=4 → ×1.5 悬崖 — sev3 与现状等价,全程连续
  • 簇内递减求和 — 保留多个独立观测互相印证的加成,但收敛有上界。Polymarket 的 El Niño 概率与 NOAA ONI 归入同一簇(本就是同一信号的两次观测),贸易战概率与关税政策同理
  • 周报旧逻辑不是删掉,是降级为两个因子簇 — 主导情景 → scenario,RSI → technical,与三域事件一同进 compute_score
  • 周报出报时做三域全量扫描,与 DB 的 365 天衰减尾巴合并;去重窗口按事件类型取其 cooldown_seconds
  • _FALLBACK_EVENT_CONFIG(161 行硬编码规则表)删除,config/regimes.yaml 成为规则单一事实源

DecisionEngine 退化为薄壳:只持有事件窗口,计算全部委托 compute_score。构造函数从 (bus, use_yaml: bool) 改为 (bus, rules=None, cfg=None)

验证

294 passed / 1 failed(那 1 个在 main 上即红,早于本次重构),ruff check 全绿。

关键性质各有一个会真正失败的测试守着:

性质 测试
顺序不变性(含等值贡献的次级排序键) test_order_invariance*
衰减正确(t 与 t+半衰期贡献恰好减半) test_half_life_decay_exact
簇内去重(5 个霜冻系事件 < 5× 单个) test_cluster_dedup_sublinear
边界(极端 score 不越界不卡死) test_ratio_bounds_never_exceeded
ML 幂等(同信号连打 3 次比率不变) test_ml_signal_is_idempotent
回测/实盘一致 test_backtest_and_live_agree
周报 ratio == 引擎 ratio test_report_and_engine_agree*
归因隔离(adjustment 不混入历史衰减) test_adjustment_isolates_new_event_from_prior_decay

实测周报评分构成(真实 DB 事件):

score=0.6032 → ratio=0.9007
  policy      +0.4987    ← 关税事件真的在定价
  positioning -0.1404
  price       -0.0551
  scenario    +0.1200
  technical   +0.1000
  llm         +0.0800

顺带修掉

  • 回测事件驱动策略此前按 datetime.now() 给历史事件计龄导致贡献归零、实际退化成静态 65%,修复后比率区间 0.65→0.92
  • 新增 tests/conftest.py 阻断 socket,兑现 README「测试无网络依赖」的承诺:全量测试 233s → 3.7s,揪出 4 个泄漏点

回测第四套逻辑已删除

backtest/engine.py 曾有第四套独立套保逻辑(_calculate_ratio + _generate_events,bespoke 事件词汇,经济观点与主引擎相反),只在 run(events_df=None) 这条不可达分支里用(全仓无人调 run())。已删除(−218 行),events_df 变必填,run(events_df=None) 抛清晰异常并指向 DecisionDB.get_events() / 域扫描器。至此「三路径共用同一纯函数」名副其实——回测只剩 run_event_driven_with_engine 一条路径。

已知未修(不阻塞)

  • get_adjustment_for_event()multiplier_sev4 已无调用方(按「不顺手改无关代码」未删)
  • DB 载入的事件硬编码 domain=Domain.SUPPLYcompute_score 不读 domain,无评分影响)

设计文档:docs/superpowers/specs/2026-07-20-hedge-ratio-scoring-design.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67

Scivor and others added 30 commits July 20, 2026 15:45
问题: DecisionEngine 是有状态累加器,存在棘轮效应、相关信号重复计数、
路径依赖、ML bias 双重施加(真 bug)、回测与实盘不同路径、系数无来源。

方案: 换成纯函数评分。事件按类型半衰期衰减 → 簇内递减求和(11 个独立
风险源)→ 簇间求和 → tanh 软饱和映射到 [0.20, 0.95]。clamp 消失,
severity 悬崖改连续,ML/LLM 降为普通簇。

验证: 顺序不变性 / 衰减正确 / 簇内去重 / 边界 / ML 幂等 / 回测实盘一致。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
核实后修正 spec 的两处前提错误:
- reports/ 从不引用 DecisionEngine,周报比率来自 pipeline.py 独立的
  40 行 RSI 启发式。仓库里存在两套互不相关的套保逻辑。
- 历史数据总量为 4 期周报 + 6 个事件,原第 5 节的迁移方案是关于空集的。

方案随之扩大为「统一为单一引擎」:主导情景与 RSI 降级为两个因子簇,
与三域事件同进 compute_score,周报/CLI/回测产出同一个数字。

计划 8 个任务,每个任务 TDD 且以可自动检查的验证点收尾。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
删除 _FALLBACK_EVENT_CONFIG 后 use_yaml=False 会使规则表为空,
而 loader 非 local_only 时会联网,测试不能简单改用 YAML 路径。
构造函数改为 rules/cfg 显式注入(None → 从 YAML 加载)。
事件集 → 比率的唯一计算入口。无 I/O、无状态、时间由调用方传入,
因此顺序不变、可回测、可与实盘共用同一条路径。

score_to_ratio 对 tanh 输出做 1e-9 epsilon 夹逼:float64 下
|score/tanh_k| 稍大 tanh 就精确饱和到 ±1.0(真实数学值差距远小于
双精度分辨率),否则 ratio 会精确撞在开区间边界上。
regime_config 即将 import core.state.scoring(下一步引入 EventRule /
ScoringConfig),而 core/state/__init__.py 此前 eager import engine,
engine 又依赖 regime_config,三者成环。改为 __getattr__ 惰性加载
DecisionEngine / compute_hedge_from_events,record / signals / state
的 eager 导出不受影响。新增 test_no_import_cycle.py 锁住这个性质。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
新增 loader.event_rules() 直出 scoring.compute_score 可用的规则表;
旧 YAML 缺字段时落 misc 簇 / 30 天半衰期,不破坏兼容。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
顺带补齐 YAML 与 engine.py 硬编码回退的分歧:新增
ML_MODEL_UPDATE、PRODUCTION_UPDATE(PRICE_30D_EXTREME_UP/DOWN 已存在,直接补字段)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
- 删除 _FALLBACK_EVENT_CONFIG(161 行),YAML 成为规则单一事实源
- 每次事件到达全量重算,路径依赖与棘轮效应消失
- update_ml_signal 改发 ML_MODEL_UPDATE 事件,修复 bias 双重施加 bug
  (同一信号重复注入现在是幂等的)
- 新增 recompute(now) / get_breakdown(),逐簇归因供 reports/ 直读
- 构造函数 use_yaml → rules/cfg 显式注入;同步 backtest/engine.py 调用点
- 删除 MLAdvisor.run() 里重复发布的 ML 事件:其 value=confidence 无方向,
  在新评分下会双重计数并把 BULLISH 信号算成加套保

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
修复 code review 发现的 4 个 Important + 4 个 Minor 问题(均围绕
Task 4 无状态薄壳重构引入的状态管理缺陷):

- I2: recompute() 与 handle() 早退分支不记状态,导致 get_state()
  与 get_breakdown() 长期不一致;两处都补上 _record_state()。
- I3: HedgeAdjustment.adjustment 的 old_ratio/new_ratio 时间基准不同,
  历史事件衰减被错误归因到当前事件;改为同一个 now 分两次重算。
- I4: 低置信度 ML 信号只清展示字段、不清窗口里的旧 ML_MODEL_UPDATE
  事件,无法真正撤销先前的高置信度 bias;清窗口逻辑移到置信度判断前。
- I5: update_ml_signal 的"清窗口→publish"跨越锁释放窗口,并发调用
  可能交错重复计入;新增独立 _ml_lock 串行化整个方法体。
- M6: MLAdvisor 删除死字段 bus/self.bus,同步 docstring 与孤儿 import。
- M7: 删除 backtest/engine.py 中描述已移除死锁的过时注释。
- M8: tests/_event() 删除未使用的 bus_engine 形参。
- M9: 收紧 test_ratio_decays_without_new_events 的容差至闭式解精度。

新增回归测试 test_get_state_tracks_recompute (I2) 与
test_low_confidence_ml_retracts_previous_signal (I4)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
DecisionEngine._make_handler.handle 记录 HedgeAdjustment.adjustment 时
必须只归因新事件自身效应;用 monkeypatch 控制 engine.py 内部的 datetime.now(),
构造「FROST 后 180 天再发 SPIKE」场景,验证 adjustment 等于同一时刻下
有/无新事件两种窗口比率之差,而非误将 FROST 的衰减算进 SPIKE 的调整量。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
旧实现一次性 publish 全部事件且用 datetime.now() 判冷却,
导致历史事件互处冷却期贡献全部减半,回测跑的不是实盘那个引擎。
顺带消除 DecisionEngine 在子进程里的线程死锁隐患。

同时关闭上一任务遗留的 C1: 回测直接调 engine._make_handler,
handler 内写死 datetime.now() 计龄,2022 年的事件 decay≈1e-5,
比率全程 0.65 → 事件驱动与静态 65% 逐笔一致、节省% 恒为 0。
改为按 bar 时间戳调 compute_hedge_from_events 后修复,
并加两个回归测试锁住「事件必须驱动可观测的比率变化」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
LLM 点评从 reports/ 层记分升级为进入评分公式的一个因子簇,
与 ML 一样受 tanh 软饱和约束,不再绕过全部约束。
compute_hedge_advice 不再自行决定比率:主导情景与 RSI 降级为
scenario / technical 两个因子簇,与三域事件一同进 compute_score。
周报、CLI、回测自此产出同一个数字。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
Code review 三个 Important + 一个测试强化:

1. china_import 到库成本 与 溯源账本 hedge_ratio 用的是改写前(第一遍)的
   比率,与最终写入 report.hedge_advice 的比率不一致,周报里出现两个矛盾
   的套保数字。把 fetch_china_import_snapshot 与相关 4 条 prov.add 挪到
   LLM 重算之后,统一读 report.hedge_advice 终态值。(评估过把 LLM 提前
   到只算一次的方案:china_import 的到库成本依赖最终 hedge ratio,LLM
   prompt 又依赖 china_import,二者互相依赖排不出线性顺序,故未采用。)

2. llm_commentary 的 prompt 里喂了即将被自己改写的 hedge_advice,删除
   该行消除循环论证;连带修掉因此变红的 test_generate_commentary_success
   断言(断言的正是被删掉的行为)。

3. gather_report_events 的历史事件循环外层 except 会在某一行坏数据
   (如非法 timestamp)时截断其后所有行,丢失衰减尾巴;改为外层只包
   DB 调用本身,逐行单独窄 except (ValueError, KeyError, TypeError):
   continue,debug 级日志,不刷屏。

4. 强化 test_report_and_engine_agree:补一条报告侧因子非零(下跌+RSI=30)
   的用例,容差从 abs=0.005(零余量)放宽到 abs=0.01;新增
   gather_report_events 的 DB 不可用降级测试。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
上一轮修复把 fetch_china_import_snapshot 挪到 LLM 点评生成之后(为了让
到库成本用最终套保比率),但副作用是 _attach_llm_commentary 运行时
report.china_import 恒为 None,导致 _build_context 里汇率/到库成本/
政策事件三行被静默跳过 —— AI 点评从此看不到关税等政策事件叙述。

china_import 快照里只有到库成本依赖套保比率,汇率与政策事件不依赖,
故拆开处理:fetch_china_import_snapshot 挪回 LLM 点评之前(用初版
hedge 算),Step 6 最终比率算出后只原地重算到库成本(纯本地计算,
不联网),不重复整体 fetch。compute_score 入口仍只调两次。

新增 tests/test_pipeline_china_import_order.py:驱动真实 pipeline.run()
(全 mock,无网络)断言喂给 LLM 的 prompt 含汇率/政策事件内容,并验证
fetch_china_import_snapshot 只被调用一次;已验证该测试在回滚本次修复后
会先失败(捕获到了这个具体回归),修复后转绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
README 架构图此前描述的是 CLI 路径,周报走的是另一套逻辑;
两者已统一,文档随之更正。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
问题:scoring property 未先 load(),若它是第一个被访问的 property,
静默返回默认 ScoringConfig() 而非 YAML 值。
方案:补一行 self.load(),与 adjustment_rules / settings / get_adjustment_rule 对齐。
验证:pytest tests/test_unified_hedge.py 全绿。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
问题:YAML 加载失败或 adjustment_rules 为空时,DecisionEngine 订阅空集、
对所有事件永久失聪,却只留一行 warning。
方案:加载异常提为 logger.error;另加一条覆盖「加载成功但规则表仍为空」
这个更隐蔽的分支。不抛异常 —— 测试会显式传 rules={},且引擎失聪不像
周报印错数那样直接对外。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
问题:gather_report_events 只实现了设计文档 §2.4 事件窗口的后半截(DB 衰减
尾巴)。events 表全仓唯一写入方是 coffee_system._persist_event,挂在
CoffeeSystem 实例的 bus 订阅上;周报 run() 是独立入口,从不构造 CoffeeSystem。
后果:周报看不到任何新鲜事件,只能看到上次跑 CLI 时落库并已衰减的那批。

方案:
- _scan_live_events():supply/finance/policy 三域扫描,每域独立 try/except
  (单域失败只丢该域),扫描器挂独立 EventBus 避免污染全局总线。
- gather_report_events(live_scan: bool = True):网络 I/O 可关闭。
- _dedupe_events():同 event_type 且 timestamp 相差 ≤1h 视为同一现实事件,
  保留较新的一条。narrative/source 在两条路径上必然不同,不参与判据。
- run() 第二次重算复用 Step 3 已扫描的事件,避免一期周报做两轮网络扫描;
  compute_score 仍只调两次。
- 修正 docstring —— 「仅靠新鲜扫描不够」这句话现在才成立。

验证:新增 DB 行解析(真实 tmp SQLite)、类型未知跳过、坏行不连累后续行、
扫描∩DB 去重、单域失败不连累其他域、三域全炸仍出报、live_scan=False
完全绕开扫描器 —— 共 8 个测试,全程无联网。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
问题:删掉 _FALLBACK_EVENT_CONFIG 后 YAML 是唯一事实源。若 loader 落到
_default_config()(adjustment_rules: {}),所有事件被 rules.get() 过滤 →
score=0 → ratio 恰好 0.65 → narrative 印「维持中性 | 无活跃因子」。
看起来像一个正常的中性判断,而不是故障。

方案:compute_hedge_advice 在 rules 为空时抛 RuntimeError,异常信息点名
config/regimes.yaml 的 adjustment_rules。周报宁可缺这一板块也不能印错数。

验证:test_compute_hedge_advice_raises_on_empty_rules。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
问题:notes 写「基于RSI+情景概率+ML信号的规则引擎」,已失效。现在的机制是
三域事件按类型半衰期衰减、归入 14 个因子簇加权评分;ML 已不直接进周报评分
(gather_report_events 不产出 ML_MODEL_UPDATE 事件),RSI/情景只是 14 个簇
中的两个。这段文字随周报发给订阅者。

方案:改为如实描述,保留「非投资建议」免责与同样简短的风格。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
Scivor and others added 5 commits July 20, 2026 20:54
- PipelineConfig 加 live_scan 字段并透传给 gather_report_events;
  test_pipeline_china_import_order.py 改用 live_scan=False(64.81s → 0.90s),
  修复"全 mock 无网络"测试实际发出真实 HTTP 请求的问题。
- _dedupe_events 去重窗口改为按事件类型取 config/regimes.yaml 的
  cooldown_seconds(价格类 5 分钟、霜冻类 10 分钟、政策类 24 小时),
  不再对所有类型统一用死的 1 小时,避免持续性事件被过度压缩计数;
  regime_config.py 中已失效的旧注释同步订正。
- 新增 tests/conftest.py:autouse fixture 默认阻断 socket 连接,
  @pytest.mark.integration 放行确需联网/起本机 mock server 的测试。
  由此揭出 4 个此前静默联网的测试:test_agent_e2e.py 是合理的本地
  mock server 测试(标记 integration 放行),另外 3 个是缺 mock
  (PriceSource/FXSource 构造时自举网络请求)——补 mock 修复,
  全量测试 233s → 3.7s。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
….run()

_generate_events/_calculate_ratio was a fourth, independent hedge-ratio
implementation with its own event vocabulary and inverted economic
assumptions vs the main scoring engine. It was only reachable via
run(events_df=None), which no code in the repo calls (run_event_driven_with_engine
already covers all three strategies). run() now requires events_df and
raises ValueError otherwise; behavior for run(events_df=<data>) is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
severity>=4 的 ×1.5 悬崖乘数已被连续的 severity/3.0 缩放取代,
multiplier_sev4 就此失活;其唯一读者 get_adjustment_for_event 在 main
上也早已零调用。一并删除字段、解析、方法及 38 个死 YAML 键。

顺带补准 adjustment_rules 的 schema 注释 —— 此前漏了 cluster 与
half_life_days(本次重构的核心字段),并订正 cooldown_seconds 的说明
(现用途是周报去重窗口)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
# Conflicts:
#	docs/superpowers/plans/2026-07-20-hedge-ratio-scoring.md
@Scivor
Scivor merged commit ce8afb4 into main Jul 21, 2026
2 checks passed
@Scivor
Scivor deleted the feat/hedge-ratio-scoring branch July 21, 2026 02:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant