Skip to content

Commit 5ee38a0

Browse files
authored
Delegate strategy plugin manual alerts (#98)
1 parent d26e801 commit 5ee38a0

5 files changed

Lines changed: 129 additions & 6 deletions

File tree

docs/strategy_plugin_runtime_contract.md

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ defaults may consume `risk_off` and deterministic
9696
default. Broad market-regime notifications may still be published through the
9797
separate `notification_targets.market_regime_notification` artifact for manual
9898
review; notification-target artifacts cannot affect position sizing.
99+
When a strategy-mounted market-regime artifact carries
100+
`execution_controls.manual_review_notification_delegated = true`, platform
101+
strategy runners should treat manual-review plugin-bot delivery as delegated to
102+
that notification target. They may still attach the strategy artifact to runtime
103+
metadata and may still report any actual position effect in the strategy run
104+
notification.
99105
SOXL retention profiles may include a deterministic SOXX price/volatility
100106
rebound context. That context is backtestable hard-data evidence only and must
101107
not promote TACO, panic reversal, AI audit, OSINT, or localized copy into
@@ -190,9 +196,14 @@ Strategy-mounted artifacts that are automation-approved, expose
190196
`position_control_allowed = true`, and request an automatic `defend` or
191197
`delever` action are intentionally excluded from the dedicated plugin-alert
192198
stream. Those position-impacting events should be reported by the strategy run
193-
that consumed the artifact. The plugin-alert stream remains for manual-review
194-
or notification-only cases, including `notification_targets`, `blocked`,
195-
`watch_only`, and `notify_manual_review` routes.
199+
that consumed the artifact. Strategy artifacts can also explicitly delegate
200+
manual-review plugin-bot delivery with
201+
`execution_controls.manual_review_notification_delegated = true` plus
202+
`manual_review_notification_target`; those delegated alerts are sent once from
203+
the matching `notification_targets` artifact. The plugin-alert stream remains
204+
for non-delegated manual-review or notification-only cases, including
205+
`notification_targets`, `blocked`, `watch_only`, and `notify_manual_review`
206+
routes.
196207

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

docs/strategy_plugin_runtime_contract.zh-CN.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,10 @@ SOXL/SOXX 已列入 `market_regime_control` 的运行时挂载清单。策略默
8585
`risk_off` 和确定性的 `position_control.volatility_delever_context` retention
8686
profiles;`risk_reduced` 仓位影响仍在策略默认配置中关闭。广义市场状态通知仍可通过独立的
8787
`notification_targets.market_regime_notification` artifact 分发给人工复核;notification-target artifact 不能影响仓位。
88+
当 strategy-mounted market-regime artifact 带有
89+
`execution_controls.manual_review_notification_delegated = true` 时,平台策略
90+
runner 应把人工复核插件 bot 通知视为已委托给该 notification target。策略
91+
runner 仍可把 strategy artifact 挂入 runtime metadata,并在策略运行通知中报告实际仓位影响。
8892
SOXL retention profiles 可以包含确定性的 SOXX 价格/波动反弹上下文。该上下文只使用可回测硬数据,
8993
不能把 TACO、panic reversal、AI audit、OSINT 或本地化文案升级成自动仓位权限。
9094

@@ -171,8 +175,12 @@ sidecar 路径维护插件账本或执行插件驱动的 allocation 变更。
171175
如果 strategy-mounted artifact 已经是 `automation_approved`、暴露
172176
`position_control_allowed = true`,并且请求自动 `defend``delever`
173177
动作,则专用插件告警流会刻意跳过它。这类会影响仓位的事件应由实际消费该
174-
artifact 的策略运行结果通知。插件告警流只保留给人工复核或 notification-only
175-
场景,包括 `notification_targets``blocked``watch_only`
178+
artifact 的策略运行结果通知。strategy artifact 也可以通过
179+
`execution_controls.manual_review_notification_delegated = true`
180+
`manual_review_notification_target` 明确把人工复核插件 bot 通知委托给统一
181+
notification target;这类委托告警只从对应 `notification_targets` artifact
182+
发送一次。插件告警流只保留给未委托的人工复核或 notification-only 场景,
183+
包括 `notification_targets``blocked``watch_only`
176184
`notify_manual_review` 路线。
177185

178186
平台仍可选择自己的投递 sink;共享 helper 已提供 email、SMS、push 和

src/quant_platform_kit/common/notification_localization.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@
3030
"strategy_plugin_alert_subject": "🚨 策略插件告警:{plugin} | {route}",
3131
"strategy_plugin_alert_title": "🚨 【策略插件告警】",
3232
"strategy_plugin_alert_context": "运行环境:{context}",
33+
"strategy_plugin_alert_target": "{target_name}:{target}",
34+
"strategy_plugin_alert_target_name_strategy": "策略",
35+
"strategy_plugin_alert_target_name_notification_target": "通知目标",
3336
"strategy_plugin_alert_strategy": "策略:{strategy}",
3437
"strategy_plugin_alert_plugin": "插件:{plugin}",
3538
"strategy_plugin_alert_status": "状态:{route}",
@@ -80,6 +83,9 @@
8083
"strategy_plugin_alert_subject": "🚨 Strategy plugin alert: {plugin} | {route}",
8184
"strategy_plugin_alert_title": "🚨 【Strategy Plugin Alert】",
8285
"strategy_plugin_alert_context": "Context: {context}",
86+
"strategy_plugin_alert_target": "{target_name}: {target}",
87+
"strategy_plugin_alert_target_name_strategy": "Strategy",
88+
"strategy_plugin_alert_target_name_notification_target": "Notification target",
8389
"strategy_plugin_alert_strategy": "Strategy: {strategy}",
8490
"strategy_plugin_alert_plugin": "Plugin: {plugin}",
8591
"strategy_plugin_alert_status": "Status: {route}",

src/quant_platform_kit/common/strategy_plugins.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,8 @@ def should_alert_strategy_plugin_signal(signal: StrategyPluginSignal) -> bool:
933933
action = _normalize_strategy_plugin_field(getattr(signal, "suggested_action", None))
934934
if _is_strategy_position_control_notice(signal, action=action):
935935
return False
936+
if _is_strategy_manual_review_notification_delegated(signal):
937+
return False
936938
return (
937939
bool(getattr(signal, "would_trade_if_enabled", False))
938940
or route not in STRATEGY_PLUGIN_NON_ALERT_ROUTES
@@ -962,6 +964,21 @@ def _is_strategy_position_control_notice(signal: StrategyPluginSignal, *, action
962964
return evidence_status == "automation_approved"
963965

964966

967+
def _is_strategy_manual_review_notification_delegated(signal: StrategyPluginSignal) -> bool:
968+
"""Return true when a strategy artifact delegates human-review alerts to a notification target."""
969+
970+
target_type = _normalize_strategy_plugin_field(getattr(signal, "target_type", None)) or "strategy"
971+
if target_type != "strategy":
972+
return False
973+
controls = getattr(signal, "execution_controls", {}) or {}
974+
if not isinstance(controls, Mapping):
975+
return False
976+
if not _as_bool(controls.get("manual_review_notification_delegated"), default=False):
977+
return False
978+
notification_target = _optional_string(controls.get("manual_review_notification_target"))
979+
return notification_target is not None
980+
981+
965982
def build_strategy_plugin_alert_guidance(
966983
signal: StrategyPluginSignal,
967984
*,
@@ -1124,7 +1141,16 @@ def build_strategy_plugin_alert_messages(
11241141
strategy = str(strategy_label or getattr(signal, "strategy", None) or "").strip()
11251142
notification_target = str(getattr(signal, "notification_target", None) or "").strip()
11261143
target_label = strategy or notification_target or "unknown"
1127-
target_name = "Notification target" if target_type == "notification_target" else "Strategy"
1144+
target_name_key = (
1145+
"strategy_plugin_alert_target_name_notification_target"
1146+
if target_type == "notification_target"
1147+
else "strategy_plugin_alert_target_name_strategy"
1148+
)
1149+
target_name = _translate(
1150+
translator,
1151+
target_name_key,
1152+
fallback="Notification target" if target_type == "notification_target" else "Strategy",
1153+
)
11281154
guidance = build_strategy_plugin_alert_guidance(signal, translator=translator)
11291155
scope_note = build_strategy_plugin_alert_scope_note(signal, translator=translator)
11301156
ai_audit_note = build_strategy_plugin_ai_audit_note(signal, translator=translator)

tests/test_strategy_plugins.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,78 @@ def test_strategy_plugin_manual_review_strategy_signal_still_alerts_plugin_bot(s
728728
self.assertEqual(len(alerts), 1)
729729
self.assertIn("Manual review only", alerts[0].body)
730730

731+
def test_delegated_manual_review_strategy_signal_stays_with_notification_target(self):
732+
signal = validate_strategy_plugin_signal_payload(
733+
{
734+
**_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL),
735+
"canonical_route": "opportunity_watch",
736+
"suggested_action": "notify_manual_review",
737+
"would_trade_if_enabled": False,
738+
"execution_controls": {
739+
**_signal_payload()["execution_controls"],
740+
"strategy_runtime_metadata_allowed": True,
741+
"position_control_allowed": True,
742+
"consumption_evidence_status": "automation_approved",
743+
"manual_review_notification_delegated": True,
744+
"manual_review_notification_target": GENERAL_MARKET_REGIME_NOTIFICATION_TARGET,
745+
"manual_review_notification_delegate": (
746+
f"notification_target:{GENERAL_MARKET_REGIME_NOTIFICATION_TARGET}"
747+
),
748+
},
749+
}
750+
)
751+
752+
self.assertFalse(should_alert_strategy_plugin_signal(signal))
753+
self.assertEqual(build_strategy_plugin_alert_messages([signal]), ())
754+
755+
def test_notification_target_alert_uses_localized_target_name(self):
756+
signal = validate_strategy_plugin_signal_payload(
757+
{
758+
**_signal_payload(plugin=PLUGIN_MARKET_REGIME_CONTROL),
759+
"target_type": "notification_target",
760+
"strategy": "",
761+
"notification_target": GENERAL_MARKET_REGIME_NOTIFICATION_TARGET,
762+
"canonical_route": "watch",
763+
"suggested_action": "notify_manual_review",
764+
"would_trade_if_enabled": False,
765+
"execution_controls": {
766+
**_signal_payload()["execution_controls"],
767+
"strategy_runtime_metadata_allowed": False,
768+
"position_control_allowed": False,
769+
"consumption_evidence_status": "notification_only",
770+
"capital_impact": "notification_only",
771+
},
772+
}
773+
)
774+
translations = {
775+
"strategy_plugin_alert_subject": "告警:{plugin}:{route}",
776+
"strategy_plugin_alert_title": "插件告警",
777+
"strategy_plugin_alert_target": "{target_name}={target}",
778+
"strategy_plugin_alert_target_name_notification_target": "通知目标",
779+
"strategy_plugin_alert_plugin": "插件={plugin}",
780+
"strategy_plugin_alert_status": "状态={route}",
781+
"strategy_plugin_alert_action": "建议={action}",
782+
"strategy_plugin_alert_mode": "模式={mode}",
783+
"strategy_plugin_alert_as_of": "时间={as_of}",
784+
"strategy_plugin_alert_scope_note": "范围={scope_note}",
785+
"strategy_plugin_alert_scope": "只通知人工复核",
786+
"strategy_plugin_name_market_regime_control": "市场状态控制通知",
787+
"strategy_plugin_mode_shadow": "影子观察",
788+
"strategy_plugin_route_watch": "观察",
789+
"strategy_plugin_action_notify_manual_review": "通知人工复核",
790+
}
791+
792+
alerts = build_strategy_plugin_alert_messages(
793+
[signal],
794+
translator=lambda key, **kwargs: translations.get(key, key).format(**kwargs)
795+
if kwargs
796+
else translations.get(key, key),
797+
)
798+
799+
self.assertEqual(len(alerts), 1)
800+
self.assertIn("通知目标=market_regime_notification", alerts[0].body)
801+
self.assertNotIn("Notification target", alerts[0].body)
802+
731803
def test_strategy_plugin_true_crisis_builds_generic_alert_message(self):
732804
signal = validate_strategy_plugin_signal_payload(
733805
{

0 commit comments

Comments
 (0)