From b68172f075d1ea9b50ef2a9197cf611a6a2dbae4 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:42:04 +0800 Subject: [PATCH] fix: allow shadow-only forward observation receipts Co-Authored-By: Codex --- ...ward_observation_runtime_contract.zh-CN.md | 4 +-- .../forward_observation_receipt.py | 9 ++++--- tests/test_forward_observation_receipt.py | 26 +++++++++++++++++++ 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/docs/forward_observation_runtime_contract.zh-CN.md b/docs/forward_observation_runtime_contract.zh-CN.md index 8d3b9091..f86d0556 100644 --- a/docs/forward_observation_runtime_contract.zh-CN.md +++ b/docs/forward_observation_runtime_contract.zh-CN.md @@ -6,13 +6,13 @@ 每个冻结候选必须显式提供自己的 `ForwardObservationPolicy`:候选 ID、策略 profile、无杠杆基准、前瞻交易日数量、复核里程碑、恢复前需要的连续健康周期、市场日历、固定或滚动窗口、窗口起点、窗口理由引用,以及精确的非 Live 证据模式。控制器没有“252 天”“20/60 天”或某个策略的隐性默认值;新候选缺少这些字段会被拒绝,不能继承 SOXL 的参数。没有已验证的 P3 历史证据和证据引用时,状态固定为 `PARKED`。 -P3 通过后,控制器可以自动给出 `start_shadow`、`start_paper`、`continue_*` 和在短暂数据/运行故障恢复后的 `resume_*` 意图。证据模式必须明确为 `shadow_decision + simulated_replay` 或 `shadow_decision + broker_paper`,不能把模拟回放、订单预览和券商 Paper 混称为同一种 Paper。数据过期或 Shadow/Paper 不一致时,才会进入可自动恢复的 `PAUSED`;风险阻断、人工冻结、身份不匹配、撤销或被新候选替代,分别进入不可自动恢复的终止状态。 +P3 通过后,控制器可以自动给出 `start_shadow`、`continue_shadow` 和在短暂数据/运行故障恢复后的 `resume_shadow` 意图;只有 policy 显式声明 Paper 通道时,才会一并给出对应的 `*_paper` 意图。Shadow-only policy 的证据模式只能是 `shadow_decision`;启用 Paper 时必须精确为 `shadow_decision + simulated_replay` 或 `shadow_decision + broker_paper`。不能把模拟回放、订单预览和券商 Paper 混称为同一种 Paper。数据过期或**已配置**的 Shadow/Paper 通道不一致时,才会进入可自动恢复的 `PAUSED`;风险阻断、人工冻结、身份不匹配、撤销或被新候选替代,分别进入不可自动恢复的终止状态。 真正的候选 Shadow 必须在同一份带时间戳的输入快照下,同时保存候选与基线的信号、 假设订单、仓位、成本和收益,并按 `candidate_id` 隔离。生产策略的近期绩效快照 只能用于监控,不能替代候选的并行 Shadow 证据。 -达到候选自己的完整前瞻窗口(例如 SOXL V7 基于其回测与风险验证采用 252 个交易日)后,控制器会停止两种非 Live 意图,并进入 `FORWARD_COMPLETE_HUMAN_REVIEW`。不同策略可以采用不同观察期,但必须由冻结候选与回测证据明确证明,不能在运行中自动修改。返回值永久包含: +达到候选自己的完整前瞻窗口(例如 SOXL V7 基于其回测与风险验证采用 252 个交易日)后,控制器会停止 policy 已配置的非 Live 通道,并进入 `FORWARD_COMPLETE_HUMAN_REVIEW`。不同策略可以采用不同观察期,但必须由冻结候选与回测证据明确证明,不能在运行中自动修改。返回值永久包含: - `no_order=true` - `live_authority_granted=false` diff --git a/src/quant_platform_kit/strategy_lifecycle/forward_observation_receipt.py b/src/quant_platform_kit/strategy_lifecycle/forward_observation_receipt.py index 0f9d2ef2..495554bc 100644 --- a/src/quant_platform_kit/strategy_lifecycle/forward_observation_receipt.py +++ b/src/quant_platform_kit/strategy_lifecycle/forward_observation_receipt.py @@ -137,15 +137,16 @@ def _evidence_modes(value: object) -> tuple[str, ...]: if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): _invalid("evidence_modes must be an array") modes = tuple(_text(item, "evidence_modes").lower() for item in value) + paper_modes = {"simulated_replay", "broker_paper"} & set(modes) if ( - len(modes) != 2 - or len(set(modes)) != 2 + len(modes) not in {1, 2} + or len(set(modes)) != len(modes) or set(modes) - FORWARD_OBSERVATION_EVIDENCE_MODES or "shadow_decision" not in modes - or not ({"simulated_replay", "broker_paper"} & set(modes)) + or len(paper_modes) > 1 ): _invalid( - "evidence_modes must contain shadow_decision and exactly one paper mode" + "evidence_modes must contain shadow_decision and at most one paper mode" ) return modes diff --git a/tests/test_forward_observation_receipt.py b/tests/test_forward_observation_receipt.py index f7c15d0b..4e557d6b 100644 --- a/tests/test_forward_observation_receipt.py +++ b/tests/test_forward_observation_receipt.py @@ -65,6 +65,23 @@ def test_receipt_binds_exact_policy_dependencies_and_sanitized_modes() -> None: assert all(token not in str(receipt).lower() for token in ("account", "order", "price")) +def test_shadow_only_policy_emits_a_receipt_without_paper_evidence() -> None: + policy = _policy( + automatic_non_live_modes=("shadow",), + non_live_evidence_modes=("shadow_decision",), + ) + receipt = build_forward_observation_receipt( + policy=policy, + observation_session="2026-08-26", + observation_index=1, + dependency_digests=_dependencies(), + evidence_modes=("shadow_decision",), + ) + + assert receipt["evidence_modes"] == ["shadow_decision"] + assert validate_forward_observation_receipt(receipt, policy=policy) == receipt + + def test_receipts_append_only_with_a_stable_candidate_policy_and_hash_chain() -> None: first = _receipt() second = _receipt(previous=first, index=2, session="2026-08-27") @@ -113,6 +130,15 @@ def test_receipt_rejects_ambiguous_modes_missing_digests_and_content_tampering() evidence_modes=("shadow_decision", "broker_paper"), ) + with pytest.raises(InvalidForwardObservationReceipt, match="at most one paper mode"): + build_forward_observation_receipt( + policy=_policy(), + observation_session="2026-08-26", + observation_index=1, + dependency_digests=_dependencies(), + evidence_modes=("shadow_decision", "broker_paper", "simulated_replay"), + ) + missing = _dependencies() missing.pop("plugin_bundle") with pytest.raises(InvalidForwardObservationReceipt, match="closed digest set"):