Skip to content

Commit 77e79ce

Browse files
Pigbibicodex
andcommitted
feat(risk): bind owner preference after private P3 ingress
Co-Authored-By: Codex <noreply@openai.com>
1 parent 4640d7f commit 77e79ce

3 files changed

Lines changed: 186 additions & 5 deletions

File tree

docs/qsl_long_horizon_risk_composer_v1.zh-CN.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
# QSL 长期复利风险政策 Composer V1
22

3-
> 状态:`ADVISORY_CORE_IMPLEMENTED_NOT_WIRED`
3+
> 状态:`PRIVATE_P3_OBSERVATION_INGRESS_IMPLEMENTED_POLICY_WRITE_NOT_WIRED`
44
55
`python/scripts/long_horizon_risk_composer.py` 是所有策略、组合与插件可复用的离线风险设计内核。
66
它把已经冻结、净成本后的 P3 策略收益路径与同周期的无杠杆基准路径,计算成一个脱敏的风险尺度前沿。
77
它不读取账户、资金、券商、凭据或网络;不写入风险政策、不改策略参数、不启动调度,也不能授予 P4、P5 或 P6。
88

9+
研究管道与控制面之间使用 `qsl.long_horizon_risk_observation.v1`。研究管道只负责产出候选、P1/P2/P3/plugin 摘要、无杠杆基准和成对净收益路径;控制面必须显式叠加所有者选择的风险偏好,才可转换为 Composer 输入。观察件是**私有 ingress 工件**:不能上传到公开仓库、Actions 公开摘要、控制台或 AI 上下文;Composer 输出才是可发布的脱敏摘要。
10+
911
## 人和系统的分工
1012

1113
人工只选择三个简单、重要的偏好之一:
@@ -18,6 +20,8 @@
1820

1921
这三个倍数是透明、版本化的偏好模板,不是模型从历史数据“发现”的真理。系统计算的内容是每个候选在每个尺度下的实际净成本路径、最大回撤、相对基准回撤、水下持续期和每 session 对数几何增长;它不会把一次历史最优结果伪装成未来保证。
2022

23+
命令行也遵守这条分工:已有 owner-bound 输入可用 `--input`;私有观察件必须同时给出 `--observation``--risk-preference`。缺少偏好即失败,不会静默选择“均衡”或任何默认档位。
24+
2125
## 必要证据和计算方法
2226

2327
输入必须精确绑定 candidate revision 与 P1/P2/P3/plugin 摘要,并至少包含每类一个完整的 252-session 以上路径:

python/scripts/long_horizon_risk_composer.py

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
RISK_COMPOSER_INPUT_SCHEMA_ID = "qsl.long_horizon_risk_composer_input.v1"
3131
RISK_COMPOSER_RECOMMENDATION_SCHEMA_ID = "qsl.long_horizon_risk_composer_recommendation.v1"
32+
RISK_OBSERVATION_SCHEMA_ID = "qsl.long_horizon_risk_observation.v1"
3233
_IDENTITY_PATTERN = re.compile(r"^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$")
3334
_REPOSITORY_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*$")
3435
_REVISION_PATTERN = re.compile(r"^[0-9a-f]{40}$")
@@ -41,9 +42,11 @@
4142
re.IGNORECASE,
4243
)
4344
_INPUT_FIELDS = {"schema", "candidate", "source_evidence", "objective", "scenario_paths", "input_sha256"}
45+
_OBSERVATION_FIELDS = {"schema", "candidate", "source_evidence", "benchmark", "scenario_paths", "observation_sha256"}
4446
_CANDIDATE_FIELDS = {"candidate_id", "candidate_kind", "strategy_repository", "strategy_revision"}
4547
_SOURCE_EVIDENCE_FIELDS = {"p1_input_digest", "p2_config_digest", "p3_evidence_sha256", "plugin_bundle_sha256"}
4648
_OBJECTIVE_FIELDS = {"risk_preference", "benchmark_id", "benchmark_kind", "sessions_per_year"}
49+
_BENCHMARK_FIELDS = {"benchmark_id", "benchmark_kind", "sessions_per_year"}
4750
_SCENARIO_FIELDS = {"scenario_id", "scenario_kind", "strategy_returns_bps", "benchmark_returns_bps"}
4851
_RECOMMENDATION_FIELDS = {
4952
"schema",
@@ -190,6 +193,13 @@ def calculate_risk_composer_recommendation_sha256(value: Mapping[str, Any]) -> s
190193
).hexdigest()
191194

