Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/forward_observation_runtime_contract.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions tests/test_forward_observation_receipt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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"):
Expand Down