refactor(state): 套保比率统一为无状态因子评分 — 消除两套并行逻辑 - #1
Merged
Conversation
问题: 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 会精确撞在开区间边界上。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67
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
- 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
1. 仓库里有两套互不相关的套保比率逻辑
DecisionEnginecompute_hedge_advicecore/state/engine.pyreports/pipeline.py:720reports/从未 importDecisionEngine。README 架构图画的「三域并行 → Decision Engine → 动态套保比率」描述的是 CLI 路径;实际发给订阅者的周报走的是另一套 40 行启发式:它完全无视关税、霜冻、ONI、COT、库存、Polymarket。
2.
DecisionEngine本身是个有状态累加器ratio ← clamp(ratio + Δ(event), 0.20, 0.95),由此带来:update_ml_signal双重施加(真 bug) —raw_ratio = self._hedge_ratio + self._ml_bias把 bias 加到已含 bias 的比率上,MLAdvisor 周期调用会复利叠加compute_hedge_from_events用datetime.now()判冷却,历史事件互处冷却期而全部减半FROST_CONFIRMED = 0.30不对应价格影响、概率或方差方案
比率改为纯函数
core/state/scoring.py::compute_score,三条路径统一。clamp完全消失 — tanh 天然渐近到 0.95/0.20,且边界附近保留梯度(旧实现撞到 0.95 后彻底失去响应)severity/3.0替代severity>=4 → ×1.5悬崖 — sev3 与现状等价,全程连续scenario,RSI →technical,与三域事件一同进compute_scorecooldown_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*test_half_life_decay_exacttest_cluster_dedup_sublineartest_ratio_bounds_never_exceededtest_ml_signal_is_idempotenttest_backtest_and_live_agreetest_report_and_engine_agree*test_adjustment_isolates_new_event_from_prior_decay实测周报评分构成(真实 DB 事件):
顺带修掉
datetime.now()给历史事件计龄导致贡献归零、实际退化成静态 65%,修复后比率区间 0.65→0.92tests/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已无调用方(按「不顺手改无关代码」未删)domain=Domain.SUPPLY(compute_score不读 domain,无评分影响)设计文档:
docs/superpowers/specs/2026-07-20-hedge-ratio-scoring-design.md🤖 Generated with Claude Code
https://claude.ai/code/session_01XNq9BjY8uVen3f233r7u67