192195

196+
def calculate_risk_observation_sha256(value: Mapping[str, Any]) -> str:
197+
"""Return the stable identity of one private P3 return-path observation."""
198+
return hashlib.sha256(
199+
_canonical_json(value, "observation_sha256", "long-horizon risk observation").encode("utf-8")
200+
).hexdigest()
201+
202+
193203
def _validate_candidate(value: Any) -> dict[str, str]:
194204
candidate = _expect_object(value, "candidate")
195205
_expect_exact_keys(candidate, _CANDIDATE_FIELDS, "candidate")
@@ -234,6 +244,20 @@ def _validate_objective(value: Any) -> dict[str, Any]:
234244
}
235245

236246

247+
def _validate_benchmark(value: Any) -> dict[str, Any]:
248+
benchmark = _expect_object(value, "benchmark")
249+
_expect_exact_keys(benchmark, _BENCHMARK_FIELDS, "benchmark")
250+
if benchmark["benchmark_kind"] != "unlevered_reference":
251+
_fail("benchmark.benchmark_kind must be unlevered_reference")
252+
return {
253+
"benchmark_id": _expect_identity(benchmark["benchmark_id"], "benchmark.benchmark_id"),
254+
"benchmark_kind": _expect_identity(benchmark["benchmark_kind"], "benchmark.benchmark_kind"),
255+
"sessions_per_year": _expect_positive_integer(
256+
benchmark["sessions_per_year"], "benchmark.sessions_per_year", maximum=366
257+
),
258+
}
259+
260+
237261
def _validate_scenario(value: Any, index: int) -> dict[str, Any]:
238262
path = f"scenario_paths[{index}]"
239263
scenario = _expect_object(value, path)
@@ -287,6 +311,68 @@ def validate_risk_composer_input(value: Any) -> dict[str, Any]:
287311
return normalized
288312

289313

