Skip to content

Commit f1d2c32

Browse files
Pigbibicodex
andauthored
require auditable plugin position control (#190)
Co-authored-by: Codex <noreply@openai.com>
1 parent d51402a commit f1d2c32

5 files changed

Lines changed: 145 additions & 16 deletions

File tree

docs/strategy_plugin_runtime_contract.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,10 @@ the matching `notification_targets` artifact. The plugin-alert stream remains
204204
for non-delegated manual-review or notification-only cases, including
205205
`notification_targets`, `blocked`, `watch_only`, and `notify_manual_review`
206206
routes.
207+
Automatic position control also requires auditable evidence: an
208+
`evidence_package_id`, a validity field such as `evidence_valid_until`, and a
209+
`bounded_budget`; if any of those are missing, treat the signal as review-only
210+
or loaded-not-applied rather than a real auto-position-control signal.
207211

208212
Platforms may still choose their delivery sinks, but shared escalation helpers
209213
are available for email, SMS, push, and Telegram:

docs/strategy_plugin_runtime_contract.zh-CN.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@ notification target;这类委托告警只从对应 `notification_targets` arti
182182
发送一次。插件告警流只保留给未委托的人工复核或 notification-only 场景,
183183
包括 `notification_targets``blocked``watch_only`
184184
`notify_manual_review` 路线。
185+
自动仓位控制还必须带可审计证据:`evidence_package_id`、有效期字段
186+
(如 `evidence_valid_until`)和 `bounded_budget`;缺少任一项时,只能按
187+
普通复核/加载态处理,不能当作真正的自动仓位信号。
185188

186189
平台仍可选择自己的投递 sink;共享 helper 已提供 email、SMS、push 和
187190
Telegram 的聚合入口:

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -925,6 +925,72 @@ def _strategy_plugin_consumption_policy(signal: StrategyPluginSignal) -> Mapping
925925
return policy if isinstance(policy, Mapping) else {}
926926

927927

928+
def _strategy_plugin_value_is_present(value: Any) -> bool:
929+
if value is None:
930+
return False
931+
if isinstance(value, bool):
932+
return value
933+
if isinstance(value, (int, float)):
934+
return value != 0
935+
if isinstance(value, str):
936+
normalized = value.strip().lower()
937+
return bool(normalized) and normalized not in {"none", "null", "unbounded", "unlimited", "infinite", "inf"}
938+
if isinstance(value, Mapping):
939+
return any(_strategy_plugin_value_is_present(item) for item in value.values())
940+
if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
941+
return any(_strategy_plugin_value_is_present(item) for item in value)
942+
return bool(value)
943+
944+
945+
def _strategy_plugin_find_field_value(value: Any, field_names: frozenset[str]) -> Any | None:
946+
if isinstance(value, Mapping):
947+
for key, item in value.items():
948+
if key in field_names and _strategy_plugin_value_is_present(item):
949+
return item
950+
found = _strategy_plugin_find_field_value(item, field_names)
951+
if found is not None:
952+
return found
953+
elif isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
954+
for item in value:
955+
found = _strategy_plugin_find_field_value(item, field_names)
956+
if found is not None:
957+
return found
958+
return None
959+
960+
961+
def _strategy_plugin_has_auditable_position_control(signal: StrategyPluginSignal) -> bool:
962+
controls = _strategy_plugin_execution_controls(signal)
963+
policy = _strategy_plugin_consumption_policy(signal)
964+
if not (
965+
_as_bool(controls.get("position_control_allowed"), default=False)
966+
or _as_bool(policy.get("position_control_allowed"), default=False)
967+
):
968+
return False
969+
if _strategy_plugin_consumption_status(signal) != "automation_approved":
970+
return False
971+
payload = getattr(signal, "payload", {}) or {}
972+
if not isinstance(payload, Mapping):
973+
return False
974+
evidence_sources: tuple[Any, ...] = (controls, payload)
975+
return all(
976+
_strategy_plugin_find_field_value(evidence_sources, field_names) is not None
977+
for field_names in (
978+
frozenset({"evidence_package_id", "evidence_package", "package_id"}),
979+
frozenset(
980+
{
981+
"evidence_valid_until",
982+
"evidence_valid_until_at",
983+
"evidence_expires_at",
984+
"valid_until",
985+
"valid_until_at",
986+
"expires_at",
987+
}
988+
),
989+
frozenset({"bounded_budget", "budget", "budget_limit"}),
990+
)
991+
)
992+
993+
928994
def _strategy_plugin_consumption_status(signal: StrategyPluginSignal) -> str:
929995
controls = _strategy_plugin_execution_controls(signal)
930996
status = _optional_string(controls.get("consumption_evidence_status"))
@@ -965,6 +1031,7 @@ def _strategy_plugin_auto_consumption_allowed(signal: StrategyPluginSignal, *, a
9651031
action in STRATEGY_PLUGIN_AUTOMATED_POSITION_ACTIONS
9661032
and _strategy_plugin_policy_allows_position_control(signal)
9671033
and _strategy_plugin_consumption_status(signal) == "automation_approved"
1034+
and _strategy_plugin_has_auditable_position_control(signal)
9681035
)
9691036

9701037

@@ -1141,10 +1208,7 @@ def _is_strategy_position_control_notice(signal: StrategyPluginSignal, *, action
11411208
return False
11421209
if not _as_bool(controls.get("strategy_runtime_metadata_allowed"), default=False):
11431210
return False
1144-
if not _as_bool(controls.get("position_control_allowed"), default=False):
1145-
return False
1146-
evidence_status = _normalize_strategy_plugin_field(controls.get("consumption_evidence_status"))
1147-
return evidence_status == "automation_approved"
1211+
return _strategy_plugin_has_auditable_position_control(signal)
11481212

11491213

11501214
def _is_strategy_manual_review_notification_delegated(signal: StrategyPluginSignal) -> bool:

tests/test_strategy_plugin_telegram_notifications.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ def _auto_consumable_signal():
4141
"strategy_runtime_metadata_allowed": True,
4242
"position_control_allowed": True,
4343
"consumption_evidence_status": "automation_approved",
44+
"evidence_package_id": "pkg_001",
45+
"evidence_valid_until": "2026-08-01T00:00:00Z",
46+
"bounded_budget": {
47+
"name": "position_control",
48+
"amount": 0.4,
49+
"unit": "fraction",
50+
},
4451
},
4552
)
4653

