Skip to content

Commit 8fe855f

Browse files
authored
Merge pull request #502 from QuantStrategyLab/feat/runtime-target-decision-data
feat(runtime): preserve verified decision-data bindings
2 parents 8258fc2 + fd838b4 commit 8fe855f

2 files changed

Lines changed: 60 additions & 0 deletions

File tree

src/quant_platform_kit/common/runtime_target.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
from typing import Any, Mapping
88
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
99

10+
from quant_platform_kit.data.decision_data_binding import DecisionDataBinding
11+
1012
from .live_continuity import LiveContinuity, build_live_continuity
1113
from .strategy_release import StrategyReleaseIdentity, build_strategy_release_identity
1214

@@ -77,6 +79,7 @@ class RuntimeTarget:
7779
account_identity: dict[str, Any] | None = None
7880
live_continuity: LiveContinuity | None = None
7981
execution_environment: RuntimeExecutionEnvironment | str | None = None
82+
decision_data: DecisionDataBinding | None = None
8083

8184
def __post_init__(self) -> None:
8285
object.__setattr__(
@@ -102,6 +105,7 @@ def to_dict(self) -> dict[str, object]:
102105
"strategy_release",
103106
"account_identity",
104107
"live_continuity",
108+
"decision_data",
105109
):
106110
if payload.get(field) is None:
107111
payload.pop(field, None)
@@ -128,6 +132,25 @@ def _normalize_optional_string(value: str | None) -> str | None:
128132
return text or None
129133

130134

135+
def _build_decision_data_binding(
136+
value: DecisionDataBinding | Mapping[str, object] | None,
137+
) -> DecisionDataBinding | None:
138+
"""Verify the public control-plane binding before exposing it to a runtime."""
139+
140+
if value is None or isinstance(value, DecisionDataBinding):
141+
return value
142+
if not isinstance(value, Mapping):
143+
raise ValueError("decision_data must be a DecisionDataBinding or object")
144+
payload = dict(value)
145+
claimed_digest = str(payload.pop("binding_sha256", "") or "").strip().lower()
146+
if claimed_digest.startswith("sha256:"):
147+
claimed_digest = claimed_digest.removeprefix("sha256:")
148+
binding = DecisionDataBinding.from_dict(payload)
149+
if claimed_digest and claimed_digest != binding.binding_sha256:
150+
raise ValueError("decision_data.binding_sha256 does not match the binding")
151+
return binding
152+
153+
131154
def _normalize_account_selector(value: Iterable[str] | str | None) -> tuple[str, ...]:
132155
if value is None:
133156
return ()
@@ -183,6 +206,7 @@ def build_runtime_target(
183206
account_identity: Mapping[str, Any] | None = None,
184207
execution_environment: RuntimeExecutionEnvironment | str | None = None,
185208
live_continuity: LiveContinuity | Mapping[str, object] | None = None,
209+
decision_data: DecisionDataBinding | Mapping[str, object] | None = None,
186210
continuity_fingerprint_payload: Mapping[str, Any] | None = None,
187211
) -> RuntimeTarget:
188212
normalized_market, normalized_calendar, normalized_timezone = _normalize_market_metadata(
@@ -210,6 +234,7 @@ def build_runtime_target(
210234
),
211235
account_identity=dict(account_identity) if account_identity is not None else None,
212236
execution_environment=execution_environment,
237+
decision_data=_build_decision_data_binding(decision_data),
213238
)
214239
if live_continuity is None:
215240
return target
@@ -295,6 +320,9 @@ def resolve_runtime_target_from_env(
295320
live_continuity = payload.get("live_continuity")
296321
if live_continuity is not None and not isinstance(live_continuity, dict):
297322
raise ValueError("RUNTIME_TARGET_JSON.live_continuity must be an object when present")
323+
decision_data = payload.get("decision_data")
324+
if decision_data is not None and not isinstance(decision_data, dict):
325+
raise ValueError("RUNTIME_TARGET_JSON.decision_data must be an object when present")
298326

299327
return build_runtime_target(
300328
platform_id=resolved_platform_id,
@@ -313,5 +341,6 @@ def resolve_runtime_target_from_env(
313341
account_identity=account_identity,
314342
execution_environment=payload.get("execution_environment"),
315343
live_continuity=live_continuity,
344+
decision_data=decision_data,
316345
continuity_fingerprint_payload=payload,
317346
)

tests/test_runtime_target.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

33
import unittest
4+
import json
45
from types import SimpleNamespace
56

7+
from quant_platform_kit.data.decision_data_binding import DecisionDataBinding
68
from quant_platform_kit.common.runtime_target import (
79
RuntimeExecutionEnvironment,
810
RuntimeTarget,
@@ -304,6 +306,35 @@ def test_resolve_runtime_target_from_env_requires_structured_json(self) -> None:
304306
with self.assertRaisesRegex(EnvironmentError, "RUNTIME_TARGET_JSON"):
305307
resolve_runtime_target_from_env(env={})
306308

309+
def test_runtime_target_preserves_a_public_safe_decision_data_binding(self) -> None:
310+
binding = DecisionDataBinding(
311+
binding_id="soxl-p1-20260901",
312+
strategy_scope="soxl_soxx_trend_income",
313+
mode="artifact_optional",
314+
source_ids=("uesp_p1",),
315+
as_of="2026-09-01",
316+
adjustment_basis="split_adjusted",
317+
artifact_sha256="a" * 64,
318+
assurance_status="VERIFIED",
319+
)
320+
payload = binding.to_dict() | {"binding_sha256": binding.binding_sha256}
321+
target = resolve_runtime_target_from_env(
322+
env={
323+
"RUNTIME_TARGET_JSON": json.dumps(
324+
{
325+
"platform_id": "ibkr",
326+
"strategy_profile": "soxl_soxx_trend_income",
327+
"dry_run_only": True,
328+
"decision_data": payload,
329+
}
330+
)
331+
},
332+
expected_platform_id="ibkr",
333+
)
334+
335+
self.assertEqual(target.decision_data.binding_id, "soxl-p1-20260901")
336+
self.assertEqual(target.to_dict()["decision_data"]["mode"], "artifact_optional")
337+
307338
def test_build_runtime_context_fields_merges_runtime_target_without_overwriting_fields(self) -> None:
308339
target = build_runtime_target(
309340
platform_id="longbridge",

0 commit comments

Comments
 (0)