314+
def validate_long_horizon_risk_observation(value: Any) -> dict[str, Any]:
315+
"""Validate a private P3 observation before an owner preference is bound.
316+
317+
The observation contains only frozen candidate identity, evidence digests,
318+
a same-window unlevered reference, and paired net-return paths. It is an
319+
internal ingress artifact: it is never suitable for a public console or
320+
AI prompt. Unlike a composer input it intentionally contains no risk
321+
preference, because that is a control-plane/owner decision.
322+
"""
323+
_reject_non_finite_or_null(value, "long-horizon risk observation")
324+
_reject_forbidden_material(value, "long-horizon risk observation")
325+
observation = _expect_object(value, "long-horizon risk observation")
326+
_expect_exact_keys(observation, _OBSERVATION_FIELDS, "long-horizon risk observation")
327+
if observation["schema"] != RISK_OBSERVATION_SCHEMA_ID:
328+
_fail(f"long-horizon risk observation.schema must be {RISK_OBSERVATION_SCHEMA_ID}")
329+
paths = _expect_list(observation["scenario_paths"], "observation.scenario_paths")
330+
if not paths or len(paths) > _MAX_SCENARIOS:
331+
_fail(f"observation.scenario_paths must contain between 1 and {_MAX_SCENARIOS} paths")
332+
normalized = {
333+
"schema": RISK_OBSERVATION_SCHEMA_ID,
334+
"candidate": _validate_candidate(observation["candidate"]),
335+
"source_evidence": _validate_source_evidence(observation["source_evidence"]),
336+
"benchmark": _validate_benchmark(observation["benchmark"]),
337+
"scenario_paths": [_validate_scenario(item, index) for index, item in enumerate(paths)],
338+
"observation_sha256": _expect_sha256(
339+
observation["observation_sha256"], "long-horizon risk observation.observation_sha256"
340+
),
341+
}
342+
if len({path["scenario_id"] for path in normalized["scenario_paths"]}) != len(normalized["scenario_paths"]):
343+
_fail("observation.scenario_paths.scenario_id values must be unique")
344+
if normalized["observation_sha256"] != calculate_risk_observation_sha256(normalized):
345+
_fail("long-horizon risk observation.observation_sha256 mismatch")
346+
return normalized
347+
348+
349+
def build_risk_composer_input_from_observation(
350+
observation: Any, *, risk_preference: str
351+
) -> dict[str, Any]:
352+
"""Attach one explicit owner preference to a frozen private observation.
353+
354+
This is deliberately a pure conversion. It does not refresh P3 data,
355+
choose a preference, write a policy, or authorize any lifecycle phase.
356+
"""
357+
normalized = validate_long_horizon_risk_observation(observation)
358+
objective = _validate_objective(
359+
{
360+
"risk_preference": risk_preference,
361+
**normalized["benchmark"],
362+
}
363+
)
364+
result: dict[str, Any] = {
365+
"schema": RISK_COMPOSER_INPUT_SCHEMA_ID,
366+
"candidate": normalized["candidate"],
367+
"source_evidence": normalized["source_evidence"],
368+
"objective": objective,
369+
"scenario_paths": normalized["scenario_paths"],
370+
"input_sha256": "",
371+
}
372+
result["input_sha256"] = calculate_risk_composer_input_sha256(result)
373+
return validate_risk_composer_input(result)
374+
375+
290376
def _scaled_return_bps(return_bps: int, scale_bps: int) -> int:
291377
product = return_bps * scale_bps
292378
return product // 10_000 if product >= 0 else -((-product + 9_999) // 10_000)
@@ -522,13 +608,33 @@ def parse_risk_composer_input_json(text: str) -> dict[str, Any]:
522608