tests/test_strategy_plugins.py

Lines changed: 63 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,22 @@ def _signal_payload(*, strategy="tqqq_growth_income", plugin="crisis_response_sh
7272
}
7373

7474

75+
def _auditable_position_control_controls():
76+
return {
77+
**_signal_payload()["execution_controls"],
78+
"strategy_runtime_metadata_allowed": True,
79+
"position_control_allowed": True,
80+
"consumption_evidence_status": "automation_approved",
81+
"evidence_package_id": "pkg_001",
82+
"evidence_valid_until": "2026-08-01T00:00:00Z",
83+
"bounded_budget": {
84+
"name": "position_control",
85+
"amount": 0.4,
86+
"unit": "fraction",
87+
},
88+
}
89+
90+
7591
class StrategyPluginsTests(unittest.TestCase):
7692
def test_parse_strategy_plugin_mounts_uses_artifact_mode_not_platform_mode(self):
7793
with tempfile.TemporaryDirectory() as tmp_dir:
@@ -625,12 +641,7 @@ def test_strategy_plugin_notification_lines_include_auto_consumption_line(self):
625641
"canonical_route": "risk_off",
626642
"suggested_action": "defend",
627643
"would_trade_if_enabled": True,
628-
"execution_controls": {
629-
**_signal_payload()["execution_controls"],
630-
"strategy_runtime_metadata_allowed": True,
631-
"position_control_allowed": True,
632-
"consumption_evidence_status": "automation_approved",
633-
},
644+
"execution_controls": _auditable_position_control_controls(),
634645
}
635646
)
636647
translations = {
@@ -657,6 +668,44 @@ def test_strategy_plugin_notification_lines_include_auto_consumption_line(self):
657668
),
658669
)
659670

671+
def test_strategy_plugin_auto_position_control_without_auditable_evidence_is_not_auto_consumed(self):
672+
signal = validate_strategy_plugin_signal_payload(
673+
{
674+
**_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL),
675+
"canonical_route": "risk_off",
676+
"suggested_action": "defend",
677+
"would_trade_if_enabled": True,
678+
"execution_controls": {
679+
**_signal_payload()["execution_controls"],
680+
"strategy_runtime_metadata_allowed": True,
681+
"position_control_allowed": True,
682+
"consumption_evidence_status": "automation_approved",
683+
},
684+
}
685+
)
686+
translations = {
687+
"strategy_plugin_line": "plugin={plugin}|enabled={enabled}|route={route}|action={action}",
688+
"strategy_plugin_enabled_true": "yes",
689+
"strategy_plugin_name_market_regime_control": "Market Regime",
690+
"strategy_plugin_route_risk_off": "risk off",
691+
"strategy_plugin_action_defend": "defend",
692+
"strategy_plugin_consumption_loaded_not_applied": "consumption=loaded-not-applied",
693+
}
694+
695+
self.assertTrue(should_alert_strategy_plugin_signal(signal))
696+
self.assertEqual(
697+
build_strategy_plugin_notification_lines(
698+
[signal],
699+
translator=lambda key, **kwargs: translations.get(key, key).format(**kwargs)
700+
if kwargs
701+
else translations.get(key, key),
702+
),
703+
(
704+
"plugin=Market Regime|enabled=yes|route=risk off|action=defend",
705+
"consumption=loaded-not-applied",
706+
),
707+
)
708+
660709
def test_strategy_plugin_notification_lines_include_review_only_consumption_line(self):
661710
signal = validate_strategy_plugin_signal_payload(
662711
{
@@ -820,12 +869,7 @@ def test_strategy_plugin_auto_position_control_signal_stays_with_strategy_notifi
820869
"canonical_route": "risk_off",
821870
"suggested_action": "defend",
822871
"would_trade_if_enabled": True,
823-
"execution_controls": {
824-
**_signal_payload()["execution_controls"],
825-
"strategy_runtime_metadata_allowed": True,
826-
"position_control_allowed": True,
827-
"consumption_evidence_status": "automation_approved",
828-
},
872+
"execution_controls": _auditable_position_control_controls(),
829873
}
830874
)
831875

@@ -890,6 +934,13 @@ def test_delegated_manual_review_strategy_signal_stays_with_notification_target(
890934
"strategy_runtime_metadata_allowed": True,
891935
"position_control_allowed": True,
892936
"consumption_evidence_status": "automation_approved",
937+
"evidence_package_id": "pkg_001",
938+
"evidence_valid_until": "2026-08-01T00:00:00Z",
939+
"bounded_budget": {
940+
"name": "position_control",
941+
"amount": 0.4,
942+
"unit": "fraction",
943+
},
893944
"manual_review_notification_delegated": True,
894945
"manual_review_notification_target": GENERAL_MARKET_REGIME_NOTIFICATION_TARGET,
895946
"manual_review_notification_delegate": (

0 commit comments

Comments
 (0)