523609
def main(argv: list[str] | None = None) -> int:
524610
parser = argparse.ArgumentParser(description="Compose a non-executing long-horizon risk recommendation")
525-
parser.add_argument("--input", type=Path, required=True, help="frozen P3 return-path evidence JSON")
611+
input_source = parser.add_mutually_exclusive_group(required=True)
612+
input_source.add_argument("--input", type=Path, help="private, owner-bound P3 return-path evidence JSON")
613+
input_source.add_argument(
614+
"--observation",
615+
type=Path,
616+
help="private P3 observation JSON; requires an explicit --risk-preference",
617+
)
618+
parser.add_argument(
619+
"--risk-preference",
620+
choices=tuple(sorted(_RISK_PREFERENCES)),
621+
help="owner-selected preference when converting a private observation",
622+
)
526623
parser.add_argument("--output", type=Path, required=True, help="advisory recommendation JSON")
527624
args = parser.parse_args(argv)
528625
try:
529-
recommendation = compose_long_horizon_risk_recommendation(
530-
parse_risk_composer_input_json(args.input.read_text(encoding="utf-8"))
531-
)
626+
if args.input is not None:
627+
if args.risk_preference is not None:
628+
_fail("--risk-preference is only valid with --observation")
629+
composer_input = parse_risk_composer_input_json(args.input.read_text(encoding="utf-8"))
630+
else:
631+
if args.risk_preference is None:
632+
_fail("--observation requires --risk-preference")
633+
composer_input = build_risk_composer_input_from_observation(
634+
parse_risk_composer_input_json(args.observation.read_text(encoding="utf-8")),
635+
risk_preference=args.risk_preference,
636+
)
637+
recommendation = compose_long_horizon_risk_recommendation(composer_input)
532638
validated = validate_risk_composer_recommendation(recommendation)
533639
args.output.write_text(
534640
json.dumps(validated, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + "\n",

python/tests/test_long_horizon_risk_composer.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import importlib.util
55
import json
66
import sys
7+
import tempfile
78
import unittest
89
from pathlib import Path
910

@@ -74,6 +75,23 @@ def _input(self, *, preference: str = "BALANCED_COMPOUNDING") -> dict[str, objec
7475
value["input_sha256"] = composer.calculate_risk_composer_input_sha256(value)
7576
return value
7677

78+
def _observation(self) -> dict[str, object]:
79+
composer_input = self._input()
80+
value: dict[str, object] = {
81+
"schema": "qsl.long_horizon_risk_observation.v1",
82+
"candidate": composer_input["candidate"],
83+
"source_evidence": composer_input["source_evidence"],
84+
"benchmark": {
85+
"benchmark_id": "soxx",
86+
"benchmark_kind": "unlevered_reference",
87+
"sessions_per_year": 252,
88+
},
89+
"scenario_paths": composer_input["scenario_paths"],
90+
"observation_sha256": "",
91+
}
92+
value["observation_sha256"] = composer.calculate_risk_observation_sha256(value)
93+
return value
94+
7795
def test_balanced_composer_selects_the_highest_robust_growth_scale_within_benchmark_drawdown_envelope(self):
7896
recommendation = composer.compose_long_horizon_risk_recommendation(self._input())
7997

@@ -144,6 +162,59 @@ def test_tampering_with_evidence_or_smuggling_capital_fails_closed(self):
144162
with self.assertRaisesRegex(composer.LongHorizonRiskComposerError, "capital_amount is forbidden"):
145163
composer.compose_long_horizon_risk_recommendation(unsafe)
146164

165+
def test_private_observation_needs_an_explicit_preference_before_composition(self):
166+
observation = self._observation()
167+
validated = composer.validate_long_horizon_risk_observation(observation)
168+
composer_input = composer.build_risk_composer_input_from_observation(
169+
validated,
170+
risk_preference="CAPITAL_PRESERVATION",
171+
)
172+
173+
self.assertEqual(composer_input["objective"]["risk_preference"], "CAPITAL_PRESERVATION")
174+
self.assertEqual(composer_input["objective"]["benchmark_id"], "soxx")
175+
self.assertEqual(composer_input, composer.validate_risk_composer_input(composer_input))
176+
self.assertEqual(
177+
composer.compose_long_horizon_risk_recommendation(composer_input)["status"],
178+
"ADVISORY_RECOMMENDATION_READY",
179+
)
180+
181+
tampered = copy.deepcopy(observation)
182+
tampered["benchmark"]["benchmark_id"] = "qqq"
183+
with self.assertRaisesRegex(composer.LongHorizonRiskComposerError, "observation_sha256 mismatch"):
184+
composer.build_risk_composer_input_from_observation(
185+
tampered,
186+
risk_preference="BALANCED_COMPOUNDING",
187+
)
188+
189+
def test_cli_accepts_private_observation_but_not_an_implicit_preference(self):
190+
with tempfile.TemporaryDirectory() as directory:
191+
root = Path(directory)
192+
observation = root / "observation.json"
193+
output = root / "recommendation.json"
194+
observation.write_text(json.dumps(self._observation()), encoding="utf-8")
195+
196+
self.assertEqual(
197+
composer.main(["--observation", str(observation), "--output", str(output)]),
198+
1,
199+
)
200+
self.assertFalse(output.exists())
201+
self.assertEqual(
202+
composer.main(
203+
[
204+
"--observation",
205+
str(observation),
206+
"--risk-preference",
207+
"GROWTH_COMPOUNDING",
208+
"--output",
209+
str(output),
210+
]
211+
),
212+
0,
213+
)
214+
result = json.loads(output.read_text(encoding="utf-8"))
215+
self.assertEqual(result["objective"]["risk_preference"], "GROWTH_COMPOUNDING")
216+
self.assertNotIn("strategy_returns", json.dumps(result))
217+
147218
def test_recommendation_digest_binds_the_frontier_and_prevents_policy_promotion_by_mutation(self):
148219
recommendation = composer.compose_long_horizon_risk_recommendation(self._input())
149220
tampered = copy.deepcopy(recommendation)

0 commit comments

Comments
 (0)